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