Support DASH Live TTML subtitles.
Also add missing file.
This commit is contained in:
parent
bb024fda08
commit
9d4e177347
@ -41,6 +41,7 @@ import com.google.android.exoplayer.drm.DrmSessionManager;
|
|||||||
import com.google.android.exoplayer.drm.MediaDrmCallback;
|
import com.google.android.exoplayer.drm.MediaDrmCallback;
|
||||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
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.text.webvtt.WebvttParser;
|
||||||
import com.google.android.exoplayer.upstream.BufferPool;
|
import com.google.android.exoplayer.upstream.BufferPool;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
@ -274,8 +275,8 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
SampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
SampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||||
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
||||||
DemoPlayer.TYPE_TEXT);
|
DemoPlayer.TYPE_TEXT);
|
||||||
textRenderer = new TextTrackRenderer(textSampleSource, new WebvttParser(), player,
|
textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper(),
|
||||||
mainHandler.getLooper());
|
new TtmlParser(), new WebvttParser());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke the callback.
|
// Invoke the callback.
|
||||||
|
@ -233,8 +233,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
|||||||
ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||||
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
||||||
DemoPlayer.TYPE_TEXT);
|
DemoPlayer.TYPE_TEXT);
|
||||||
textRenderer = new TextTrackRenderer(ttmlSampleSource, new TtmlParser(), player,
|
textRenderer = new TextTrackRenderer(ttmlSampleSource, player, mainHandler.getLooper(),
|
||||||
mainHandler.getLooper());
|
new TtmlParser());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke the callback.
|
// Invoke the callback.
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer;
|
package com.google.android.exoplayer;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@ -86,6 +87,15 @@ public class MediaFormat {
|
|||||||
sampleRate, bitrate, initializationData);
|
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)
|
@TargetApi(16)
|
||||||
private MediaFormat(android.media.MediaFormat format) {
|
private MediaFormat(android.media.MediaFormat format) {
|
||||||
this.frameworkMediaFormat = format;
|
this.frameworkMediaFormat = format;
|
||||||
|
@ -272,16 +272,23 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
|||||||
downstreamPositionUs = positionUs;
|
downstreamPositionUs = positionUs;
|
||||||
chunkSource.continueBuffering(positionUs);
|
chunkSource.continueBuffering(positionUs);
|
||||||
updateLoadControl();
|
updateLoadControl();
|
||||||
|
|
||||||
|
boolean haveSamples = false;
|
||||||
if (isPendingReset() || mediaChunks.isEmpty()) {
|
if (isPendingReset() || mediaChunks.isEmpty()) {
|
||||||
return false;
|
// No sample available.
|
||||||
} else if (mediaChunks.getFirst().sampleAvailable()) {
|
} else if (mediaChunks.getFirst().sampleAvailable()) {
|
||||||
// There's a sample available to be read from the current chunk.
|
// There's a sample available to be read from the current chunk.
|
||||||
return true;
|
haveSamples = true;
|
||||||
} else {
|
} else {
|
||||||
// It may be the case that the current chunk has been fully read but not yet discarded and
|
// 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.
|
// 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
|
@Override
|
||||||
@ -380,7 +387,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void maybeThrowLoadableException() throws IOException {
|
private void maybeThrowLoadableException() throws IOException {
|
||||||
if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) {
|
if (currentLoadableException != null && (currentLoadableExceptionFatal
|
||||||
|
|| currentLoadableExceptionCount > minLoadableRetryCount)) {
|
||||||
throw currentLoadableException;
|
throw currentLoadableException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,7 @@ import java.util.ArrayList;
|
|||||||
public static final int TYPE_uuid = 0x75756964;
|
public static final int TYPE_uuid = 0x75756964;
|
||||||
public static final int TYPE_senc = 0x73656E63;
|
public static final int TYPE_senc = 0x73656E63;
|
||||||
public static final int TYPE_pasp = 0x70617370;
|
public static final int TYPE_pasp = 0x70617370;
|
||||||
|
public static final int TYPE_TTML = 0x54544D4C;
|
||||||
|
|
||||||
public final int type;
|
public final int type;
|
||||||
|
|
||||||
|
@ -428,7 +428,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||||||
private static Track parseTrak(ContainerAtom trak) {
|
private static Track parseTrak(ContainerAtom trak) {
|
||||||
ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
|
ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
|
||||||
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
|
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);
|
Pair<Integer, Long> header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
|
||||||
int id = header.first;
|
int id = header.first;
|
||||||
@ -528,6 +529,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||||||
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize);
|
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize);
|
||||||
mediaFormat = audioSampleEntry.first;
|
mediaFormat = audioSampleEntry.first;
|
||||||
trackEncryptionBoxes[i] = audioSampleEntry.second;
|
trackEncryptionBoxes[i] = audioSampleEntry.second;
|
||||||
|
} else if (childAtomType == Atom.TYPE_TTML) {
|
||||||
|
mediaFormat = MediaFormat.createTtmlFormat();
|
||||||
}
|
}
|
||||||
stsd.setPosition(childStartPosition + childAtomSize);
|
stsd.setPosition(childStartPosition + childAtomSize);
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,10 @@ public final class Track {
|
|||||||
* Type of an audio track.
|
* Type of an audio track.
|
||||||
*/
|
*/
|
||||||
public static final int TYPE_AUDIO = 0x736F756E;
|
public static final int TYPE_AUDIO = 0x736F756E;
|
||||||
|
/**
|
||||||
|
* Type of a text track.
|
||||||
|
*/
|
||||||
|
public static final int TYPE_TEXT = 0x74657874;
|
||||||
/**
|
/**
|
||||||
* Type of a hint track.
|
* Type of a hint track.
|
||||||
*/
|
*/
|
||||||
|
@ -358,8 +358,9 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
MediaFormat format = MediaFormat.createAudioFormat(mimeType, -1, trackElement.numChannels,
|
MediaFormat format = MediaFormat.createAudioFormat(mimeType, -1, trackElement.numChannels,
|
||||||
trackElement.sampleRate, csd);
|
trackElement.sampleRate, csd);
|
||||||
return format;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,8 +58,9 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
private final TextRenderer textRenderer;
|
private final TextRenderer textRenderer;
|
||||||
private final SampleSource source;
|
private final SampleSource source;
|
||||||
private final MediaFormatHolder formatHolder;
|
private final MediaFormatHolder formatHolder;
|
||||||
private final SubtitleParser subtitleParser;
|
private final SubtitleParser[] subtitleParsers;
|
||||||
|
|
||||||
|
private int parserIndex;
|
||||||
private int trackIndex;
|
private int trackIndex;
|
||||||
|
|
||||||
private long currentPositionUs;
|
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 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 textRenderer The text renderer.
|
||||||
* @param textRendererLooper The looper associated with the thread on which textRenderer should be
|
* @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
|
* 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
|
* 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
|
* 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.
|
* 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,
|
public TextTrackRenderer(SampleSource source, TextRenderer textRenderer,
|
||||||
TextRenderer textRenderer, Looper textRendererLooper) {
|
Looper textRendererLooper, SubtitleParser... subtitleParsers) {
|
||||||
this.source = Assertions.checkNotNull(source);
|
this.source = Assertions.checkNotNull(source);
|
||||||
this.subtitleParser = Assertions.checkNotNull(subtitleParser);
|
|
||||||
this.textRenderer = Assertions.checkNotNull(textRenderer);
|
this.textRenderer = Assertions.checkNotNull(textRenderer);
|
||||||
this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper,
|
this.textRendererHandler = textRendererLooper == null ? null
|
||||||
this);
|
: new Handler(textRendererLooper, this);
|
||||||
|
this.subtitleParsers = Assertions.checkNotNull(subtitleParsers);
|
||||||
formatHolder = new MediaFormatHolder();
|
formatHolder = new MediaFormatHolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,10 +103,13 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ExoPlaybackException(e);
|
throw new ExoPlaybackException(e);
|
||||||
}
|
}
|
||||||
for (int i = 0; i < source.getTrackCount(); i++) {
|
for (int i = 0; i < subtitleParsers.length; i++) {
|
||||||
if (subtitleParser.canParse(source.getTrackInfo(i).mimeType)) {
|
for (int j = 0; j < source.getTrackCount(); j++) {
|
||||||
trackIndex = i;
|
if (subtitleParsers[i].canParse(source.getTrackInfo(j).mimeType)) {
|
||||||
return TrackRenderer.STATE_PREPARED;
|
parserIndex = i;
|
||||||
|
trackIndex = j;
|
||||||
|
return TrackRenderer.STATE_PREPARED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return TrackRenderer.STATE_IGNORE;
|
return TrackRenderer.STATE_IGNORE;
|
||||||
@ -115,7 +120,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
source.enable(trackIndex, positionUs);
|
source.enable(trackIndex, positionUs);
|
||||||
parserThread = new HandlerThread("textParser");
|
parserThread = new HandlerThread("textParser");
|
||||||
parserThread.start();
|
parserThread.start();
|
||||||
parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParser);
|
parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParsers[parserIndex]);
|
||||||
seekToInternal(positionUs);
|
seekToInternal(positionUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +194,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
|
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
|
||||||
if (result == SampleSource.SAMPLE_READ) {
|
if (result == SampleSource.SAMPLE_READ) {
|
||||||
parserHelper.startParseOperation();
|
parserHelper.startParseOperation();
|
||||||
|
textRendererNeedsUpdate = false;
|
||||||
} else if (result == SampleSource.END_OF_STREAM) {
|
} else if (result == SampleSource.END_OF_STREAM) {
|
||||||
inputStreamEnded = true;
|
inputStreamEnded = true;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user