1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.tuner.ts; 18 19 import android.util.Log; 20 import android.util.SparseArray; 21 import android.util.SparseBooleanArray; 22 import com.android.tv.tuner.data.PsiData.PatItem; 23 import com.android.tv.tuner.data.PsiData.PmtItem; 24 import com.android.tv.tuner.data.PsipData.EitItem; 25 import com.android.tv.tuner.data.PsipData.EttItem; 26 import com.android.tv.tuner.data.PsipData.MgtItem; 27 import com.android.tv.tuner.data.PsipData.SdtItem; 28 import com.android.tv.tuner.data.PsipData.VctItem; 29 import com.android.tv.tuner.data.SectionParser; 30 import com.android.tv.tuner.data.SectionParser.OutputListener; 31 import com.android.tv.tuner.data.TunerChannel; 32 import com.android.tv.tuner.util.ByteArrayBuffer; 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.HashMap; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.TreeSet; 39 40 /** Parses MPEG-2 TS packets. */ 41 public class TsParser { 42 private static final String TAG = "TsParser"; 43 private static final boolean DEBUG = false; 44 45 public static final int ATSC_SI_BASE_PID = 0x1ffb; 46 public static final int PAT_PID = 0x0000; 47 public static final int DVB_SDT_PID = 0x0011; 48 public static final int DVB_EIT_PID = 0x0012; 49 private static final int TS_PACKET_START_CODE = 0x47; 50 private static final int TS_PACKET_TEI_MASK = 0x80; 51 private static final int TS_PACKET_SIZE = 188; 52 53 /* 54 * Using a SparseArray removes the need to auto box the int key for mStreamMap 55 * in feedTdPacket which is called 100 times a second. This greatly reduces the 56 * number of objects created and the frequency of garbage collection. 57 * Other maps might be suitable for a SparseArray, but the performance 58 * trade offs must be considered carefully. 59 * mStreamMap is the only one called at such a high rate. 60 */ 61 private final SparseArray<Stream> mStreamMap = new SparseArray<>(); 62 private final Map<Integer, VctItem> mSourceIdToVctItemMap = new HashMap<>(); 63 private final Map<Integer, String> mSourceIdToVctItemDescriptionMap = new HashMap<>(); 64 private final Map<Integer, VctItem> mProgramNumberToVctItemMap = new HashMap<>(); 65 private final Map<Integer, List<PmtItem>> mProgramNumberToPMTMap = new HashMap<>(); 66 private final Map<Integer, List<EitItem>> mSourceIdToEitMap = new HashMap<>(); 67 private final Map<Integer, SdtItem> mProgramNumberToSdtItemMap = new HashMap<>(); 68 private final Map<EventSourceEntry, List<EitItem>> mEitMap = new HashMap<>(); 69 private final Map<EventSourceEntry, List<EttItem>> mETTMap = new HashMap<>(); 70 private final TreeSet<Integer> mEITPids = new TreeSet<>(); 71 private final TreeSet<Integer> mETTPids = new TreeSet<>(); 72 private final SparseBooleanArray mProgramNumberHandledStatus = new SparseBooleanArray(); 73 private final SparseBooleanArray mVctItemHandledStatus = new SparseBooleanArray(); 74 private final TsOutputListener mListener; 75 private final boolean mIsDvbSignal; 76 77 private int mVctItemCount; 78 private int mHandledVctItemCount; 79 private int mVctSectionParsedCount; 80 private boolean[] mVctSectionParsed; 81 82 public interface TsOutputListener { onPatDetected(List<PatItem> items)83 void onPatDetected(List<PatItem> items); 84 onEitPidDetected(int pid)85 void onEitPidDetected(int pid); 86 onVctItemParsed(VctItem channel, List<PmtItem> pmtItems)87 void onVctItemParsed(VctItem channel, List<PmtItem> pmtItems); 88 onEitItemParsed(VctItem channel, List<EitItem> items)89 void onEitItemParsed(VctItem channel, List<EitItem> items); 90 onEttPidDetected(int pid)91 void onEttPidDetected(int pid); 92 onAllVctItemsParsed()93 void onAllVctItemsParsed(); 94 onSdtItemParsed(SdtItem channel, List<PmtItem> pmtItems)95 void onSdtItemParsed(SdtItem channel, List<PmtItem> pmtItems); 96 } 97 98 private abstract static class Stream { 99 private static final int INVALID_CONTINUITY_COUNTER = -1; 100 private static final int NUM_CONTINUITY_COUNTER = 16; 101 102 protected int mContinuityCounter = INVALID_CONTINUITY_COUNTER; 103 protected final ByteArrayBuffer mPacket = new ByteArrayBuffer(TS_PACKET_SIZE); 104 feedData(byte[] data, int continuityCounter, boolean startIndicator)105 public void feedData(byte[] data, int continuityCounter, boolean startIndicator) { 106 if ((mContinuityCounter + 1) % NUM_CONTINUITY_COUNTER != continuityCounter) { 107 mPacket.setLength(0); 108 } 109 mContinuityCounter = continuityCounter; 110 handleData(data, startIndicator); 111 } 112 handleData(byte[] data, boolean startIndicator)113 protected abstract void handleData(byte[] data, boolean startIndicator); 114 resetDataVersions()115 protected abstract void resetDataVersions(); 116 } 117 118 private class SectionStream extends Stream { 119 private final SectionParser mSectionParser; 120 private final int mPid; 121 SectionStream(int pid)122 public SectionStream(int pid) { 123 mPid = pid; 124 mSectionParser = new SectionParser(mSectionListener); 125 } 126 127 @Override handleData(byte[] data, boolean startIndicator)128 protected void handleData(byte[] data, boolean startIndicator) { 129 int startPos = 0; 130 if (mPacket.length() == 0) { 131 if (startIndicator) { 132 startPos = (data[0] & 0xff) + 1; 133 } else { 134 // Don't know where the section starts yet. Wait until start indicator is on. 135 return; 136 } 137 } else { 138 if (startIndicator) { 139 startPos = 1; 140 } 141 } 142 143 // When a broken packet is encountered, parsing will stop and return right away. 144 if (startPos >= data.length) { 145 mPacket.setLength(0); 146 return; 147 } 148 mPacket.append(data, startPos, data.length - startPos); 149 mSectionParser.parseSections(mPacket); 150 } 151 152 @Override resetDataVersions()153 protected void resetDataVersions() { 154 mSectionParser.resetVersionNumbers(); 155 } 156 157 private final OutputListener mSectionListener = 158 new OutputListener() { 159 @Override 160 public void onPatParsed(List<PatItem> items) { 161 for (PatItem i : items) { 162 startListening(i.getPmtPid()); 163 } 164 if (mListener != null) { 165 mListener.onPatDetected(items); 166 } 167 } 168 169 @Override 170 public void onPmtParsed(int programNumber, List<PmtItem> items) { 171 mProgramNumberToPMTMap.put(programNumber, items); 172 if (DEBUG) { 173 Log.d( 174 TAG, 175 "onPMTParsed, programNo " 176 + programNumber 177 + " handledStatus is " 178 + mProgramNumberHandledStatus.get( 179 programNumber, false)); 180 } 181 int statusIndex = mProgramNumberHandledStatus.indexOfKey(programNumber); 182 if (statusIndex < 0) { 183 mProgramNumberHandledStatus.put(programNumber, false); 184 } 185 if (!mProgramNumberHandledStatus.get(programNumber)) { 186 VctItem vctItem = mProgramNumberToVctItemMap.get(programNumber); 187 if (vctItem != null) { 188 // When PMT is parsed later than VCT. 189 mProgramNumberHandledStatus.put(programNumber, true); 190 handleVctItem(vctItem, items); 191 mHandledVctItemCount++; 192 if (mHandledVctItemCount >= mVctItemCount 193 && mVctSectionParsedCount >= mVctSectionParsed.length 194 && mListener != null) { 195 mListener.onAllVctItemsParsed(); 196 } 197 } 198 SdtItem sdtItem = mProgramNumberToSdtItemMap.get(programNumber); 199 if (sdtItem != null) { 200 // When PMT is parsed later than SDT. 201 mProgramNumberHandledStatus.put(programNumber, true); 202 handleSdtItem(sdtItem, items); 203 } 204 } 205 } 206 207 @Override 208 public void onMgtParsed(List<MgtItem> items) { 209 for (MgtItem i : items) { 210 if (mStreamMap.get(i.getTableTypePid()) != null) { 211 continue; 212 } 213 if (i.getTableType() >= MgtItem.TABLE_TYPE_EIT_RANGE_START 214 && i.getTableType() <= MgtItem.TABLE_TYPE_EIT_RANGE_END) { 215 startListening(i.getTableTypePid()); 216 mEITPids.add(i.getTableTypePid()); 217 if (mListener != null) { 218 mListener.onEitPidDetected(i.getTableTypePid()); 219 } 220 } else if (i.getTableType() == MgtItem.TABLE_TYPE_CHANNEL_ETT 221 || (i.getTableType() >= MgtItem.TABLE_TYPE_ETT_RANGE_START 222 && i.getTableType() 223 <= MgtItem.TABLE_TYPE_ETT_RANGE_END)) { 224 startListening(i.getTableTypePid()); 225 mETTPids.add(i.getTableTypePid()); 226 if (mListener != null) { 227 mListener.onEttPidDetected(i.getTableTypePid()); 228 } 229 } 230 } 231 } 232 233 @Override 234 public void onVctParsed( 235 List<VctItem> items, int sectionNumber, int lastSectionNumber) { 236 if (mVctSectionParsed == null) { 237 mVctSectionParsed = new boolean[lastSectionNumber + 1]; 238 } else if (mVctSectionParsed[sectionNumber]) { 239 // The current section was handled before. 240 if (DEBUG) { 241 Log.d(TAG, "Duplicate VCT section found."); 242 } 243 return; 244 } 245 mVctSectionParsed[sectionNumber] = true; 246 mVctSectionParsedCount++; 247 mVctItemCount += items.size(); 248 for (VctItem i : items) { 249 if (DEBUG) Log.d(TAG, "onVCTParsed " + i); 250 if (i.getSourceId() != 0) { 251 mSourceIdToVctItemMap.put(i.getSourceId(), i); 252 i.setDescription( 253 mSourceIdToVctItemDescriptionMap.get(i.getSourceId())); 254 } 255 int programNumber = i.getProgramNumber(); 256 mProgramNumberToVctItemMap.put(programNumber, i); 257 List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber); 258 if (pmtList != null) { 259 mProgramNumberHandledStatus.put(programNumber, true); 260 handleVctItem(i, pmtList); 261 mHandledVctItemCount++; 262 if (mHandledVctItemCount >= mVctItemCount 263 && mVctSectionParsedCount >= mVctSectionParsed.length 264 && mListener != null) { 265 mListener.onAllVctItemsParsed(); 266 } 267 } else { 268 mProgramNumberHandledStatus.put(programNumber, false); 269 Log.i( 270 TAG, 271 "onVCTParsed, but PMT for programNo " 272 + programNumber 273 + " is not found yet."); 274 } 275 } 276 } 277 278 @Override 279 public void onEitParsed(int sourceId, List<EitItem> items) { 280 if (DEBUG) Log.d(TAG, "onEITParsed " + sourceId); 281 EventSourceEntry entry = new EventSourceEntry(mPid, sourceId); 282 mEitMap.put(entry, items); 283 handleEvents(sourceId); 284 } 285 286 @Override 287 public void onEttParsed(int sourceId, List<EttItem> descriptions) { 288 if (DEBUG) { 289 Log.d( 290 TAG, 291 String.format( 292 "onETTParsed sourceId: %d, descriptions.size(): %d", 293 sourceId, descriptions.size())); 294 } 295 for (EttItem item : descriptions) { 296 if (item.eventId == 0) { 297 // Channel description 298 mSourceIdToVctItemDescriptionMap.put(sourceId, item.text); 299 VctItem vctItem = mSourceIdToVctItemMap.get(sourceId); 300 if (vctItem != null) { 301 vctItem.setDescription(item.text); 302 List<PmtItem> pmtItems = 303 mProgramNumberToPMTMap.get(vctItem.getProgramNumber()); 304 if (pmtItems != null) { 305 handleVctItem(vctItem, pmtItems); 306 } 307 } 308 } 309 } 310 311 // Event Information description 312 EventSourceEntry entry = new EventSourceEntry(mPid, sourceId); 313 mETTMap.put(entry, descriptions); 314 handleEvents(sourceId); 315 } 316 317 @Override 318 public void onSdtParsed(List<SdtItem> sdtItems) { 319 for (SdtItem sdtItem : sdtItems) { 320 if (DEBUG) Log.d(TAG, "onSdtParsed " + sdtItem); 321 int programNumber = sdtItem.getServiceId(); 322 mProgramNumberToSdtItemMap.put(programNumber, sdtItem); 323 List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber); 324 if (pmtList != null) { 325 mProgramNumberHandledStatus.put(programNumber, true); 326 handleSdtItem(sdtItem, pmtList); 327 } else { 328 mProgramNumberHandledStatus.put(programNumber, false); 329 Log.i( 330 TAG, 331 "onSdtParsed, but PMT for programNo " 332 + programNumber 333 + " is not found yet."); 334 } 335 } 336 } 337 }; 338 } 339 340 private static class EventSourceEntry { 341 public final int pid; 342 public final int sourceId; 343 EventSourceEntry(int pid, int sourceId)344 public EventSourceEntry(int pid, int sourceId) { 345 this.pid = pid; 346 this.sourceId = sourceId; 347 } 348 349 @Override hashCode()350 public int hashCode() { 351 int result = 17; 352 result = 31 * result + pid; 353 result = 31 * result + sourceId; 354 return result; 355 } 356 357 @Override equals(Object obj)358 public boolean equals(Object obj) { 359 if (obj instanceof EventSourceEntry) { 360 EventSourceEntry another = (EventSourceEntry) obj; 361 return pid == another.pid && sourceId == another.sourceId; 362 } 363 return false; 364 } 365 } 366 handleVctItem(VctItem channel, List<PmtItem> pmtItems)367 private void handleVctItem(VctItem channel, List<PmtItem> pmtItems) { 368 if (DEBUG) { 369 Log.d(TAG, "handleVctItem " + channel); 370 } 371 if (mListener != null) { 372 mListener.onVctItemParsed(channel, pmtItems); 373 } 374 int sourceId = channel.getSourceId(); 375 int statusIndex = mVctItemHandledStatus.indexOfKey(sourceId); 376 if (statusIndex < 0) { 377 mVctItemHandledStatus.put(sourceId, false); 378 return; 379 } 380 if (!mVctItemHandledStatus.valueAt(statusIndex)) { 381 List<EitItem> eitItems = mSourceIdToEitMap.get(sourceId); 382 if (eitItems != null) { 383 // When VCT is parsed later than EIT. 384 mVctItemHandledStatus.put(sourceId, true); 385 handleEitItems(channel, eitItems); 386 } 387 } 388 } 389 handleEitItems(VctItem channel, List<EitItem> items)390 private void handleEitItems(VctItem channel, List<EitItem> items) { 391 if (mListener != null) { 392 mListener.onEitItemParsed(channel, items); 393 } 394 } 395 handleSdtItem(SdtItem channel, List<PmtItem> pmtItems)396 private void handleSdtItem(SdtItem channel, List<PmtItem> pmtItems) { 397 if (DEBUG) { 398 Log.d(TAG, "handleSdtItem " + channel); 399 } 400 if (mListener != null) { 401 mListener.onSdtItemParsed(channel, pmtItems); 402 } 403 } 404 handleEvents(int sourceId)405 private void handleEvents(int sourceId) { 406 Map<Integer, EitItem> itemSet = new HashMap<>(); 407 for (int pid : mEITPids) { 408 List<EitItem> eitItems = mEitMap.get(new EventSourceEntry(pid, sourceId)); 409 if (eitItems != null) { 410 for (EitItem item : eitItems) { 411 item.setDescription(null); 412 itemSet.put(item.getEventId(), item); 413 } 414 } 415 } 416 for (int pid : mETTPids) { 417 List<EttItem> ettItems = mETTMap.get(new EventSourceEntry(pid, sourceId)); 418 if (ettItems != null) { 419 for (EttItem ettItem : ettItems) { 420 if (ettItem.eventId != 0) { 421 EitItem item = itemSet.get(ettItem.eventId); 422 if (item != null) { 423 item.setDescription(ettItem.text); 424 } 425 } 426 } 427 } 428 } 429 List<EitItem> items = new ArrayList<>(itemSet.values()); 430 mSourceIdToEitMap.put(sourceId, items); 431 VctItem channel = mSourceIdToVctItemMap.get(sourceId); 432 if (channel != null && mProgramNumberHandledStatus.get(channel.getProgramNumber())) { 433 mVctItemHandledStatus.put(sourceId, true); 434 handleEitItems(channel, items); 435 } else { 436 mVctItemHandledStatus.put(sourceId, false); 437 if (!mIsDvbSignal) { 438 // Log only when zapping to non-DVB channels, since there is not VCT in DVB signal. 439 Log.i(TAG, "onEITParsed, but VCT for sourceId " + sourceId + " is not found yet."); 440 } 441 } 442 } 443 444 /** 445 * Creates MPEG-2 TS parser. 446 * 447 * @param listener TsOutputListener 448 */ TsParser(TsOutputListener listener, boolean isDvbSignal)449 public TsParser(TsOutputListener listener, boolean isDvbSignal) { 450 startListening(PAT_PID); 451 startListening(ATSC_SI_BASE_PID); 452 mIsDvbSignal = isDvbSignal; 453 if (isDvbSignal) { 454 startListening(DVB_EIT_PID); 455 startListening(DVB_SDT_PID); 456 } 457 mListener = listener; 458 } 459 startListening(int pid)460 private void startListening(int pid) { 461 mStreamMap.put(pid, new SectionStream(pid)); 462 } 463 feedTSPacket(byte[] tsData, int pos)464 private boolean feedTSPacket(byte[] tsData, int pos) { 465 if (tsData.length < pos + TS_PACKET_SIZE) { 466 if (DEBUG) Log.d(TAG, "Data should include a single TS packet."); 467 return false; 468 } 469 if (tsData[pos] != TS_PACKET_START_CODE) { 470 if (DEBUG) Log.d(TAG, "Invalid ts packet."); 471 return false; 472 } 473 if ((tsData[pos + 1] & TS_PACKET_TEI_MASK) != 0) { 474 if (DEBUG) Log.d(TAG, "Erroneous ts packet."); 475 return false; 476 } 477 478 // For details for the structure of TS packet, see H.222.0 Table 2-2. 479 int pid = ((tsData[pos + 1] & 0x1f) << 8) | (tsData[pos + 2] & 0xff); 480 boolean hasAdaptation = (tsData[pos + 3] & 0x20) != 0; 481 boolean hasPayload = (tsData[pos + 3] & 0x10) != 0; 482 boolean payloadStartIndicator = (tsData[pos + 1] & 0x40) != 0; 483 int continuityCounter = tsData[pos + 3] & 0x0f; 484 Stream stream = mStreamMap.get(pid); 485 int payloadPos = pos; 486 payloadPos += hasAdaptation ? 5 + (tsData[pos + 4] & 0xff) : 4; 487 if (!hasPayload || stream == null) { 488 // We are not interested in this packet. 489 return false; 490 } 491 if (payloadPos >= pos + TS_PACKET_SIZE) { 492 if (DEBUG) Log.d(TAG, "Payload should be included in a single TS packet."); 493 return false; 494 } 495 stream.feedData( 496 Arrays.copyOfRange(tsData, payloadPos, pos + TS_PACKET_SIZE), 497 continuityCounter, 498 payloadStartIndicator); 499 return true; 500 } 501 502 /** 503 * Feeds MPEG-2 TS data to parse. 504 * 505 * @param tsData buffer for ATSC TS stream 506 * @param pos the offset where buffer starts 507 * @param length The length of available data 508 */ feedTSData(byte[] tsData, int pos, int length)509 public void feedTSData(byte[] tsData, int pos, int length) { 510 for (; pos <= length - TS_PACKET_SIZE; pos += TS_PACKET_SIZE) { 511 feedTSPacket(tsData, pos); 512 } 513 } 514 515 /** 516 * Retrieves the channel information regardless of being well-formed. 517 * 518 * @return {@link List} of {@link TunerChannel} 519 */ getMalFormedChannels()520 public List<TunerChannel> getMalFormedChannels() { 521 List<TunerChannel> incompleteChannels = new ArrayList<>(); 522 for (int i = 0; i < mProgramNumberHandledStatus.size(); i++) { 523 if (!mProgramNumberHandledStatus.valueAt(i)) { 524 int programNumber = mProgramNumberHandledStatus.keyAt(i); 525 List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber); 526 if (pmtList != null) { 527 TunerChannel tunerChannel = new TunerChannel(programNumber, pmtList); 528 incompleteChannels.add(tunerChannel); 529 } 530 } 531 } 532 return incompleteChannels; 533 } 534 535 /** Reset the versions so that data with old version number can be handled. */ resetDataVersions()536 public void resetDataVersions() { 537 for (int eitPid : mEITPids) { 538 Stream stream = mStreamMap.get(eitPid); 539 if (stream != null) { 540 stream.resetDataVersions(); 541 } 542 } 543 } 544 } 545