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