Support DASH Live TTML subtitles.

Also add missing file.
This commit is contained in:
Oliver Woodman 2014-12-12 14:12:00 +00:00
parent bb024fda08
commit 9d4e177347
10 changed files with 94 additions and 22 deletions

View File

@ -41,6 +41,7 @@ import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.drm.MediaDrmCallback;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.text.TextTrackRenderer;
import com.google.android.exoplayer.text.ttml.TtmlParser;
import com.google.android.exoplayer.text.webvtt.WebvttParser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
@ -274,8 +275,8 @@ public class DashRendererBuilder implements RendererBuilder,
SampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_TEXT);
textRenderer = new TextTrackRenderer(textSampleSource, new WebvttParser(), player,
mainHandler.getLooper());
textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper(),
new TtmlParser(), new WebvttParser());
}
// Invoke the callback.

View File

@ -233,8 +233,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_TEXT);
textRenderer = new TextTrackRenderer(ttmlSampleSource, new TtmlParser(), player,
mainHandler.getLooper());
textRenderer = new TextTrackRenderer(ttmlSampleSource, player, mainHandler.getLooper(),
new TtmlParser());
}
// Invoke the callback.

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2014 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.google.android.exoplayer.demo.full.player;
/**
* Exception thrown when the required level of DRM is not supported.
*/
public final class UnsupportedDrmException extends Exception {
public static final int REASON_NO_DRM = 0;
public static final int REASON_UNSUPPORTED_SCHEME = 1;
public static final int REASON_UNKNOWN = 2;
public final int reason;
public UnsupportedDrmException(int reason) {
this.reason = reason;
}
public UnsupportedDrmException(int reason, Exception cause) {
super(cause);
this.reason = reason;
}
}

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.annotation.SuppressLint;
@ -86,6 +87,15 @@ public class MediaFormat {
sampleRate, bitrate, initializationData);
}
public static MediaFormat createTtmlFormat() {
return createFormatForMimeType(MimeTypes.APPLICATION_TTML);
}
public static MediaFormat createFormatForMimeType(String mimeType) {
return new MediaFormat(mimeType, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, null);
}
@TargetApi(16)
private MediaFormat(android.media.MediaFormat format) {
this.frameworkMediaFormat = format;

View File

@ -272,16 +272,23 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
downstreamPositionUs = positionUs;
chunkSource.continueBuffering(positionUs);
updateLoadControl();
boolean haveSamples = false;
if (isPendingReset() || mediaChunks.isEmpty()) {
return false;
// No sample available.
} else if (mediaChunks.getFirst().sampleAvailable()) {
// There's a sample available to be read from the current chunk.
return true;
haveSamples = true;
} else {
// It may be the case that the current chunk has been fully read but not yet discarded and
// that the next chunk has an available sample. Return true if so, otherwise false.
return mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable();
haveSamples = mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable();
}
if (!haveSamples) {
maybeThrowLoadableException();
}
return haveSamples;
}
@Override
@ -380,7 +387,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
}
private void maybeThrowLoadableException() throws IOException {
if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) {
if (currentLoadableException != null && (currentLoadableExceptionFatal
|| currentLoadableExceptionCount > minLoadableRetryCount)) {
throw currentLoadableException;
}
}

View File

@ -58,6 +58,7 @@ import java.util.ArrayList;
public static final int TYPE_uuid = 0x75756964;
public static final int TYPE_senc = 0x73656E63;
public static final int TYPE_pasp = 0x70617370;
public static final int TYPE_TTML = 0x54544D4C;
public final int type;

View File

@ -428,7 +428,8 @@ public final class FragmentedMp4Extractor implements Extractor {
private static Track parseTrak(ContainerAtom trak) {
ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO);
Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO
|| trackType == Track.TYPE_TEXT);
Pair<Integer, Long> header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
int id = header.first;
@ -528,6 +529,8 @@ public final class FragmentedMp4Extractor implements Extractor {
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize);
mediaFormat = audioSampleEntry.first;
trackEncryptionBoxes[i] = audioSampleEntry.second;
} else if (childAtomType == Atom.TYPE_TTML) {
mediaFormat = MediaFormat.createTtmlFormat();
}
stsd.setPosition(childStartPosition + childAtomSize);
}

