From 9d4e1773474579ac1be07ca588ff06bd75dce10d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 12 Dec 2014 14:12:00 +0000 Subject: [PATCH] Support DASH Live TTML subtitles. Also add missing file. --- .../demo/full/player/DashRendererBuilder.java | 5 ++- .../SmoothStreamingRendererBuilder.java | 4 +- .../full/player/UnsupportedDrmException.java | 38 +++++++++++++++++++ .../google/android/exoplayer/MediaFormat.java | 10 +++++ .../exoplayer/chunk/ChunkSampleSource.java | 16 ++++++-- .../android/exoplayer/parser/mp4/Atom.java | 1 + .../parser/mp4/FragmentedMp4Extractor.java | 5 ++- .../android/exoplayer/parser/mp4/Track.java | 4 ++ .../SmoothStreamingChunkSource.java | 3 +- .../exoplayer/text/TextTrackRenderer.java | 30 +++++++++------ 10 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 demo/src/main/java/com/google/android/exoplayer/demo/full/player/UnsupportedDrmException.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java index 81a9d0268d..907175aea6 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java @@ -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. diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index 1a515ba4d2..e02ebe5ca8 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -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. diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/UnsupportedDrmException.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/UnsupportedDrmException.java new file mode 100644 index 0000000000..3776b8bef5 --- /dev/null +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/UnsupportedDrmException.java @@ -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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index 24db47ff77..20a45738bc 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -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; diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index f16fc4c4df..038289d841 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -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; } } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java index 60c9ae6984..9a2341e904 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java @@ -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; diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java index 34f0404083..52fe8a94a1 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java @@ -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 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); } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Track.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Track.java index 710626bc2e..a5306c70fa 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Track.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Track.java @@ -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. */ diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 936fdf824d..0bc36a0b8b 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -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; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 4fd581bf56..c85eb469c1 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -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; }