aboutsummaryrefslogtreecommitdiff
path: root/tuner/src/com/android/tv/tuner/ts/TsParser.java
diff options
context:
space:
mode:
Diffstat (limited to 'tuner/src/com/android/tv/tuner/ts/TsParser.java')
-rw-r--r--tuner/src/com/android/tv/tuner/ts/TsParser.java543
1 files changed, 543 insertions, 0 deletions
diff --git a/tuner/src/com/android/tv/tuner/ts/TsParser.java b/tuner/src/com/android/tv/tuner/ts/TsParser.java
new file mode 100644
index 00000000..2307c22a
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/ts/TsParser.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.ts;
+
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import com.android.tv.tuner.data.PsiData.PatItem;
+import com.android.tv.tuner.data.PsiData.PmtItem;
+import com.android.tv.tuner.data.PsipData.EitItem;
+import com.android.tv.tuner.data.PsipData.EttItem;
+import com.android.tv.tuner.data.PsipData.MgtItem;
+import com.android.tv.tuner.data.PsipData.SdtItem;
+import com.android.tv.tuner.data.PsipData.VctItem;
+import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.ts.SectionParser.OutputListener;
+import com.android.tv.tuner.util.ByteArrayBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+
+/** Parses MPEG-2 TS packets. */
+public class TsParser {
+ private static final String TAG = "TsParser";
+ private static final boolean DEBUG = false;
+
+ public static final int ATSC_SI_BASE_PID = 0x1ffb;
+ public static final int PAT_PID = 0x0000;
+ public static final int DVB_SDT_PID = 0x0011;
+ public static final int DVB_EIT_PID = 0x0012;
+ private static final int TS_PACKET_START_CODE = 0x47;
+ private static final int TS_PACKET_TEI_MASK = 0x80;
+ private static final int TS_PACKET_SIZE = 188;
+
+ /*
+ * Using a SparseArray removes the need to auto box the int key for mStreamMap
+ * in feedTdPacket which is called 100 times a second. This greatly reduces the
+ * number of objects created and the frequency of garbage collection.
+ * Other maps might be suitable for a SparseArray, but the performance
+ * trade offs must be considered carefully.
+ * mStreamMap is the only one called at such a high rate.
+ */
+ private final SparseArray<Stream> mStreamMap = new SparseArray<>();
+ private final Map<Integer, VctItem> mSourceIdToVctItemMap = new HashMap<>();
+ private final Map<Integer, String> mSourceIdToVctItemDescriptionMap = new HashMap<>();
+ private final Map<Integer, VctItem> mProgramNumberToVctItemMap = new HashMap<>();
+ private final Map<Integer, List<PmtItem>> mProgramNumberToPMTMap = new HashMap<>();
+ private final Map<Integer, List<EitItem>> mSourceIdToEitMap = new HashMap<>();
+ private final Map<Integer, SdtItem> mProgramNumberToSdtItemMap = new HashMap<>();
+ private final Map<EventSourceEntry, List<EitItem>> mEitMap = new HashMap<>();
+ private final Map<EventSourceEntry, List<EttItem>> mETTMap = new HashMap<>();
+ private final TreeSet<Integer> mEITPids = new TreeSet<>();
+ private final TreeSet<Integer> mETTPids = new TreeSet<>();
+ private final SparseBooleanArray mProgramNumberHandledStatus = new SparseBooleanArray();
+ private final SparseBooleanArray mVctItemHandledStatus = new SparseBooleanArray();
+ private final TsOutputListener mListener;
+ private final boolean mIsDvbSignal;
+
+ private int mVctItemCount;
+ private int mHandledVctItemCount;
+ private int mVctSectionParsedCount;
+ private boolean[] mVctSectionParsed;
+
+ public interface TsOutputListener {
+ void onPatDetected(List<PatItem> items);
+
+ void onEitPidDetected(int pid);
+
+ void onVctItemParsed(VctItem channel, List<PmtItem> pmtItems);
+
+ void onEitItemParsed(VctItem channel, List<EitItem> items);
+
+ void onEttPidDetected(int pid);
+
+ void onAllVctItemsParsed();
+
+ void onSdtItemParsed(SdtItem channel, List<PmtItem> pmtItems);
+ }
+
+ private abstract static class Stream {
+ private static final int INVALID_CONTINUITY_COUNTER = -1;
+ private static final int NUM_CONTINUITY_COUNTER = 16;
+
+ protected int mContinuityCounter = INVALID_CONTINUITY_COUNTER;
+ protected final ByteArrayBuffer mPacket = new ByteArrayBuffer(TS_PACKET_SIZE);
+
+ public void feedData(byte[] data, int continuityCounter, boolean startIndicator) {
+ if ((mContinuityCounter + 1) % NUM_CONTINUITY_COUNTER != continuityCounter) {
+ mPacket.setLength(0);
+ }
+ mContinuityCounter = continuityCounter;
+ handleData(data, startIndicator);
+ }
+
+ protected abstract void handleData(byte[] data, boolean startIndicator);
+
+ protected abstract void resetDataVersions();
+ }
+
+ private class SectionStream extends Stream {
+ private final SectionParser mSectionParser;
+ private final int mPid;
+
+ public SectionStream(int pid) {
+ mPid = pid;
+ mSectionParser = new SectionParser(mSectionListener);
+ }
+
+ @Override
+ protected void handleData(byte[] data, boolean startIndicator) {
+ int startPos = 0;
+ if (mPacket.length() == 0) {
+ if (startIndicator) {
+ startPos = (data[0] & 0xff) + 1;
+ } else {
+ // Don't know where the section starts yet. Wait until start indicator is on.
+ return;
+ }
+ } else {
+ if (startIndicator) {
+ startPos = 1;
+ }
+ }
+
+ // When a broken packet is encountered, parsing will stop and return right away.
+ if (startPos >= data.length) {
+ mPacket.setLength(0);
+ return;
+ }
+ mPacket.append(data, startPos, data.length - startPos);
+ mSectionParser.parseSections(mPacket);
+ }
+
+ @Override
+ protected void resetDataVersions() {
+ mSectionParser.resetVersionNumbers();
+ }
+
+ private final OutputListener mSectionListener =
+ new OutputListener() {
+ @Override
+ public void onPatParsed(List<PatItem> items) {
+ for (PatItem i : items) {
+ startListening(i.getPmtPid());
+ }
+ if (mListener != null) {
+ mListener.onPatDetected(items);
+ }
+ }
+
+ @Override
+ public void onPmtParsed(int programNumber, List<PmtItem> items) {
+ mProgramNumberToPMTMap.put(programNumber, items);
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onPMTParsed, programNo "
+ + programNumber
+ + " handledStatus is "
+ + mProgramNumberHandledStatus.get(
+ programNumber, false));
+ }
+ int statusIndex = mProgramNumberHandledStatus.indexOfKey(programNumber);
+ if (statusIndex < 0) {
+ mProgramNumberHandledStatus.put(programNumber, false);
+ }
+ if (!mProgramNumberHandledStatus.get(programNumber)) {
+ VctItem vctItem = mProgramNumberToVctItemMap.get(programNumber);
+ if (vctItem != null) {
+ // When PMT is parsed later than VCT.
+ mProgramNumberHandledStatus.put(programNumber, true);
+ handleVctItem(vctItem, items);
+ mHandledVctItemCount++;
+ if (mHandledVctItemCount >= mVctItemCount
+ && mVctSectionParsedCount >= mVctSectionParsed.length
+ && mListener != null) {
+ mListener.onAllVctItemsParsed();
+ }
+ }
+ SdtItem sdtItem = mProgramNumberToSdtItemMap.get(programNumber);
+ if (sdtItem != null) {
+ // When PMT is parsed later than SDT.
+ mProgramNumberHandledStatus.put(programNumber, true);
+ handleSdtItem(sdtItem, items);
+ }
+ }
+ }
+
+ @Override
+ public void onMgtParsed(List<MgtItem> items) {
+ for (MgtItem i : items) {
+ if (mStreamMap.get(i.getTableTypePid()) != null) {
+ continue;
+ }
+ if (i.getTableType() >= MgtItem.TABLE_TYPE_EIT_RANGE_START
+ && i.getTableType() <= MgtItem.TABLE_TYPE_EIT_RANGE_END) {
+ startListening(i.getTableTypePid());
+ mEITPids.add(i.getTableTypePid());
+ if (mListener != null) {
+ mListener.onEitPidDetected(i.getTableTypePid());
+ }
+ } else if (i.getTableType() == MgtItem.TABLE_TYPE_CHANNEL_ETT
+ || (i.getTableType() >= MgtItem.TABLE_TYPE_ETT_RANGE_START
+ && i.getTableType()
+ <= MgtItem.TABLE_TYPE_ETT_RANGE_END)) {
+ startListening(i.getTableTypePid());
+ mETTPids.add(i.getTableTypePid());
+ if (mListener != null) {
+ mListener.onEttPidDetected(i.getTableTypePid());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onVctParsed(
+ List<VctItem> items, int sectionNumber, int lastSectionNumber) {
+ if (mVctSectionParsed == null) {
+ mVctSectionParsed = new boolean[lastSectionNumber + 1];
+ } else if (mVctSectionParsed[sectionNumber]) {
+ // The current section was handled before.
+ if (DEBUG) {
+ Log.d(TAG, "Duplicate VCT section found.");
+ }
+ return;
+ }
+ mVctSectionParsed[sectionNumber] = true;
+ mVctSectionParsedCount++;
+ mVctItemCount += items.size();
+ for (VctItem i : items) {
+ if (DEBUG) Log.d(TAG, "onVCTParsed " + i);
+ if (i.getSourceId() != 0) {
+ mSourceIdToVctItemMap.put(i.getSourceId(), i);
+ i.setDescription(
+ mSourceIdToVctItemDescriptionMap.get(i.getSourceId()));
+ }
+ int programNumber = i.getProgramNumber();
+ mProgramNumberToVctItemMap.put(programNumber, i);
+ List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber);
+ if (pmtList != null) {
+ mProgramNumberHandledStatus.put(programNumber, true);
+ handleVctItem(i, pmtList);
+ mHandledVctItemCount++;
+ if (mHandledVctItemCount >= mVctItemCount
+ && mVctSectionParsedCount >= mVctSectionParsed.length
+ && mListener != null) {
+ mListener.onAllVctItemsParsed();
+ }
+ } else {
+ mProgramNumberHandledStatus.put(programNumber, false);
+ Log.i(
+ TAG,
+ "onVCTParsed, but PMT for programNo "
+ + programNumber
+ + " is not found yet.");
+ }
+ }
+ }
+
+ @Override
+ public void onEitParsed(int sourceId, List<EitItem> items) {
+ if (DEBUG) Log.d(TAG, "onEITParsed " + sourceId);
+ EventSourceEntry entry = new EventSourceEntry(mPid, sourceId);
+ mEitMap.put(entry, items);
+ handleEvents(sourceId);
+ }
+
+ @Override
+ public void onEttParsed(int sourceId, List<EttItem> descriptions) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ String.format(
+ "onETTParsed sourceId: %d, descriptions.size(): %d",
+ sourceId, descriptions.size()));
+ }
+ for (EttItem item : descriptions) {
+ if (item.eventId == 0) {
+ // Channel description
+ mSourceIdToVctItemDescriptionMap.put(sourceId, item.text);
+ VctItem vctItem = mSourceIdToVctItemMap.get(sourceId);
+ if (vctItem != null) {
+ vctItem.setDescription(item.text);
+ List<PmtItem> pmtItems =
+ mProgramNumberToPMTMap.get(vctItem.getProgramNumber());
+ if (pmtItems != null) {
+ handleVctItem(vctItem, pmtItems);
+ }
+ }
+ }
+ }
+
+ // Event Information description
+ EventSourceEntry entry = new EventSourceEntry(mPid, sourceId);
+ mETTMap.put(entry, descriptions);
+ handleEvents(sourceId);
+ }
+
+ @Override
+ public void onSdtParsed(List<SdtItem> sdtItems) {
+ for (SdtItem sdtItem : sdtItems) {
+ if (DEBUG) Log.d(TAG, "onSdtParsed " + sdtItem);
+ int programNumber = sdtItem.getServiceId();
+ mProgramNumberToSdtItemMap.put(programNumber, sdtItem);
+ List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber);
+ if (pmtList != null) {
+ mProgramNumberHandledStatus.put(programNumber, true);
+ handleSdtItem(sdtItem, pmtList);
+ } else {
+ mProgramNumberHandledStatus.put(programNumber, false);
+ Log.i(
+ TAG,
+ "onSdtParsed, but PMT for programNo "
+ + programNumber
+ + " is not found yet.");
+ }
+ }
+ }
+ };
+ }
+
+ private static class EventSourceEntry {
+ public final int pid;
+ public final int sourceId;
+
+ public EventSourceEntry(int pid, int sourceId) {
+ this.pid = pid;
+ this.sourceId = sourceId;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + pid;
+ result = 31 * result + sourceId;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof EventSourceEntry) {
+ EventSourceEntry another = (EventSourceEntry) obj;
+ return pid == another.pid && sourceId == another.sourceId;
+ }
+ return false;
+ }
+ }
+
+ private void handleVctItem(VctItem channel, List<PmtItem> pmtItems) {
+ if (DEBUG) {
+ Log.d(TAG, "handleVctItem " + channel);
+ }
+ if (mListener != null) {
+ mListener.onVctItemParsed(channel, pmtItems);
+ }
+ int sourceId = channel.getSourceId();
+ int statusIndex = mVctItemHandledStatus.indexOfKey(sourceId);
+ if (statusIndex < 0) {
+ mVctItemHandledStatus.put(sourceId, false);
+ return;
+ }
+ if (!mVctItemHandledStatus.valueAt(statusIndex)) {
+ List<EitItem> eitItems = mSourceIdToEitMap.get(sourceId);
+ if (eitItems != null) {
+ // When VCT is parsed later than EIT.
+ mVctItemHandledStatus.put(sourceId, true);
+ handleEitItems(channel, eitItems);
+ }
+ }
+ }
+
+ private void handleEitItems(VctItem channel, List<EitItem> items) {
+ if (mListener != null) {
+ mListener.onEitItemParsed(channel, items);
+ }
+ }
+
+ private void handleSdtItem(SdtItem channel, List<PmtItem> pmtItems) {
+ if (DEBUG) {
+ Log.d(TAG, "handleSdtItem " + channel);
+ }
+ if (mListener != null) {
+ mListener.onSdtItemParsed(channel, pmtItems);
+ }
+ }
+
+ private void handleEvents(int sourceId) {
+ Map<Integer, EitItem> itemSet = new HashMap<>();
+ for (int pid : mEITPids) {
+ List<EitItem> eitItems = mEitMap.get(new EventSourceEntry(pid, sourceId));
+ if (eitItems != null) {
+ for (EitItem item : eitItems) {
+ item.setDescription(null);
+ itemSet.put(item.getEventId(), item);
+ }
+ }
+ }
+ for (int pid : mETTPids) {
+ List<EttItem> ettItems = mETTMap.get(new EventSourceEntry(pid, sourceId));
+ if (ettItems != null) {
+ for (EttItem ettItem : ettItems) {
+ if (ettItem.eventId != 0) {
+ EitItem item = itemSet.get(ettItem.eventId);
+ if (item != null) {
+ item.setDescription(ettItem.text);
+ }
+ }
+ }
+ }
+ }
+ List<EitItem> items = new ArrayList<>(itemSet.values());
+ mSourceIdToEitMap.put(sourceId, items);
+ VctItem channel = mSourceIdToVctItemMap.get(sourceId);
+ if (channel != null && mProgramNumberHandledStatus.get(channel.getProgramNumber())) {
+ mVctItemHandledStatus.put(sourceId, true);
+ handleEitItems(channel, items);
+ } else {
+ mVctItemHandledStatus.put(sourceId, false);
+ if (!mIsDvbSignal) {
+ // Log only when zapping to non-DVB channels, since there is not VCT in DVB signal.
+ Log.i(TAG, "onEITParsed, but VCT for sourceId " + sourceId + " is not found yet.");
+ }
+ }
+ }
+
+ /**
+ * Creates MPEG-2 TS parser.
+ *
+ * @param listener TsOutputListener
+ */
+ public TsParser(TsOutputListener listener, boolean isDvbSignal) {
+ startListening(PAT_PID);
+ startListening(ATSC_SI_BASE_PID);
+ mIsDvbSignal = isDvbSignal;
+ if (isDvbSignal) {
+ startListening(DVB_EIT_PID);
+ startListening(DVB_SDT_PID);
+ }
+ mListener = listener;
+ }
+
+ private void startListening(int pid) {
+ mStreamMap.put(pid, new SectionStream(pid));
+ }
+
+ private boolean feedTSPacket(byte[] tsData, int pos) {
+ if (tsData.length < pos + TS_PACKET_SIZE) {
+ if (DEBUG) Log.d(TAG, "Data should include a single TS packet.");
+ return false;
+ }
+ if (tsData[pos] != TS_PACKET_START_CODE) {
+ if (DEBUG) Log.d(TAG, "Invalid ts packet.");
+ return false;
+ }
+ if ((tsData[pos + 1] & TS_PACKET_TEI_MASK) != 0) {
+ if (DEBUG) Log.d(TAG, "Erroneous ts packet.");
+ return false;
+ }
+
+ // For details for the structure of TS packet, see H.222.0 Table 2-2.
+ int pid = ((tsData[pos + 1] & 0x1f) << 8) | (tsData[pos + 2] & 0xff);
+ boolean hasAdaptation = (tsData[pos + 3] & 0x20) != 0;
+ boolean hasPayload = (tsData[pos + 3] & 0x10) != 0;
+ boolean payloadStartIndicator = (tsData[pos + 1] & 0x40) != 0;
+ int continuityCounter = tsData[pos + 3] & 0x0f;
+ Stream stream = mStreamMap.get(pid);
+ int payloadPos = pos;
+ payloadPos += hasAdaptation ? 5 + (tsData[pos + 4] & 0xff) : 4;
+ if (!hasPayload || stream == null) {
+ // We are not interested in this packet.
+ return false;
+ }
+ if (payloadPos >= pos + TS_PACKET_SIZE) {
+ if (DEBUG) Log.d(TAG, "Payload should be included in a single TS packet.");
+ return false;
+ }
+ stream.feedData(
+ Arrays.copyOfRange(tsData, payloadPos, pos + TS_PACKET_SIZE),
+ continuityCounter,
+ payloadStartIndicator);
+ return true;
+ }
+
+ /**
+ * Feeds MPEG-2 TS data to parse.
+ *
+ * @param tsData buffer for ATSC TS stream
+ * @param pos the offset where buffer starts
+ * @param length The length of available data
+ */
+ public void feedTSData(byte[] tsData, int pos, int length) {
+ for (; pos <= length - TS_PACKET_SIZE; pos += TS_PACKET_SIZE) {
+ feedTSPacket(tsData, pos);
+ }
+ }
+
+ /**
+ * Retrieves the channel information regardless of being well-formed.
+ *
+ * @return {@link List} of {@link TunerChannel}
+ */
+ public List<TunerChannel> getMalFormedChannels() {
+ List<TunerChannel> incompleteChannels = new ArrayList<>();
+ for (int i = 0; i < mProgramNumberHandledStatus.size(); i++) {
+ if (!mProgramNumberHandledStatus.valueAt(i)) {
+ int programNumber = mProgramNumberHandledStatus.keyAt(i);
+ List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber);
+ if (pmtList != null) {
+ TunerChannel tunerChannel = new TunerChannel(programNumber, pmtList);
+ incompleteChannels.add(tunerChannel);
+ }
+ }
+ }
+ return incompleteChannels;
+ }
+
+ /** Reset the versions so that data with old version number can be handled. */
+ public void resetDataVersions() {
+ for (int eitPid : mEITPids) {
+ Stream stream = mStreamMap.get(eitPid);
+ if (stream != null) {
+ stream.resetDataVersions();
+ }
+ }
+ }
+}