View File

@ -30,6 +30,10 @@ public final class Track {
* Type of an audio track.
*/
public static final int TYPE_AUDIO = 0x736F756E;
/**
* Type of a text track.
*/
public static final int TYPE_TEXT = 0x74657874;
/**
* Type of a hint track.
*/

View File

@ -358,8 +358,9 @@ public class SmoothStreamingChunkSource implements ChunkSource {
MediaFormat format = MediaFormat.createAudioFormat(mimeType, -1, trackElement.numChannels,
trackElement.sampleRate, csd);
return format;
} else if (streamElement.type == StreamElement.TYPE_TEXT) {
return MediaFormat.createFormatForMimeType(streamElement.tracks[trackIndex].mimeType);
}
// TODO: Do subtitles need a format? MediaFormat supports KEY_LANGUAGE.
return null;
}

View File

@ -58,8 +58,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
private final TextRenderer textRenderer;
private final SampleSource source;
private final MediaFormatHolder formatHolder;
private final SubtitleParser subtitleParser;
private final SubtitleParser[] subtitleParsers;
private int parserIndex;
private int trackIndex;
private long currentPositionUs;
@ -73,21 +74,22 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
/**
* @param source A source from which samples containing subtitle data can be read.
* @param subtitleParser A subtitle parser that will parse Subtitle objects from the source.
* @param textRenderer The text renderer.
* @param textRendererLooper The looper associated with the thread on which textRenderer should be
* invoked. If the renderer makes use of standard Android UI components, then this should
* normally be the looper associated with the applications' main thread, which can be
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the
* renderer should be invoked directly on the player's internal rendering thread.
* @param subtitleParsers An array of available subtitle parsers. Where multiple parsers are able
* to render a subtitle, the one with the lowest index will be preferred.
*/
public TextTrackRenderer(SampleSource source, SubtitleParser subtitleParser,
TextRenderer textRenderer, Looper textRendererLooper) {
public TextTrackRenderer(SampleSource source, TextRenderer textRenderer,
Looper textRendererLooper, SubtitleParser... subtitleParsers) {
this.source = Assertions.checkNotNull(source);
this.subtitleParser = Assertions.checkNotNull(subtitleParser);
this.textRenderer = Assertions.checkNotNull(textRenderer);
this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper,
this);
this.textRendererHandler = textRendererLooper == null ? null
: new Handler(textRendererLooper, this);
this.subtitleParsers = Assertions.checkNotNull(subtitleParsers);
formatHolder = new MediaFormatHolder();
}
@ -101,10 +103,13 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
for (int i = 0; i < source.getTrackCount(); i++) {
if (subtitleParser.canParse(source.getTrackInfo(i).mimeType)) {
trackIndex = i;
return TrackRenderer.STATE_PREPARED;
for (int i = 0; i < subtitleParsers.length; i++) {
for (int j = 0; j < source.getTrackCount(); j++) {
if (subtitleParsers[i].canParse(source.getTrackInfo(j).mimeType)) {
parserIndex = i;
trackIndex = j;
return TrackRenderer.STATE_PREPARED;
}
}
}
return TrackRenderer.STATE_IGNORE;
@ -115,7 +120,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
source.enable(trackIndex, positionUs);
parserThread = new HandlerThread("textParser");
parserThread.start();
parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParser);
parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParsers[parserIndex]);
seekToInternal(positionUs);
}
@ -189,6 +194,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ) {
parserHelper.startParseOperation();
textRendererNeedsUpdate = false;
} else if (result == SampleSource.END_OF_STREAM) {
inputStreamEnded = true;
}