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