blob: be46983b2fdc71dd6556af5293705a4854b13dd7 [file] [log] [blame]
/*
* 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.SectionParser;
import com.android.tv.tuner.data.SectionParser.OutputListener;
import com.android.tv.tuner.data.TunerChannel;
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();
}
}
}
}