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