diff options
Diffstat (limited to 'tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java')
-rw-r--r-- | tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java new file mode 100644 index 00000000..593b576e --- /dev/null +++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.exoplayer; + +import android.net.Uri; +import android.os.Handler; +import com.android.tv.tuner.exoplayer.buffer.BufferManager; +import com.android.tv.tuner.exoplayer.buffer.SamplePool; +import com.android.tv.tuner.tvinput.PlaybackBufferListener; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.util.MimeTypes; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** Extracts samples from {@link DataSource} for MPEG-TS streams. */ +public final class MpegTsSampleExtractor implements SampleExtractor { + public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708"; + + private static final int CC_BUFFER_SIZE_IN_BYTES = 9600 / 8; + + private final SampleExtractor mSampleExtractor; + private final List<MediaFormat> mTrackFormats = new ArrayList<>(); + private final List<Boolean> mReachedEos = new ArrayList<>(); + private int mVideoTrackIndex; + private final SamplePool mCcSamplePool = new SamplePool(); + private final List<SampleHolder> mPendingCcSamples = new LinkedList<>(); + + private int mCea708TextTrackIndex; + private boolean mCea708TextTrackSelected; + + private CcParser mCcParser; + + private void init() { + mVideoTrackIndex = -1; + mCea708TextTrackIndex = -1; + mCea708TextTrackSelected = false; + } + + /** + * Creates MpegTsSampleExtractor for {@link DataSource}. + * + * @param source the {@link DataSource} to extract from + * @param bufferManager the manager for reading & writing samples backed by physical storage + * @param bufferListener the {@link PlaybackBufferListener} to notify buffer storage status + * change + */ + public MpegTsSampleExtractor( + DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener) { + mSampleExtractor = + new ExoPlayerSampleExtractor( + Uri.EMPTY, source, bufferManager, bufferListener, false); + init(); + } + + /** + * Creates MpegTsSampleExtractor for a recorded program. + * + * @param bufferManager the samples provider which is stored in physical storage + * @param bufferListener the {@link PlaybackBufferListener} to notify buffer storage status + * change + */ + public MpegTsSampleExtractor( + BufferManager bufferManager, PlaybackBufferListener bufferListener) { + mSampleExtractor = new FileSampleExtractor(bufferManager, bufferListener); + init(); + } + + @Override + public void maybeThrowError() throws IOException { + if (mSampleExtractor != null) { + mSampleExtractor.maybeThrowError(); + } + } + + @Override + public boolean prepare() throws IOException { + if (!mSampleExtractor.prepare()) { + return false; + } + List<MediaFormat> formats = mSampleExtractor.getTrackFormats(); + int trackCount = formats.size(); + mTrackFormats.clear(); + mReachedEos.clear(); + + for (int i = 0; i < trackCount; ++i) { + mTrackFormats.add(formats.get(i)); + mReachedEos.add(false); + String mime = formats.get(i).mimeType; + if (MimeTypes.isVideo(mime) && mVideoTrackIndex == -1) { + mVideoTrackIndex = i; + if (android.media.MediaFormat.MIMETYPE_VIDEO_MPEG2.equals(mime)) { + mCcParser = new Mpeg2CcParser(); + } else if (android.media.MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) { + mCcParser = new H264CcParser(); + } + } + } + + if (mVideoTrackIndex != -1) { + mCea708TextTrackIndex = trackCount; + } + if (mCea708TextTrackIndex >= 0) { + mTrackFormats.add( + MediaFormat.createTextFormat( + null, MIMETYPE_TEXT_CEA_708, 0, mTrackFormats.get(0).durationUs, "")); + } + return true; + } + + @Override + public List<MediaFormat> getTrackFormats() { + return mTrackFormats; + } + + @Override + public void selectTrack(int index) { + if (index == mCea708TextTrackIndex) { + mCea708TextTrackSelected = true; + return; + } + mSampleExtractor.selectTrack(index); + } + + @Override + public void deselectTrack(int index) { + if (index == mCea708TextTrackIndex) { + mCea708TextTrackSelected = false; + return; + } + mSampleExtractor.deselectTrack(index); + } + + @Override + public long getBufferedPositionUs() { + return mSampleExtractor.getBufferedPositionUs(); + } + + @Override + public void seekTo(long positionUs) { + mSampleExtractor.seekTo(positionUs); + for (SampleHolder holder : mPendingCcSamples) { + mCcSamplePool.releaseSample(holder); + } + mPendingCcSamples.clear(); + } + + @Override + public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { + if (track != mCea708TextTrackIndex) { + mSampleExtractor.getTrackMediaFormat(track, outMediaFormatHolder); + } + } + + @Override + public int readSample(int track, SampleHolder sampleHolder) { + if (track == mCea708TextTrackIndex) { + if (mCea708TextTrackSelected && !mPendingCcSamples.isEmpty()) { + SampleHolder holder = mPendingCcSamples.remove(0); + holder.data.flip(); + sampleHolder.timeUs = holder.timeUs; + sampleHolder.data.put(holder.data); + mCcSamplePool.releaseSample(holder); + return SampleSource.SAMPLE_READ; + } else { + return mVideoTrackIndex < 0 || mReachedEos.get(mVideoTrackIndex) + ? SampleSource.END_OF_STREAM + : SampleSource.NOTHING_READ; + } + } + + int result = mSampleExtractor.readSample(track, sampleHolder); + switch (result) { + case SampleSource.END_OF_STREAM: + { + mReachedEos.set(track, true); + break; + } + case SampleSource.SAMPLE_READ: + { + if (mCea708TextTrackSelected + && track == mVideoTrackIndex + && sampleHolder.data != null) { + mCcParser.mayParseClosedCaption(sampleHolder.data, sampleHolder.timeUs); + } + break; + } + } + return result; + } + + @Override + public void release() { + mSampleExtractor.release(); + mVideoTrackIndex = -1; + mCea708TextTrackIndex = -1; + mCea708TextTrackSelected = false; + } + + @Override + public boolean continueBuffering(long positionUs) { + return mSampleExtractor.continueBuffering(positionUs); + } + + @Override + public void setOnCompletionListener(OnCompletionListener listener, Handler handler) {} + + private abstract class CcParser { + // Interim buffer for reduce direct access to ByteBuffer which is expensive. Using + // relatively small buffer size in order to minimize memory footprint increase. + protected final byte[] mBuffer = new byte[1024]; + + abstract void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs); + + protected int parseClosedCaption(ByteBuffer buffer, int offset, long presentationTimeUs) { + // For the details of user_data_type_structure, see ATSC A/53 Part 4 - Table 6.9. + int pos = offset; + if (pos + 2 >= buffer.position()) { + return offset; + } + boolean processCcDataFlag = (buffer.get(pos) & 64) != 0; + int ccCount = buffer.get(pos) & 0x1f; + pos += 2; + if (!processCcDataFlag || pos + 3 * ccCount >= buffer.position() || ccCount == 0) { + return offset; + } + SampleHolder holder = mCcSamplePool.acquireSample(CC_BUFFER_SIZE_IN_BYTES); + for (int i = 0; i < 3 * ccCount; i++) { + holder.data.put(buffer.get(pos++)); + } + holder.timeUs = presentationTimeUs; + mPendingCcSamples.add(holder); + return pos; + } + } + + private class Mpeg2CcParser extends CcParser { + private static final int PATTERN_LENGTH = 9; + + @Override + public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) { + int totalSize = buffer.position(); + // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with + // overlapping to handle the case that the pattern exists in the boundary. + for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) { + buffer.position(i); + int size = Math.min(totalSize - i, mBuffer.length); + buffer.get(mBuffer, 0, size); + int j = 0; + while (j < size - PATTERN_LENGTH) { + // Find the start prefix code of private user data. + if (mBuffer[j] == 0 + && mBuffer[j + 1] == 0 + && mBuffer[j + 2] == 1 + && (mBuffer[j + 3] & 0xff) == 0xb2) { + // ATSC closed caption data embedded in MPEG2VIDEO stream has 'GA94' user + // identifier and user data type code 3. + if (mBuffer[j + 4] == 'G' + && mBuffer[j + 5] == 'A' + && mBuffer[j + 6] == '9' + && mBuffer[j + 7] == '4' + && mBuffer[j + 8] == 3) { + j = + parseClosedCaption( + buffer, + i + j + PATTERN_LENGTH, + presentationTimeUs) + - i; + } else { + j += PATTERN_LENGTH; + } + } else { + ++j; + } + } + } + buffer.position(totalSize); + } + } + + private class H264CcParser extends CcParser { + private static final int PATTERN_LENGTH = 14; + + @Override + public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) { + int totalSize = buffer.position(); + // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with + // overlapping to handle the case that the pattern exists in the boundary. + for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) { + buffer.position(i); + int size = Math.min(totalSize - i, mBuffer.length); + buffer.get(mBuffer, 0, size); + int j = 0; + while (j < size - PATTERN_LENGTH) { + // Find the start prefix code of a NAL Unit. + if (mBuffer[j] == 0 && mBuffer[j + 1] == 0 && mBuffer[j + 2] == 1) { + int nalType = mBuffer[j + 3] & 0x1f; + int payloadType = mBuffer[j + 4] & 0xff; + + // ATSC closed caption data embedded in H264 private user data has NAL type + // 6, payload type 4, and 'GA94' user identifier for ATSC. + if (nalType == 6 + && payloadType == 4 + && mBuffer[j + 9] == 'G' + && mBuffer[j + 10] == 'A' + && mBuffer[j + 11] == '9' + && mBuffer[j + 12] == '4') { + j = + parseClosedCaption( + buffer, + i + j + PATTERN_LENGTH, + presentationTimeUs) + - i; + } else { + j += 7; + } + } else { + ++j; + } + } + } + buffer.position(totalSize); + } + } +} |