blob: 582f18c5966d25f828f92dcd0e7dca54c359c346 [file] [log] [blame]
/*
* 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.PlaybackBufferListener;
import com.android.tv.tuner.exoplayer.buffer.SamplePool;
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 com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
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
* @param concurrentDvrPlaybackFlags
*/
public MpegTsSampleExtractor(
DataSource source,
BufferManager bufferManager,
PlaybackBufferListener bufferListener,
ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
mSampleExtractor =
new ExoPlayerSampleExtractor(
Uri.EMPTY,
source,
bufferManager,
bufferListener,
false,
concurrentDvrPlaybackFlags);
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,
ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
mSampleExtractor =
new FileSampleExtractor(bufferManager, bufferListener, concurrentDvrPlaybackFlags);
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);
}
}
}