• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.exoplayer;
18 
19 import android.net.Uri;
20 import android.os.Handler;
21 
22 import com.google.android.exoplayer.MediaFormat;
23 import com.google.android.exoplayer.MediaFormatHolder;
24 import com.google.android.exoplayer.SampleHolder;
25 import com.google.android.exoplayer.SampleSource;
26 import com.google.android.exoplayer.upstream.DataSource;
27 import com.google.android.exoplayer.util.MimeTypes;
28 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
29 import com.android.tv.tuner.exoplayer.buffer.SamplePool;
30 import com.android.tv.tuner.tvinput.PlaybackBufferListener;
31 
32 import java.io.IOException;
33 import java.nio.ByteBuffer;
34 import java.util.ArrayList;
35 import java.util.LinkedList;
36 import java.util.List;
37 
38 /**
39  * Extracts samples from {@link DataSource} for MPEG-TS streams.
40  */
41 public final class MpegTsSampleExtractor implements SampleExtractor {
42     public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708";
43 
44     private static final int CC_BUFFER_SIZE_IN_BYTES = 9600 / 8;
45 
46     private final SampleExtractor mSampleExtractor;
47     private final List<MediaFormat> mTrackFormats = new ArrayList<>();
48     private final List<Boolean> mReachedEos = new ArrayList<>();
49     private int mVideoTrackIndex;
50     private final SamplePool mCcSamplePool = new SamplePool();
51     private final List<SampleHolder> mPendingCcSamples = new LinkedList<>();
52 
53     private int mCea708TextTrackIndex;
54     private boolean mCea708TextTrackSelected;
55 
56     private CcParser mCcParser;
57 
init()58     private void init() {
59         mVideoTrackIndex = -1;
60         mCea708TextTrackIndex = -1;
61         mCea708TextTrackSelected = false;
62     }
63 
64     /**
65      * Creates MpegTsSampleExtractor for {@link DataSource}.
66      *
67      * @param source the {@link DataSource} to extract from
68      * @param bufferManager the manager for reading & writing samples backed by physical storage
69      * @param bufferListener the {@link PlaybackBufferListener}
70      *                      to notify buffer storage status change
71      */
MpegTsSampleExtractor(DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener)72     public MpegTsSampleExtractor(DataSource source, BufferManager bufferManager,
73             PlaybackBufferListener bufferListener) {
74         mSampleExtractor = new ExoPlayerSampleExtractor(Uri.EMPTY, source, bufferManager,
75                 bufferListener, false);
76         init();
77     }
78 
79     /**
80      * Creates MpegTsSampleExtractor for a recorded program.
81      *
82      * @param bufferManager the samples provider which is stored in physical storage
83      * @param bufferListener the {@link PlaybackBufferListener}
84      *                      to notify buffer storage status change
85      */
MpegTsSampleExtractor(BufferManager bufferManager, PlaybackBufferListener bufferListener)86     public MpegTsSampleExtractor(BufferManager bufferManager,
87             PlaybackBufferListener bufferListener) {
88         mSampleExtractor = new FileSampleExtractor(bufferManager, bufferListener);
89         init();
90     }
91 
92     @Override
maybeThrowError()93     public void maybeThrowError() throws IOException {
94         if (mSampleExtractor != null) {
95             mSampleExtractor.maybeThrowError();
96         }
97     }
98 
99     @Override
prepare()100     public boolean prepare() throws IOException {
101         if(!mSampleExtractor.prepare()) {
102             return false;
103         }
104         List<MediaFormat> formats = mSampleExtractor.getTrackFormats();
105         int trackCount = formats.size();
106         mTrackFormats.clear();
107         mReachedEos.clear();
108 
109         for (int i = 0; i < trackCount; ++i) {
110             mTrackFormats.add(formats.get(i));
111             mReachedEos.add(false);
112             String mime = formats.get(i).mimeType;
113             if (MimeTypes.isVideo(mime) && mVideoTrackIndex == -1) {
114                 mVideoTrackIndex = i;
115                 if (android.media.MediaFormat.MIMETYPE_VIDEO_MPEG2.equals(mime)) {
116                     mCcParser = new Mpeg2CcParser();
117                 } else if (android.media.MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) {
118                     mCcParser = new H264CcParser();
119                 }
120             }
121         }
122 
123         if (mVideoTrackIndex != -1) {
124             mCea708TextTrackIndex = trackCount;
125         }
126         if (mCea708TextTrackIndex >= 0) {
127             mTrackFormats.add(MediaFormat.createTextFormat(null, MIMETYPE_TEXT_CEA_708, 0,
128                     mTrackFormats.get(0).durationUs, ""));
129         }
130         return true;
131     }
132 
133     @Override
getTrackFormats()134     public List<MediaFormat> getTrackFormats() {
135         return mTrackFormats;
136     }
137 
138     @Override
selectTrack(int index)139     public void selectTrack(int index) {
140         if (index == mCea708TextTrackIndex) {
141             mCea708TextTrackSelected = true;
142             return;
143         }
144         mSampleExtractor.selectTrack(index);
145     }
146 
147     @Override
deselectTrack(int index)148     public void deselectTrack(int index) {
149         if (index == mCea708TextTrackIndex) {
150             mCea708TextTrackSelected = false;
151             return;
152         }
153         mSampleExtractor.deselectTrack(index);
154     }
155 
156     @Override
getBufferedPositionUs()157     public long getBufferedPositionUs() {
158         return mSampleExtractor.getBufferedPositionUs();
159     }
160 
161     @Override
seekTo(long positionUs)162     public void seekTo(long positionUs) {
163         mSampleExtractor.seekTo(positionUs);
164         for (SampleHolder holder : mPendingCcSamples) {
165             mCcSamplePool.releaseSample(holder);
166         }
167         mPendingCcSamples.clear();
168     }
169 
170     @Override
getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder)171     public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) {
172         if (track != mCea708TextTrackIndex) {
173             mSampleExtractor.getTrackMediaFormat(track, outMediaFormatHolder);
174         }
175     }
176 
177     @Override
readSample(int track, SampleHolder sampleHolder)178     public int readSample(int track, SampleHolder sampleHolder) {
179         if (track == mCea708TextTrackIndex) {
180             if (mCea708TextTrackSelected && !mPendingCcSamples.isEmpty()) {
181                 SampleHolder holder = mPendingCcSamples.remove(0);
182                 holder.data.flip();
183                 sampleHolder.timeUs = holder.timeUs;
184                 sampleHolder.data.put(holder.data);
185                 mCcSamplePool.releaseSample(holder);
186                 return SampleSource.SAMPLE_READ;
187             } else {
188                 return mVideoTrackIndex < 0 || mReachedEos.get(mVideoTrackIndex)
189                         ? SampleSource.END_OF_STREAM : SampleSource.NOTHING_READ;
190             }
191         }
192 
193         int result = mSampleExtractor.readSample(track, sampleHolder);
194         switch (result) {
195             case SampleSource.END_OF_STREAM: {
196                 mReachedEos.set(track, true);
197                 break;
198             }
199             case SampleSource.SAMPLE_READ: {
200                 if (mCea708TextTrackSelected && track == mVideoTrackIndex
201                         && sampleHolder.data != null) {
202                     mCcParser.mayParseClosedCaption(sampleHolder.data, sampleHolder.timeUs);
203                 }
204                 break;
205             }
206         }
207         return result;
208     }
209 
210     @Override
release()211     public void release() {
212         mSampleExtractor.release();
213         mVideoTrackIndex = -1;
214         mCea708TextTrackIndex = -1;
215         mCea708TextTrackSelected = false;
216     }
217 
218     @Override
continueBuffering(long positionUs)219     public boolean continueBuffering(long positionUs) {
220         return mSampleExtractor.continueBuffering(positionUs);
221     }
222 
223     @Override
setOnCompletionListener(OnCompletionListener listener, Handler handler)224     public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { }
225 
226     private abstract class CcParser {
227         // Interim buffer for reduce direct access to ByteBuffer which is expensive. Using
228         // relatively small buffer size in order to minimize memory footprint increase.
229         protected final byte[] mBuffer = new byte[1024];
230 
mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs)231         abstract void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs);
232 
parseClosedCaption(ByteBuffer buffer, int offset, long presentationTimeUs)233         protected int parseClosedCaption(ByteBuffer buffer, int offset, long presentationTimeUs) {
234             // For the details of user_data_type_structure, see ATSC A/53 Part 4 - Table 6.9.
235             int pos = offset;
236             if (pos + 2 >= buffer.position()) {
237                 return offset;
238             }
239             boolean processCcDataFlag = (buffer.get(pos) & 64) != 0;
240             int ccCount = buffer.get(pos) & 0x1f;
241             pos += 2;
242             if (!processCcDataFlag || pos + 3 * ccCount >= buffer.position() || ccCount == 0) {
243                 return offset;
244             }
245             SampleHolder holder = mCcSamplePool.acquireSample(CC_BUFFER_SIZE_IN_BYTES);
246             for (int i = 0; i < 3 * ccCount; i++) {
247                 holder.data.put(buffer.get(pos++));
248             }
249             holder.timeUs = presentationTimeUs;
250             mPendingCcSamples.add(holder);
251             return pos;
252         }
253     }
254 
255     private class Mpeg2CcParser extends CcParser {
256         private static final int PATTERN_LENGTH = 9;
257 
258         @Override
mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs)259         public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) {
260             int totalSize = buffer.position();
261             // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with
262             // overlapping to handle the case that the pattern exists in the boundary.
263             for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) {
264                 buffer.position(i);
265                 int size = Math.min(totalSize - i, mBuffer.length);
266                 buffer.get(mBuffer, 0, size);
267                 int j = 0;
268                 while (j < size - PATTERN_LENGTH) {
269                     // Find the start prefix code of private user data.
270                     if (mBuffer[j] == 0
271                             && mBuffer[j + 1] == 0
272                             && mBuffer[j + 2] == 1
273                             && (mBuffer[j + 3] & 0xff) == 0xb2) {
274                         // ATSC closed caption data embedded in MPEG2VIDEO stream has 'GA94' user
275                         // identifier and user data type code 3.
276                         if (mBuffer[j + 4] == 'G'
277                                 && mBuffer[j + 5] == 'A'
278                                 && mBuffer[j + 6] == '9'
279                                 && mBuffer[j + 7] == '4'
280                                 && mBuffer[j + 8] == 3) {
281                             j = parseClosedCaption(buffer, i + j + PATTERN_LENGTH,
282                                     presentationTimeUs) - i;
283                         } else {
284                             j += PATTERN_LENGTH;
285                         }
286                     } else {
287                         ++j;
288                     }
289                 }
290             }
291             buffer.position(totalSize);
292         }
293     }
294 
295     private class H264CcParser extends CcParser {
296         private static final int PATTERN_LENGTH = 14;
297 
298         @Override
mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs)299         public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) {
300             int totalSize = buffer.position();
301             // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with
302             // overlapping to handle the case that the pattern exists in the boundary.
303             for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) {
304                 buffer.position(i);
305                 int size = Math.min(totalSize - i, mBuffer.length);
306                 buffer.get(mBuffer, 0, size);
307                 int j = 0;
308                 while (j < size - PATTERN_LENGTH) {
309                     // Find the start prefix code of a NAL Unit.
310                     if (mBuffer[j] == 0
311                             && mBuffer[j + 1] == 0
312                             && mBuffer[j + 2] == 1) {
313                         int nalType = mBuffer[j + 3] & 0x1f;
314                         int payloadType = mBuffer[j + 4] & 0xff;
315 
316                         // ATSC closed caption data embedded in H264 private user data has NAL type
317                         // 6, payload type 4, and 'GA94' user identifier for ATSC.
318                         if (nalType == 6 && payloadType == 4 && mBuffer[j + 9] == 'G'
319                                 && mBuffer[j + 10] == 'A'
320                                 && mBuffer[j + 11] == '9'
321                                 && mBuffer[j + 12] == '4') {
322                             j = parseClosedCaption(buffer, i + j + PATTERN_LENGTH,
323                                     presentationTimeUs) - i;
324                         } else {
325                             j += 7;
326                         }
327                     } else {
328                         ++j;
329                     }
330                 }
331             }
332             buffer.position(totalSize);
333         }
334     }
335 }
336