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
17package com.android.usbtuner.ts;
18
19import android.util.Log;
20import android.util.SparseArray;
21import android.util.SparseBooleanArray;
22
23import com.android.usbtuner.data.PsiData.PatItem;
24import com.android.usbtuner.data.PsiData.PmtItem;
25import com.android.usbtuner.data.PsipData.EitItem;
26import com.android.usbtuner.data.PsipData.EttItem;
27import com.android.usbtuner.data.PsipData.MgtItem;
28import com.android.usbtuner.data.PsipData.VctItem;
29import com.android.usbtuner.data.TunerChannel;
30import com.android.usbtuner.ts.SectionParser.OutputListener;
31import com.android.usbtuner.util.ByteArrayBuffer;
32
33import java.util.ArrayList;
34import java.util.Arrays;
35import java.util.HashMap;
36import java.util.List;
37import java.util.Map;
38import java.util.TreeSet;
39
40/**
41 * Parses MPEG-2 TS packets.
42 */
43public class TsParser {
44    private static final String TAG = "TsParser";
45    private static boolean DEBUG = false;
46
47    public static final int ATSC_SI_BASE_PID = 0x1ffb;
48    public static final int PAT_PID = 0x0000;
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<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 TsOutputListener mListener;
74
75    private int mPartialTSPacketSize;
76    private byte[] mPartialTSPacketBuf = new byte[TS_PACKET_SIZE];
77
78    public interface TsOutputListener {
79        void onPatDetected(List<PatItem> items);
80        void onEitPidDetected(int pid);
81        void onVctItemParsed(VctItem channel, List<PmtItem> pmtItems);
82        void onEitItemParsed(VctItem channel, List<EitItem> items);
83        void onEttPidDetected(int pid);
84    }
85
86    private abstract class Stream {
87        private static final int INVALID_CONTINUITY_COUNTER = -1;
88        private static final int NUM_CONTINUITY_COUNTER = 16;
89
90        protected int mContinuityCounter = INVALID_CONTINUITY_COUNTER;
91        protected final ByteArrayBuffer mPacket = new ByteArrayBuffer(TS_PACKET_SIZE);
92
93        public void feedData(byte[] data, int continuityCounter, boolean startIndicator) {
94            if ((mContinuityCounter + 1) % NUM_CONTINUITY_COUNTER != continuityCounter) {
95                mPacket.setLength(0);
96            }
97            mContinuityCounter = continuityCounter;
98            handleData(data, startIndicator);
99        }
100
101        protected abstract void handleData(byte[] data, boolean startIndicator);
102    }
103
104    private class SectionStream extends Stream {
105        private final SectionParser mSectionParser;
106        private final int mPid;
107
108        public SectionStream(int pid) {
109            mPid = pid;
110            mSectionParser = new SectionParser(mSectionListener);
111        }
112
113        @Override
114        protected void handleData(byte[] data, boolean startIndicator) {
115            int startPos = 0;
116            if (mPacket.length() == 0) {
117                if (startIndicator) {
118                    startPos = (data[0] & 0xff) + 1;
119                } else {
120                    // Don't know where the section starts yet. Wait until start indicator is on.
121                    return;
122                }
123            } else {
124                if (startIndicator) {
125                    startPos = 1;
126                }
127            }
128
129            // When a broken packet is encountered, parsing will stop and return right away.
130            if (startPos >= data.length) {
131                mPacket.setLength(0);
132                return;
133            }
134            mPacket.append(data, startPos, data.length - startPos);
135            mSectionParser.parseSections(mPacket);
136        }
137
138        private OutputListener mSectionListener = new OutputListener() {
139            @Override
140            public void onPatParsed(List<PatItem> items) {
141                for (PatItem i : items) {
142                    startListening(i.getPmtPid());
143                }
144                if (mListener != null) {
145                    mListener.onPatDetected(items);
146                }
147            }
148
149            @Override
150            public void onPmtParsed(int programNumber, List<PmtItem> items) {
151                mProgramNumberToPMTMap.put(programNumber, items);
152                if (DEBUG) {
153                    Log.d(TAG, "onPMTParsed, programNo " + programNumber + " handledStatus is "
154                            + mProgramNumberHandledStatus.get(programNumber, false));
155                }
156                int statusIndex = mProgramNumberHandledStatus.indexOfKey(programNumber);
157                if (statusIndex < 0) {
158                    mProgramNumberHandledStatus.put(programNumber, false);
159                    return;
160                }
161                if (!mProgramNumberHandledStatus.valueAt(statusIndex)) {
162                    VctItem vctItem = mProgramNumberToVctItemMap.get(programNumber);
163                    if (vctItem != null) {
164                        // When PMT is parsed later than VCT.
165                        mProgramNumberHandledStatus.put(programNumber, true);
166                        handleVctItem(vctItem, items);
167                    }
168                }
169            }
170
171            @Override
172            public void onMgtParsed(List<MgtItem> items) {
173                for (MgtItem i : items) {
174                    if (mStreamMap.get(i.getTableTypePid()) != null) {
175                        continue;
176                    }
177                    if (i.getTableType() >= MgtItem.TABLE_TYPE_EIT_RANGE_START
178                            && i.getTableType() <= MgtItem.TABLE_TYPE_EIT_RANGE_END) {
179                        startListening(i.getTableTypePid());
180                        mEITPids.add(i.getTableTypePid());
181                        if (mListener != null) {
182                            mListener.onEitPidDetected(i.getTableTypePid());
183                        }
184                    } else if (i.getTableType() == MgtItem.TABLE_TYPE_CHANNEL_ETT ||
185                            (i.getTableType() >= MgtItem.TABLE_TYPE_ETT_RANGE_START
186                                    && i.getTableType() <= MgtItem.TABLE_TYPE_ETT_RANGE_END)) {
187                        startListening(i.getTableTypePid());
188                        mETTPids.add(i.getTableTypePid());
189                        if (mListener != null) {
190                            mListener.onEttPidDetected(i.getTableTypePid());
191                        }
192                    }
193                }
194            }
195
196            @Override
197            public void onVctParsed(List<VctItem> items) {
198                for (VctItem i : items) {
199                    if (DEBUG) Log.d(TAG, "onVCTParsed " + i);
200                    if (i.getSourceId() != 0) {
201                        mSourceIdToVctItemMap.put(i.getSourceId(), i);
202                        i.setDescription(mSourceIdToVctItemDescriptionMap.get(i.getSourceId()));
203                    }
204                    int programNumber = i.getProgramNumber();
205                    mProgramNumberToVctItemMap.put(programNumber, i);
206                    List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber);
207                    if (pmtList != null) {
208                        mProgramNumberHandledStatus.put(programNumber, true);
209                        handleVctItem(i, pmtList);
210                    } else {
211                        mProgramNumberHandledStatus.put(programNumber, false);
212                        Log.i(TAG, "onVCTParsed, but PMT for programNo " + programNumber
213                                + " is not found yet.");
214                    }
215                }
216            }
217
218            @Override
219            public void onEitParsed(int sourceId, List<EitItem> items) {
220                if (DEBUG) Log.d(TAG, "onEITParsed " + sourceId);
221                EventSourceEntry entry = new EventSourceEntry(mPid, sourceId);
222                mEitMap.put(entry, items);
223                handleEvents(sourceId);
224            }
225
226            @Override
227            public void onEttParsed(int sourceId, List<EttItem> descriptions) {
228                if (DEBUG) {
229                    Log.d(TAG, String.format("onETTParsed sourceId: %d, descriptions.size(): %d",
230                            sourceId, descriptions.size()));
231                }
232                for (EttItem item : descriptions) {
233                    if (item.eventId == 0) {
234                        // Channel description
235                        mSourceIdToVctItemDescriptionMap.put(sourceId, item.text);
236                        VctItem vctItem = mSourceIdToVctItemMap.get(sourceId);
237                        if (vctItem != null) {
238                            vctItem.setDescription(item.text);
239                            List<PmtItem> pmtItems =
240                                    mProgramNumberToPMTMap.get(vctItem.getProgramNumber());
241                            if (pmtItems != null) {
242                                handleVctItem(vctItem, pmtItems);
243                            }
244                        }
245                    }
246                }
247
248                // Event Information description
249                EventSourceEntry entry = new EventSourceEntry(mPid, sourceId);
250                mETTMap.put(entry, descriptions);
251                handleEvents(sourceId);
252            }
253        };
254    }
255
256    private static class EventSourceEntry {
257        public final int pid;
258        public final int sourceId;
259
260        public EventSourceEntry(int pid, int sourceId) {
261            this.pid = pid;
262            this.sourceId = sourceId;
263        }
264
265        @Override
266        public int hashCode() {
267            int result = 17;
268            result = 31 * result + pid;
269            result = 31 * result + sourceId;
270            return result;
271        }
272
273        @Override
274        public boolean equals(Object obj) {
275            if (obj instanceof EventSourceEntry) {
276                EventSourceEntry another = (EventSourceEntry) obj;
277                return pid == another.pid && sourceId == another.sourceId;
278            }
279            return false;
280        }
281    }
282
283    private void handleVctItem(VctItem channel, List<PmtItem> pmtItems) {
284        if (mListener != null) {
285            mListener.onVctItemParsed(channel, pmtItems);
286        }
287        int sourceId = channel.getSourceId();
288        int statusIndex = mVctItemHandledStatus.indexOfKey(sourceId);
289        if (statusIndex < 0) {
290            mVctItemHandledStatus.put(sourceId, false);
291            return;
292        }
293        if (!mVctItemHandledStatus.valueAt(statusIndex)) {
294            List<EitItem> eitItems = mSourceIdToEitMap.get(sourceId);
295            if (eitItems != null) {
296                // When VCT is parsed later than EIT.
297                mVctItemHandledStatus.put(sourceId, true);
298                handleEitItems(channel, eitItems);
299            }
300        }
301    }
302
303    private void handleEitItems(VctItem channel, List<EitItem> items) {
304        if (mListener != null) {
305            mListener.onEitItemParsed(channel, items);
306        }
307    }
308
309    private void handleEvents(int sourceId) {
310        Map<Integer, EitItem> itemSet = new HashMap<>();
311        for (int pid : mEITPids) {
312            List<EitItem> eitItems = mEitMap.get(new EventSourceEntry(pid, sourceId));
313            if (eitItems != null) {
314                for (EitItem item : eitItems) {
315                    item.setDescription(null);
316                    itemSet.put(item.getEventId(), item);
317                }
318            }
319        }
320        for (int pid : mETTPids) {
321            List<EttItem> ettItems = mETTMap.get(new EventSourceEntry(pid, sourceId));
322            if (ettItems != null) {
323                for (EttItem ettItem : ettItems) {
324                    if (ettItem.eventId != 0) {
325                        EitItem item = itemSet.get(ettItem.eventId);
326                        if (item != null) {
327                            item.setDescription(ettItem.text);
328                        }
329                    }
330                }
331            }
332        }
333        List<EitItem> items = new ArrayList<>(itemSet.values());
334        mSourceIdToEitMap.put(sourceId, items);
335        VctItem channel = mSourceIdToVctItemMap.get(sourceId);
336        if (channel != null && mProgramNumberHandledStatus.get(channel.getProgramNumber())) {
337            mVctItemHandledStatus.put(sourceId, true);
338            handleEitItems(channel, items);
339        } else {
340            mVctItemHandledStatus.put(sourceId, false);
341            Log.i(TAG, "onEITParsed, but VCT for sourceId " + sourceId + " is not found yet.");
342        }
343    }
344
345    public TsParser(TsOutputListener listener) {
346        startListening(ATSC_SI_BASE_PID);
347        startListening(PAT_PID);
348        mListener = listener;
349    }
350
351    private void startListening(int pid) {
352        mStreamMap.put(pid, new SectionStream(pid));
353    }
354
355    private boolean feedTSPacket(byte[] tsData, int pos) {
356        if (tsData.length < pos + TS_PACKET_SIZE) {
357            if (DEBUG) Log.d(TAG, "Data should include a single TS packet.");
358            return false;
359        }
360        if (tsData[pos] != TS_PACKET_START_CODE) {
361            if (DEBUG) Log.d(TAG, "Invalid ts packet.");
362            return false;
363        }
364        if ((tsData[pos + 1] & TS_PACKET_TEI_MASK) != 0) {
365            if (DEBUG) Log.d(TAG, "Erroneous ts packet.");
366            return false;
367        }
368
369        // For details for the structure of TS packet, see H.222.0 Table 2-2.
370        int pid = ((tsData[pos + 1] & 0x1f) << 8) | (tsData[pos + 2] & 0xff);
371        boolean hasAdaptation = (tsData[pos + 3] & 0x20) != 0;
372        boolean hasPayload = (tsData[pos + 3] & 0x10) != 0;
373        boolean payloadStartIndicator = (tsData[pos + 1] & 0x40) != 0;
374        int continuityCounter = tsData[pos + 3] & 0x0f;
375        Stream stream = mStreamMap.get(pid);
376        int payloadPos = pos;
377        payloadPos += hasAdaptation ? 5 + (tsData[pos + 4] & 0xff) : 4;
378        if (!hasPayload || stream == null) {
379            // We are not interested in this packet.
380            return false;
381        }
382        if (payloadPos > pos + TS_PACKET_SIZE) {
383            if (DEBUG) Log.d(TAG, "Payload should be included in a single TS packet.");
384            return false;
385        }
386        stream.feedData(Arrays.copyOfRange(tsData, payloadPos, pos + TS_PACKET_SIZE),
387                continuityCounter, payloadStartIndicator);
388        return true;
389    }
390
391    public void feedTSData(byte[] tsData, int pos, int length) {
392        int origPos = pos;
393        if (mPartialTSPacketSize != 0
394                && (mPartialTSPacketSize + length) > TS_PACKET_SIZE) {
395            System.arraycopy(tsData, pos, mPartialTSPacketBuf, mPartialTSPacketSize,
396                    TS_PACKET_SIZE - mPartialTSPacketSize);
397            feedTSPacket(mPartialTSPacketBuf, 0);
398            pos += TS_PACKET_SIZE - mPartialTSPacketSize;
399            mPartialTSPacketSize = 0;
400        }
401        for (; pos <= length - TS_PACKET_SIZE; pos += TS_PACKET_SIZE) {
402            feedTSPacket(tsData, pos);
403        }
404        int remaining = origPos + length - pos;
405        if (remaining > 0) {
406            System.arraycopy(tsData, pos, mPartialTSPacketBuf, mPartialTSPacketSize, remaining);
407        }
408    }
409
410    public List<TunerChannel> getIncompleteChannels() {
411        List<TunerChannel> incompleteChannels = new ArrayList<>();
412        for (int i = 0; i < mProgramNumberHandledStatus.size(); i++) {
413            if (!mProgramNumberHandledStatus.valueAt(i)) {
414                int programNumber = mProgramNumberHandledStatus.keyAt(i);
415                List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber);
416                if (pmtList != null) {
417                    TunerChannel tunerChannel = new TunerChannel(programNumber, pmtList);
418                    incompleteChannels.add(tunerChannel);
419                }
420            }
421        }
422        return incompleteChannels;
423    }
424}
425