diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java index ef9433b3f0..1568e28de5 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java @@ -18,24 +18,18 @@ package com.google.android.exoplayer.demo.player; import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; -import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.ChunkSource; -import com.google.android.exoplayer.chunk.Format; -import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; -import com.google.android.exoplayer.chunk.MultiTrackChunkSource; -import com.google.android.exoplayer.chunk.VideoFormatSelectorUtil; import com.google.android.exoplayer.dash.DashChunkSource; +import com.google.android.exoplayer.dash.DefaultDashTrackSelector; import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; import com.google.android.exoplayer.dash.mpd.Period; -import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.dash.mpd.UtcTimingElement; import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver; import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback; @@ -44,8 +38,6 @@ import com.google.android.exoplayer.drm.MediaDrmCallback; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.drm.UnsupportedDrmException; 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.DataSource; import com.google.android.exoplayer.upstream.DefaultAllocator; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; @@ -60,8 +52,6 @@ import android.os.Handler; import android.util.Log; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; /** * A {@link RendererBuilder} for DASH. @@ -194,23 +184,11 @@ public class DashRendererBuilder implements RendererBuilder { DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); boolean hasContentProtection = false; - int videoAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_VIDEO); - int audioAdaptationSetIndex = period.getAdaptationSetIndex(AdaptationSet.TYPE_AUDIO); - AdaptationSet videoAdaptationSet = null; - AdaptationSet audioAdaptationSet = null; - if (videoAdaptationSetIndex != -1) { - videoAdaptationSet = period.adaptationSets.get(videoAdaptationSetIndex); - hasContentProtection |= videoAdaptationSet.hasContentProtection(); - } - if (audioAdaptationSetIndex != -1) { - audioAdaptationSet = period.adaptationSets.get(audioAdaptationSetIndex); - hasContentProtection |= audioAdaptationSet.hasContentProtection(); - } - - // Fail if we have neither video or audio. - if (videoAdaptationSet == null && audioAdaptationSet == null) { - player.onRenderersError(new IllegalStateException("No video or audio adaptation sets")); - return; + for (int i = 0; i < period.adaptationSets.size(); i++) { + AdaptationSet adaptationSet = period.adaptationSets.get(i); + if (adaptationSet.type != AdaptationSet.TYPE_UNKNOWN) { + hasContentProtection |= adaptationSet.hasContentProtection(); + } } // Check drm support if necessary. @@ -225,129 +203,49 @@ public class DashRendererBuilder implements RendererBuilder { try { drmSessionManager = StreamingDrmSessionManager.newWidevineInstance( player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); - filterHdContent = videoAdaptationSet != null && videoAdaptationSet.hasContentProtection() - && getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1; + filterHdContent = getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1; } catch (UnsupportedDrmException e) { player.onRenderersError(e); return; } } - // Determine which video representations we should use for playback. - int[] videoRepresentationIndices = null; - if (videoAdaptationSet != null) { - try { - videoRepresentationIndices = VideoFormatSelectorUtil.selectVideoFormatsForDefaultDisplay( - context, videoAdaptationSet.representations, null, filterHdContent); - } catch (DecoderQueryException e) { - player.onRenderersError(e); - return; - } - } - // Build the video renderer. - final MediaCodecVideoTrackRenderer videoRenderer; - if (videoRepresentationIndices == null || videoRepresentationIndices.length == 0) { - videoRenderer = null; - } else { - DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); - ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, - videoAdaptationSetIndex, videoRepresentationIndices, videoDataSource, - new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset, - mainHandler, player); - ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, - VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, - DemoPlayer.TYPE_VIDEO); - videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true, - MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50); - } - - // Build the audio chunk sources. - List audioChunkSourceList = new ArrayList<>(); - List audioTrackNameList = new ArrayList<>(); - if (audioAdaptationSet != null) { - DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); - FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); - List audioRepresentations = audioAdaptationSet.representations; - List codecs = new ArrayList<>(); - for (int i = 0; i < audioRepresentations.size(); i++) { - Format format = audioRepresentations.get(i).format; - audioTrackNameList.add(format.id + " (" + format.audioChannels + "ch, " + - format.audioSamplingRate + "Hz)"); - audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, - new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS, - elapsedRealtimeOffset, mainHandler, player)); - codecs.add(format.codecs); - } - } + DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, + DefaultDashTrackSelector.newVideoInstance(context, true, filterHdContent), + videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, + elapsedRealtimeOffset, mainHandler, player); + ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, + VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, + DemoPlayer.TYPE_VIDEO); + TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, + drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, + mainHandler, player, 50); // Build the audio renderer. - final String[] audioTrackNames; - final MultiTrackChunkSource audioChunkSource; - final TrackRenderer audioRenderer; - if (audioChunkSourceList.isEmpty()) { - audioTrackNames = null; - audioChunkSource = null; - audioRenderer = null; - } else { - audioTrackNames = new String[audioTrackNameList.size()]; - audioTrackNameList.toArray(audioTrackNames); - audioChunkSource = new MultiTrackChunkSource(audioChunkSourceList); - SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, - AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, - DemoPlayer.TYPE_AUDIO); - audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true, - mainHandler, player, AudioCapabilities.getCapabilities(context)); - } + DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher, + DefaultDashTrackSelector.newAudioInstance(), audioDataSource, null, LIVE_EDGE_LATENCY_MS, + elapsedRealtimeOffset, mainHandler, player); + ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, + AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, + DemoPlayer.TYPE_AUDIO); + TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, + drmSessionManager, true, mainHandler, player, AudioCapabilities.getCapabilities(context)); - // Build the text chunk sources. + // Build the text renderer. DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); - FormatEvaluator textEvaluator = new FormatEvaluator.FixedEvaluator(); - List textChunkSourceList = new ArrayList<>(); - List textTrackNameList = new ArrayList<>(); - for (int i = 0; i < period.adaptationSets.size(); i++) { - AdaptationSet adaptationSet = period.adaptationSets.get(i); - if (adaptationSet.type == AdaptationSet.TYPE_TEXT) { - List representations = adaptationSet.representations; - for (int j = 0; j < representations.size(); j++) { - Representation representation = representations.get(j); - textTrackNameList.add(representation.format.id); - textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j}, - textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset, - mainHandler, player)); - } - } - } - - // Build the text renderers - final String[] textTrackNames; - final MultiTrackChunkSource textChunkSource; - final TrackRenderer textRenderer; - if (textChunkSourceList.isEmpty()) { - textTrackNames = null; - textChunkSource = null; - textRenderer = null; - } else { - textTrackNames = new String[textTrackNameList.size()]; - textTrackNameList.toArray(textTrackNames); - textChunkSource = new MultiTrackChunkSource(textChunkSourceList); - SampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, - TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, - DemoPlayer.TYPE_TEXT); - textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper(), - new TtmlParser(), new WebvttParser()); - } + ChunkSource textChunkSource = new DashChunkSource(manifestFetcher, + DefaultDashTrackSelector.newTextInstance(), textDataSource, null, LIVE_EDGE_LATENCY_MS, + elapsedRealtimeOffset, mainHandler, player); + ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, + TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, + DemoPlayer.TYPE_TEXT); + TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player, + mainHandler.getLooper()); // Invoke the callback. - String[][] trackNames = new String[DemoPlayer.RENDERER_COUNT][]; - trackNames[DemoPlayer.TYPE_AUDIO] = audioTrackNames; - trackNames[DemoPlayer.TYPE_TEXT] = textTrackNames; - - MultiTrackChunkSource[] multiTrackChunkSources = - new MultiTrackChunkSource[DemoPlayer.RENDERER_COUNT]; - multiTrackChunkSources[DemoPlayer.TYPE_AUDIO] = audioChunkSource; - multiTrackChunkSources[DemoPlayer.TYPE_TEXT] = textChunkSource; - TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index acefd9930a..dae1c91695 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -342,7 +342,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi player.release(); } - public int getPlaybackState() { if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { return STATE_PREPARING; diff --git a/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/DashRendererBuilder.java b/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/DashRendererBuilder.java index 7c35fcfa63..4f59a3b396 100644 --- a/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/DashRendererBuilder.java +++ b/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/DashRendererBuilder.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.ChunkSource; -import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.dash.mpd.AdaptationSet; @@ -109,7 +108,7 @@ public class DashRendererBuilder implements ManifestCallback queue = new ArrayList<>(); @@ -232,7 +232,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { private static MediaPresentationDescription buildMpd(long durationMs, List representations, boolean live, boolean limitTimeshiftBuffer) { - AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); + AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_VIDEO, representations); Period period = new Period(null, 0, Collections.singletonList(adaptationSet)); return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, durationMs, -1, live, -1, (limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1, null, null, @@ -259,7 +259,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { long periodDurationMs = VOD_DURATION_MS; for (int i = 0; i < 2; i++) { Representation representation = buildVodRepresentation(REGULAR_VIDEO); - AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, + AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_VIDEO, Collections.singletonList(representation)); Period period = new Period(null, timeMs, Collections.singletonList(adaptationSet)); periods.add(period); @@ -288,7 +288,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { long periodDurationMs = LIVE_DURATION_MS; for (int i = 0; i < MULTI_PERIOD_COUNT; i++) { Representation representation = buildSegmentTimelineRepresentation(LIVE_DURATION_MS, 0); - AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, + AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_VIDEO, Collections.singletonList(representation)); Period period = new Period(null, periodStartTimeMs, Collections.singletonList(adaptationSet)); periods.add(period); @@ -303,7 +303,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { long periodDurationMs = LIVE_DURATION_MS; for (int i = 0; i < MULTI_PERIOD_COUNT; i++) { Representation representation = buildSegmentTemplateRepresentation(); - AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, + AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_VIDEO, Collections.singletonList(representation)); Period period = new Period(null, periodStartTimeMs, Collections.singletonList(adaptationSet)); periods.add(period); @@ -322,9 +322,10 @@ public class DashChunkSourceTest extends InstrumentationTestCase { ManifestFetcher manifestFetcher = mock(ManifestFetcher.class); when(manifestFetcher.getManifest()).thenReturn(mpd); DashChunkSource chunkSource = new DashChunkSource(manifestFetcher, mpd, - AdaptationSet.TYPE_VIDEO, null, mock(DataSource.class), EVALUATOR, + DefaultDashTrackSelector.newVideoInstance(null, false, false), mock(DataSource.class), null, new FakeClock(mpd.availabilityStartTime + mpd.duration - ELAPSED_REALTIME_OFFSET_MS), liveEdgeLatencyMs * 1000, ELAPSED_REALTIME_OFFSET_MS * 1000, startAtLiveEdge, null, null); + chunkSource.prepare(); chunkSource.enable(0); return chunkSource; } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java index 2a51262b7c..dd978ba51b 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer.chunk; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.dash.DashChunkSource; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource; import com.google.android.exoplayer.util.Assertions; import java.io.IOException; @@ -26,9 +28,12 @@ import java.util.List; /** * A {@link ChunkSource} providing the ability to switch between multiple other {@link ChunkSource} * instances. + * + * @deprecated {@link DashChunkSource} and {@link SmoothStreamingChunkSource} both support multiple + * tracks directly, so use of this class should not be required. It will be deleted once legacy + * uses have been removed. */ -// TODO: Expose multiple tracks directly in DashChunkSource and SmoothStreamingChunkSource, and -// delete this class. +@Deprecated public final class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent { /** diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index d59d1b2115..b995715dc7 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer.chunk.InitializationChunk; import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer.dash.DashTrackSelector.Output; import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.ContentProtection; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; @@ -41,7 +42,6 @@ import com.google.android.exoplayer.dash.mpd.RangedUri; import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.drm.DrmInitData; import com.google.android.exoplayer.extractor.ChunkIndex; -import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer.extractor.webm.WebmExtractor; import com.google.android.exoplayer.upstream.DataSource; @@ -55,6 +55,7 @@ import android.os.Handler; import android.util.SparseArray; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -73,7 +74,7 @@ import java.util.List; * */ // TODO: handle cases where the above assumption are false -public class DashChunkSource implements ChunkSource { +public class DashChunkSource implements ChunkSource, Output { /** * Interface definition for a callback to be notified of {@link DashChunkSource} events. @@ -100,38 +101,26 @@ public class DashChunkSource implements ChunkSource { } - /** - * Specifies that we should process all tracks. - */ - public static final int USE_ALL_TRACKS = -1; - private final Handler eventHandler; private final EventListener eventListener; - private final MediaFormat trackFormat; private final DataSource dataSource; - private final FormatEvaluator formatEvaluator; + private final FormatEvaluator adaptiveFormatEvaluator; private final Evaluation evaluation; + private final ManifestFetcher manifestFetcher; + private final DashTrackSelector trackSelector; + private final ArrayList tracks; + private final SparseArray periodHolders; private final Clock systemClock; private final long liveEdgeLatencyUs; private final long elapsedRealtimeOffsetUs; - private final int maxWidth; - private final int maxHeight; private final long[] availableRangeValues; - private final SparseArray periodHolders; - - private final ManifestFetcher manifestFetcher; - private final int adaptationSetIndex; - private final int[] representationIndices; - private MediaPresentationDescription currentManifest; - - private int periodHolderNextIndex; - - private DrmInitData drmInitData; + private ExposedTrack enabledTrack; + private int nextPeriodHolderIndex; private TimeRange availableRange; - + private boolean prepareCalled; private boolean startAtLiveEdge; private boolean lastChunkWasInitialization; private IOException fatalError; @@ -140,44 +129,49 @@ public class DashChunkSource implements ChunkSource { * Lightweight constructor to use for fixed duration content. * * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param formatEvaluator Selects from the available formats. + * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. * @param durationMs The duration of the content. + * @param adaptationSetType The type of the adaptation set to which the representations belong. + * One of {@link AdaptationSet#TYPE_AUDIO}, {@link AdaptationSet#TYPE_VIDEO} and + * {@link AdaptationSet#TYPE_TEXT}. * @param representations The representations to be considered by the source. */ - public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator, - long durationMs, Representation... representations) { - this(buildManifest(durationMs, Arrays.asList(representations)), 0, null, dataSource, - formatEvaluator); + public DashChunkSource(DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator, + long durationMs, int adaptationSetType, Representation... representations) { + this(buildManifest(durationMs, adaptationSetType, Arrays.asList(representations)), null, + dataSource, adaptiveFormatEvaluator); } /** * Lightweight constructor to use for fixed duration content. * * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param formatEvaluator Selects from the available formats. + * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. * @param durationMs The duration of the content. + * @param adaptationSetType The type of the adaptation set to which the representations belong. + * One of {@link AdaptationSet#TYPE_AUDIO}, {@link AdaptationSet#TYPE_VIDEO} and + * {@link AdaptationSet#TYPE_TEXT}. * @param representations The representations to be considered by the source. */ - public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator, - long durationMs, List representations) { - this(buildManifest(durationMs, representations), 0, null, dataSource, formatEvaluator); + public DashChunkSource(DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator, + long durationMs, int adaptationSetType, List representations) { + this(buildManifest(durationMs, adaptationSetType, representations), + DefaultDashTrackSelector.newVideoInstance(null, false, false), dataSource, + adaptiveFormatEvaluator); } /** * Constructor to use for fixed duration content. * * @param manifest The manifest. - * @param adaptationSetIndex The index of the adaptation set that should be used. - * @param representationIndices The indices of the representations within the adaptations set - * that should be used. May be null if all representations within the adaptation set should - * be considered. + * @param trackSelector Selects tracks from manifest periods to be exposed by this source. * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param formatEvaluator Selects from the available formats. + * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. */ - public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex, - int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) { - this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator, - new SystemClock(), 0, 0, false, null, null); + public DashChunkSource(MediaPresentationDescription manifest, DashTrackSelector trackSelector, + DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator) { + this(null, manifest, trackSelector, dataSource, adaptiveFormatEvaluator, new SystemClock(), 0, + 0, false, null, null); } /** @@ -188,12 +182,9 @@ public class DashChunkSource implements ChunkSource { * * @param manifestFetcher A fetcher for the manifest, which must have already successfully * completed an initial load. - * @param adaptationSetIndex The index of the adaptation set that should be used. - * @param representationIndices The indices of the representations within the adaptations set - * that should be used. May be null if all representations within the adaptation set should - * be considered. + * @param trackSelector Selects tracks from manifest periods to be exposed by this source. * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param formatEvaluator Selects from the available formats. + * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. * @param liveEdgeLatencyMs For live streams, the number of milliseconds that the playback should * lag behind the "live edge" (i.e. the end of the most recently defined media in the * manifest). Choosing a small value will minimize latency introduced by the player, however @@ -207,11 +198,11 @@ public class DashChunkSource implements ChunkSource { * @param eventListener A listener of events. May be null if delivery of events is not required. */ public DashChunkSource(ManifestFetcher manifestFetcher, - int adaptationSetIndex, int[] representationIndices, DataSource dataSource, - FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs, + DashTrackSelector trackSelector, DataSource dataSource, + FormatEvaluator adaptiveFormatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs, Handler eventHandler, EventListener eventListener) { - this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices, - dataSource, formatEvaluator, new SystemClock(), liveEdgeLatencyMs * 1000, + this(manifestFetcher, manifestFetcher.getManifest(), trackSelector, + dataSource, adaptiveFormatEvaluator, new SystemClock(), liveEdgeLatencyMs * 1000, elapsedRealtimeOffsetMs * 1000, true, eventHandler, eventListener); } @@ -220,12 +211,9 @@ public class DashChunkSource implements ChunkSource { * * @param manifestFetcher A fetcher for the manifest, which must have already successfully * completed an initial load. - * @param adaptationSetIndex The index of the adaptation set that should be used. - * @param representationIndices The indices of the representations within the adaptations set - * that should be used. May be null if all representations within the adaptation set should - * be considered. + * @param trackSelector Selects tracks from manifest periods to be exposed by this source. * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param formatEvaluator Selects from the available formats. + * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. * @param liveEdgeLatencyMs For live streams, the number of milliseconds that the playback should * lag behind the "live edge" (i.e. the end of the most recently defined media in the * manifest). Choosing a small value will minimize latency introduced by the player, however @@ -241,25 +229,24 @@ public class DashChunkSource implements ChunkSource { * @param eventListener A listener of events. May be null if delivery of events is not required. */ public DashChunkSource(ManifestFetcher manifestFetcher, - int adaptationSetIndex, int[] representationIndices, DataSource dataSource, - FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs, + DashTrackSelector trackSelector, DataSource dataSource, + FormatEvaluator adaptiveFormatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs, boolean startAtLiveEdge, Handler eventHandler, EventListener eventListener) { - this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices, - dataSource, formatEvaluator, new SystemClock(), liveEdgeLatencyMs * 1000, + this(manifestFetcher, manifestFetcher.getManifest(), trackSelector, + dataSource, adaptiveFormatEvaluator, new SystemClock(), liveEdgeLatencyMs * 1000, elapsedRealtimeOffsetMs * 1000, startAtLiveEdge, eventHandler, eventListener); } /* package */ DashChunkSource(ManifestFetcher manifestFetcher, - MediaPresentationDescription initialManifest, int adaptationSetIndex, - int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator, + MediaPresentationDescription initialManifest, DashTrackSelector trackSelector, + DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator, Clock systemClock, long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs, boolean startAtLiveEdge, Handler eventHandler, EventListener eventListener) { this.manifestFetcher = manifestFetcher; this.currentManifest = initialManifest; - this.adaptationSetIndex = adaptationSetIndex; - this.representationIndices = representationIndices; + this.trackSelector = trackSelector; this.dataSource = dataSource; - this.formatEvaluator = formatEvaluator; + this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; this.systemClock = systemClock; this.liveEdgeLatencyUs = liveEdgeLatencyUs; this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetUs; @@ -268,74 +255,56 @@ public class DashChunkSource implements ChunkSource { this.eventListener = eventListener; this.evaluation = new Evaluation(); this.availableRangeValues = new long[2]; - - drmInitData = getDrmInitData(currentManifest, adaptationSetIndex); periodHolders = new SparseArray<>(); + tracks = new ArrayList(); + } - processManifest(currentManifest); + // ChunkSource implementation. - long totalDurationUs = 0; - int maxWidth = 0; - int maxHeight = 0; - String mimeType = ""; - for (int i = 0; i < periodHolders.size(); i++) { - PeriodHolder periodHolder = periodHolders.valueAt(i); - if (totalDurationUs != C.UNKNOWN_TIME_US) { - if (periodHolder.durationUs == C.UNKNOWN_TIME_US) { - totalDurationUs = C.UNKNOWN_TIME_US; - } else { - totalDurationUs += periodHolder.durationUs; - } - } - maxWidth = Math.max(maxWidth, periodHolder.maxWidth); - maxHeight = Math.max(maxHeight, periodHolder.maxHeight); - mimeType = periodHolder.mimeType; + @Override + public void maybeThrowError() throws IOException { + if (fatalError != null) { + throw fatalError; + } else if (manifestFetcher != null) { + manifestFetcher.maybeThrowError(); } - this.maxWidth = maxWidth == 0 ? MediaFormat.NO_VALUE : maxWidth; - this.maxHeight = maxHeight == 0 ? MediaFormat.NO_VALUE : maxHeight; - // TODO: Remove this and pass proper formats instead (b/22996976). - this.trackFormat = MediaFormat.createFormatForMimeType(mimeType, MediaFormat.NO_VALUE, - totalDurationUs); } @Override public boolean prepare() { - return true; + if (!prepareCalled) { + prepareCalled = true; + try { + trackSelector.selectTracks(currentManifest, 0, this); + } catch (IOException e) { + fatalError = e; + } + } + return fatalError == null; } @Override public int getTrackCount() { - return 1; + return tracks.size(); } @Override public final MediaFormat getFormat(int track) { - return trackFormat; - } - - // VisibleForTesting - /* package */ TimeRange getAvailableRange() { - return availableRange; + return tracks.get(track).trackFormat; } @Override public void enable(int track) { - fatalError = null; - formatEvaluator.enable(); + enabledTrack = tracks.get(track); + processManifest(currentManifest); + if (enabledTrack.isAdaptive()) { + adaptiveFormatEvaluator.enable(); + } if (manifestFetcher != null) { manifestFetcher.enable(); } } - @Override - public void disable(List queue) { - formatEvaluator.disable(); - if (manifestFetcher != null) { - manifestFetcher.disable(); - } - availableRange = null; - } - @Override public void continueBuffering(long playbackPositionUs) { if (manifestFetcher == null || !currentManifest.dynamic || fatalError != null) { @@ -372,15 +341,15 @@ public class DashChunkSource implements ChunkSource { evaluation.queueSize = queue.size(); if (evaluation.format == null || !lastChunkWasInitialization) { - PeriodHolder periodHolder = null; - if (!queue.isEmpty()) { - periodHolder = periodHolders.get(queue.get(queue.size() - 1).parentId); + if (enabledTrack.isAdaptive()) { + adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, enabledTrack.adaptiveFormats, + evaluation); + } else { + evaluation.format = enabledTrack.fixedFormat; + evaluation.trigger = Chunk.TRIGGER_MANUAL; } - if (periodHolder == null) { - periodHolder = periodHolders.valueAt(0); - } - formatEvaluator.evaluate(queue, playbackPositionUs, periodHolder.formats, evaluation); } + Format selectedFormat = evaluation.format; out.queueSize = evaluation.queueSize; @@ -467,7 +436,6 @@ public class DashChunkSource implements ChunkSource { RepresentationHolder representationHolder = periodHolder.representationHolders.get(selectedFormat.id); Representation selectedRepresentation = representationHolder.representation; - ChunkExtractorWrapper extractorWrapper = representationHolder.extractorWrapper; RangedUri pendingInitializationUri = null; RangedUri pendingIndexUri = null; @@ -483,8 +451,8 @@ public class DashChunkSource implements ChunkSource { if (pendingInitializationUri != null || pendingIndexUri != null) { // We have initialization and/or index requests to make. Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri, - selectedRepresentation, extractorWrapper, dataSource, periodHolder.localIndex, - evaluation.trigger); + selectedRepresentation, representationHolder.extractorWrapper, dataSource, + periodHolder.localIndex, evaluation.trigger); lastChunkWasInitialization = true; out.chunk = initializationChunk; return; @@ -499,15 +467,6 @@ public class DashChunkSource implements ChunkSource { out.chunk = nextMediaChunk; } - @Override - public void maybeThrowError() throws IOException { - if (fatalError != null) { - throw fatalError; - } else if (manifestFetcher != null) { - manifestFetcher.maybeThrowError(); - } - } - @Override public void onChunkLoadCompleted(Chunk chunk) { if (chunk instanceof InitializationChunk) { @@ -531,8 +490,8 @@ public class DashChunkSource implements ChunkSource { // The null check avoids overwriting drmInitData obtained from the manifest with drmInitData // obtained from the stream, as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - if (drmInitData == null && initializationChunk.hasDrmInitData()) { - drmInitData = initializationChunk.getDrmInitData(); + if (periodHolder.drmInitData == null && initializationChunk.hasDrmInitData()) { + periodHolder.drmInitData = initializationChunk.getDrmInitData(); } } } @@ -542,6 +501,105 @@ public class DashChunkSource implements ChunkSource { // Do nothing. } + @Override + public void disable(List queue) { + if (enabledTrack.isAdaptive()) { + adaptiveFormatEvaluator.disable(); + } + if (manifestFetcher != null) { + manifestFetcher.disable(); + } + periodHolders.clear(); + evaluation.format = null; + availableRange = null; + fatalError = null; + enabledTrack = null; + } + + // DashTrackSelector.Output implementation. + + @Override + public void adaptiveTrack(MediaPresentationDescription manifest, int periodIndex, + int adaptationSetIndex, int[] representationIndices) { + if (adaptiveFormatEvaluator == null) { + // Do nothing. + return; + } + AdaptationSet adaptationSet = manifest.getPeriod(periodIndex).adaptationSets.get( + adaptationSetIndex); + int maxWidth = 0; + int maxHeight = 0; + Format maxHeightRepresentationFormat = null; + Format[] representationFormats = new Format[representationIndices.length]; + for (int i = 0; i < representationFormats.length; i++) { + Format format = adaptationSet.representations.get(representationIndices[i]).format; + if (maxHeightRepresentationFormat == null || format.height > maxHeight) { + maxHeightRepresentationFormat = format; + } + maxWidth = Math.max(maxWidth, format.width); + maxHeight = Math.max(maxHeight, format.height); + representationFormats[i] = format; + } + Arrays.sort(representationFormats, new DecreasingBandwidthComparator()); + long trackDurationUs = manifest.dynamic ? C.UNKNOWN_TIME_US : manifest.duration * 1000; + MediaFormat trackFormat = buildTrackFormat(adaptationSet.type, maxHeightRepresentationFormat, + trackDurationUs).copyAsAdaptive(); + tracks.add(new ExposedTrack(trackFormat, adaptationSetIndex, representationFormats, maxWidth, + maxHeight)); + } + + @Override + public void fixedTrack(MediaPresentationDescription manifest, int periodIndex, + int adaptationSetIndex, int representationIndex) { + List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; + AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); + Format representationFormat = adaptationSet.representations.get(representationIndex).format; + MediaFormat trackFormat = buildTrackFormat(adaptationSet.type, representationFormat, + manifest.dynamic ? C.UNKNOWN_TIME_US : manifest.duration * 1000); + tracks.add(new ExposedTrack(trackFormat, adaptationSetIndex, representationFormat)); + } + + // Private methods. + + // Visible for testing. + /* package */ TimeRange getAvailableRange() { + return availableRange; + } + + private static MediaPresentationDescription buildManifest(long durationMs, + int adaptationSetType, List representations) { + AdaptationSet adaptationSet = new AdaptationSet(0, adaptationSetType, representations); + Period period = new Period(null, 0, Collections.singletonList(adaptationSet)); + return new MediaPresentationDescription(-1, durationMs, -1, false, -1, -1, null, null, + Collections.singletonList(period)); + } + + private static MediaFormat buildTrackFormat(int adaptationSetType, Format format, + long durationUs) { + switch (adaptationSetType) { + case AdaptationSet.TYPE_VIDEO: + return MediaFormat.createVideoFormat(getMediaMimeType(format), format.bitrate, + MediaFormat.NO_VALUE, durationUs, format.width, format.height, 0, null); + case AdaptationSet.TYPE_AUDIO: + return MediaFormat.createAudioFormat(getMediaMimeType(format), format.bitrate, + MediaFormat.NO_VALUE, durationUs, format.audioChannels, format.audioSamplingRate, null); + case AdaptationSet.TYPE_TEXT: + return MediaFormat.createTextFormat(getMediaMimeType(format), format.bitrate, + format.language, durationUs); + default: + throw new IllegalStateException("Invalid type: " + adaptationSetType); + } + } + + private static String getMediaMimeType(Format format) { + String mimeType = format.mimeType; + if (MimeTypes.APPLICATION_MP4.equals(format.mimeType) && "stpp".equals(format.codecs)) { + return MimeTypes.APPLICATION_TTML; + } + // TODO: Use codecs to determine media mime type for other formats too. + return mimeType; + } + private static boolean mimeTypeIsWebm(String mimeType) { return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); } @@ -590,12 +648,13 @@ public class DashChunkSource implements ChunkSource { boolean isMediaFormatFinal = (mediaFormat != null); return new ContainerMediaChunk(dataSource, dataSpec, trigger, representation.format, startTimeUs, endTimeUs, segmentNum, isLastSegment, sampleOffsetUs, - representationHolder.extractorWrapper, mediaFormat, maxWidth, maxHeight, drmInitData, - isMediaFormatFinal, periodHolder.localIndex); + representationHolder.extractorWrapper, mediaFormat, enabledTrack.adaptiveMaxWidth, + enabledTrack.adaptiveMaxHeight, periodHolder.drmInitData, isMediaFormatFinal, + periodHolder.localIndex); } } - private long getNowUs() { + private long getNowUnixTimeUs() { if (elapsedRealtimeOffsetUs != 0) { return (systemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs; } else { @@ -603,46 +662,6 @@ public class DashChunkSource implements ChunkSource { } } - private static String getMediaMimeType(Representation representation) { - String mimeType = representation.format.mimeType; - if (MimeTypes.APPLICATION_MP4.equals(representation.format.mimeType) - && "stpp".equals(representation.format.codecs)) { - return MimeTypes.APPLICATION_TTML; - } - // TODO: Use codecs to determine media mime type for other formats too. - return mimeType; - } - - private static DrmInitData getDrmInitData(MediaPresentationDescription manifest, - int adaptationSetIndex) { - AdaptationSet adaptationSet = manifest.getPeriod(0).adaptationSets.get(adaptationSetIndex); - String drmInitMimeType = mimeTypeIsWebm(adaptationSet.representations.get(0).format.mimeType) - ? MimeTypes.VIDEO_WEBM : MimeTypes.VIDEO_MP4; - if (adaptationSet.contentProtections.isEmpty()) { - return null; - } else { - DrmInitData.Mapped drmInitData = null; - for (int i = 0; i < adaptationSet.contentProtections.size(); i++) { - ContentProtection contentProtection = adaptationSet.contentProtections.get(i); - if (contentProtection.uuid != null && contentProtection.data != null) { - if (drmInitData == null) { - drmInitData = new DrmInitData.Mapped(drmInitMimeType); - } - drmInitData.put(contentProtection.uuid, contentProtection.data); - } - } - return drmInitData; - } - } - - private static MediaPresentationDescription buildManifest(long durationMs, - List representations) { - AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); - Period period = new Period(null, 0, Collections.singletonList(adaptationSet)); - return new MediaPresentationDescription(-1, durationMs, -1, false, -1, -1, null, null, - Collections.singletonList(period)); - } - private PeriodHolder findPeriodHolder(long positionUs) { // if positionUs is before the first period, return the first period if (positionUs < periodHolders.valueAt(0).getAvailableStartTimeUs()) { @@ -670,10 +689,15 @@ public class DashChunkSource implements ChunkSource { periodHolders.remove(periodHolder.localIndex); } - // Update existing periods. + // Update existing periods. Only the first and last periods can change. try { - for (int i = 0; i < periodHolders.size(); i++) { - periodHolders.valueAt(i).updatePeriod(manifest, i); + int periodHolderCount = periodHolders.size(); + if (periodHolderCount > 0) { + periodHolders.valueAt(0).updatePeriod(manifest, 0, enabledTrack); + if (periodHolderCount > 1) { + int lastIndex = periodHolderCount - 1; + periodHolders.valueAt(lastIndex).updatePeriod(manifest, lastIndex, enabledTrack); + } } } catch (BehindLiveWindowException e) { fatalError = e; @@ -682,14 +706,13 @@ public class DashChunkSource implements ChunkSource { // Add new periods. for (int i = periodHolders.size(); i < manifest.getPeriodCount(); i++) { - PeriodHolder periodHolder = new PeriodHolder(periodHolderNextIndex, manifest, i, - adaptationSetIndex, representationIndices); - periodHolders.put(periodHolderNextIndex, periodHolder); - periodHolderNextIndex++; + PeriodHolder holder = new PeriodHolder(nextPeriodHolderIndex, manifest, i, enabledTrack); + periodHolders.put(nextPeriodHolderIndex, holder); + nextPeriodHolderIndex++; } // Update the available range. - TimeRange newAvailableRange = getAvailableRange(getNowUs()); + TimeRange newAvailableRange = getAvailableRange(getNowUnixTimeUs()); if (availableRange == null || !availableRange.equals(newAvailableRange)) { availableRange = newAvailableRange; notifyAvailableRangeChanged(availableRange); @@ -698,7 +721,7 @@ public class DashChunkSource implements ChunkSource { currentManifest = manifest; } - private TimeRange getAvailableRange(long nowUs) { + private TimeRange getAvailableRange(long nowUnixTimeUs) { PeriodHolder firstPeriod = periodHolders.valueAt(0); PeriodHolder lastPeriod = periodHolders.valueAt(periodHolders.size() - 1); @@ -711,7 +734,7 @@ public class DashChunkSource implements ChunkSource { long maxEndPositionUs = lastPeriod.isIndexUnbounded() ? Long.MAX_VALUE : lastPeriod.getAvailableEndTimeUs(); long elapsedRealtimeAtZeroUs = (systemClock.elapsedRealtime() * 1000) - - (nowUs - (currentManifest.availabilityStartTime * 1000)); + - (nowUnixTimeUs - (currentManifest.availabilityStartTime * 1000)); long timeShiftBufferDepthUs = currentManifest.timeShiftBufferDepth == -1 ? -1 : currentManifest.timeShiftBufferDepth * 1000; return new DynamicTimeRange(minStartPositionUs, maxEndPositionUs, elapsedRealtimeAtZeroUs, @@ -729,6 +752,47 @@ public class DashChunkSource implements ChunkSource { } } + // Private classes. + + private static final class ExposedTrack { + + public final MediaFormat trackFormat; + + private final int adaptationSetIndex; + + // Non-adaptive track variables. + private final Format fixedFormat; + + // Adaptive track variables. + private final Format[] adaptiveFormats; + private final int adaptiveMaxWidth; + private final int adaptiveMaxHeight; + + public ExposedTrack(MediaFormat trackFormat, int adaptationSetIndex, Format fixedFormat) { + this.trackFormat = trackFormat; + this.adaptationSetIndex = adaptationSetIndex; + this.fixedFormat = fixedFormat; + this.adaptiveFormats = null; + this.adaptiveMaxWidth = -1; + this.adaptiveMaxHeight = -1; + } + + public ExposedTrack(MediaFormat trackFormat, int adaptationSetIndex, Format[] adaptiveFormats, + int maxWidth, int maxHeight) { + this.trackFormat = trackFormat; + this.adaptationSetIndex = adaptationSetIndex; + this.adaptiveFormats = adaptiveFormats; + this.adaptiveMaxWidth = maxWidth; + this.adaptiveMaxHeight = maxHeight; + this.fixedFormat = null; + } + + public boolean isAdaptive() { + return adaptiveFormats != null; + } + + } + private static final class RepresentationHolder { public final ChunkExtractorWrapper extractorWrapper; @@ -738,16 +802,18 @@ public class DashChunkSource implements ChunkSource { public MediaFormat mediaFormat; private final long periodStartTimeUs; - private long periodDurationUs; + private long periodDurationUs; private int segmentNumShift; public RepresentationHolder(long periodStartTimeUs, long periodDurationUs, - Representation representation, ChunkExtractorWrapper extractorWrapper) { + Representation representation) { this.periodStartTimeUs = periodStartTimeUs; this.periodDurationUs = periodDurationUs; this.representation = representation; - this.extractorWrapper = extractorWrapper; + extractorWrapper = MimeTypes.TEXT_VTT.equals(representation.format.mimeType) ? null + : new ChunkExtractorWrapper(mimeTypeIsWebm(representation.format.mimeType) + ? new WebmExtractor() : new FragmentedMp4Extractor()); segmentIndex = representation.getIndex(); } @@ -775,15 +841,15 @@ public class DashChunkSource implements ChunkSource { int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); if (oldIndexEndTimeUs == newIndexStartTimeUs) { - // The new manifest continues where the old one ended, with no overlap. + // The new index continues where the old one ended, with no overlap. segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 1 - newIndexFirstSegmentNum; } else if (oldIndexEndTimeUs < newIndexStartTimeUs) { - // There's a gap between the old manifest and the new one which means we've slipped - // behind the live window and can't proceed. + // There's a gap between the old index and the new one which means we've slipped behind the + // live window and can't proceed. throw new BehindLiveWindowException(); } else { - // The new manifest overlaps with the old one. + // The new index overlaps with the old one. segmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs, periodDurationUs) - newIndexFirstSegmentNum; } @@ -823,77 +889,65 @@ public class DashChunkSource implements ChunkSource { public final int localIndex; public final long startTimeUs; - public final long durationUs; - public final String mimeType; - public final Format[] formats; public final HashMap representationHolders; - private final int adaptationSetIndex; private final int[] representationIndices; - private final int maxWidth; - private final int maxHeight; + + private DrmInitData drmInitData; private boolean indexIsUnbounded; private boolean indexIsExplicit; private long availableStartTimeUs; private long availableEndTimeUs; - public PeriodHolder(int localIndex, MediaPresentationDescription manifest, - int manifestIndex, int adaptationSetIndex, int[] representationIndices) { + public PeriodHolder(int localIndex, MediaPresentationDescription manifest, int manifestIndex, + ExposedTrack selectedTrack) { this.localIndex = localIndex; - this.adaptationSetIndex = adaptationSetIndex; - this.representationIndices = representationIndices; Period period = manifest.getPeriod(manifestIndex); + long periodDurationUs = getPeriodDurationUs(manifest, manifestIndex); + AdaptationSet adaptationSet = period.adaptationSets.get(selectedTrack.adaptationSetIndex); + List representations = adaptationSet.representations; + startTimeUs = period.startMs * 1000; - durationUs = getPeriodDurationUs(manifest, manifestIndex); + drmInitData = getDrmInitData(adaptationSet); - List periodRepresentations = - period.adaptationSets.get(adaptationSetIndex).representations; - int representationCount = representationIndices != null ? representationIndices.length - : periodRepresentations.size(); - formats = new Format[representationCount]; - representationHolders = new HashMap<>(representationCount); - - int maxWidth = 0; - int maxHeight = 0; - String mimeType = ""; - for (int i = 0; i < representationCount; i++) { - int representationIndex = representationIndices != null ? representationIndices[i] : i; - Representation representation = periodRepresentations.get(representationIndex); - formats[i] = representation.format; - mimeType = getMediaMimeType(representation); - maxWidth = Math.max(formats[i].width, maxWidth); - maxHeight = Math.max(formats[i].height, maxHeight); - Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor() - : new FragmentedMp4Extractor(); - RepresentationHolder representationHolder = new RepresentationHolder(startTimeUs, - durationUs, representation, new ChunkExtractorWrapper(extractor)); - representationHolders.put(formats[i].id, representationHolder); + if (!selectedTrack.isAdaptive()) { + representationIndices = new int[] { + getRepresentationIndex(representations, selectedTrack.fixedFormat.id)}; + } else { + representationIndices = new int[selectedTrack.adaptiveFormats.length]; + for (int j = 0; j < selectedTrack.adaptiveFormats.length; j++) { + representationIndices[j] = getRepresentationIndex( + representations, selectedTrack.adaptiveFormats[j].id); + } } - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - this.mimeType = mimeType; - Arrays.sort(formats, new DecreasingBandwidthComparator()); - updateRepresentationIndependentProperties(); + representationHolders = new HashMap<>(); + for (int i = 0; i < representationIndices.length; i++) { + Representation representation = representations.get(representationIndices[i]); + RepresentationHolder representationHolder = new RepresentationHolder(startTimeUs, + periodDurationUs, representation); + representationHolders.put(representation.format.id, representationHolder); + } + updateRepresentationIndependentProperties(periodDurationUs, + representations.get(representationIndices[0])); } - public void updatePeriod(MediaPresentationDescription manifest, int manifestIndex) - throws BehindLiveWindowException { + public void updatePeriod(MediaPresentationDescription manifest, int manifestIndex, + ExposedTrack selectedTrack) throws BehindLiveWindowException { Period period = manifest.getPeriod(manifestIndex); - long durationUs = getPeriodDurationUs(manifest, manifestIndex); + long periodDurationUs = getPeriodDurationUs(manifest, manifestIndex); + List representations = period.adaptationSets + .get(selectedTrack.adaptationSetIndex).representations; - List representations = - period.adaptationSets.get(adaptationSetIndex).representations; - int representationCount = formats.length; - for (int i = 0; i < representationCount; i++) { - int representationIndex = representationIndices != null ? representationIndices[i] : i; - Representation representation = representations.get(representationIndex); - representationHolders.get(representation.format.id).updateRepresentation(durationUs, + for (int j = 0; j < representationIndices.length; j++) { + Representation representation = representations.get(representationIndices[j]); + representationHolders.get(representation.format.id).updateRepresentation(periodDurationUs, representation); } - updateRepresentationIndependentProperties(); + updateRepresentationIndependentProperties(periodDurationUs, + representations.get(representationIndices[0])); } public long getAvailableStartTimeUs() { @@ -915,25 +969,57 @@ public class DashChunkSource implements ChunkSource { return indexIsExplicit; } - private void updateRepresentationIndependentProperties() { - // Arbitrarily use the first representation to derive representation independent properties. - Representation representation = representationHolders.get(formats[0].id).representation; - DashSegmentIndex segmentIndex = representation.getIndex(); + // Private methods. + + private void updateRepresentationIndependentProperties(long periodDurationUs, + Representation arbitaryRepresentation) { + DashSegmentIndex segmentIndex = arbitaryRepresentation.getIndex(); if (segmentIndex != null) { int firstSegmentNum = segmentIndex.getFirstSegmentNum(); - int lastSegmentNum = segmentIndex.getLastSegmentNum(durationUs); + int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs); indexIsUnbounded = lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; indexIsExplicit = segmentIndex.isExplicit(); availableStartTimeUs = startTimeUs + segmentIndex.getTimeUs(firstSegmentNum); if (!indexIsUnbounded) { availableEndTimeUs = startTimeUs + segmentIndex.getTimeUs(lastSegmentNum) - + segmentIndex.getDurationUs(lastSegmentNum, durationUs); + + segmentIndex.getDurationUs(lastSegmentNum, periodDurationUs); } } else { indexIsUnbounded = false; indexIsExplicit = true; availableStartTimeUs = startTimeUs; - availableEndTimeUs = startTimeUs + durationUs; + availableEndTimeUs = startTimeUs + periodDurationUs; + } + } + + private static int getRepresentationIndex(List representations, + String formatId) { + for (int i = 0; i < representations.size(); i++) { + Representation representation = representations.get(i); + if (formatId.equals(representation.format.id)) { + return i; + } + } + throw new IllegalStateException("Missing format id: " + formatId); + } + + private static DrmInitData getDrmInitData(AdaptationSet adaptationSet) { + String drmInitMimeType = mimeTypeIsWebm(adaptationSet.representations.get(0).format.mimeType) + ? MimeTypes.VIDEO_WEBM : MimeTypes.VIDEO_MP4; + if (adaptationSet.contentProtections.isEmpty()) { + return null; + } else { + DrmInitData.Mapped drmInitData = null; + for (int i = 0; i < adaptationSet.contentProtections.size(); i++) { + ContentProtection contentProtection = adaptationSet.contentProtections.get(i); + if (contentProtection.uuid != null && contentProtection.data != null) { + if (drmInitData == null) { + drmInitData = new DrmInitData.Mapped(drmInitMimeType); + } + drmInitData.put(contentProtection.uuid, contentProtection.data); + } + } + return drmInitData; } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashTrackSelector.java b/library/src/main/java/com/google/android/exoplayer/dash/DashTrackSelector.java new file mode 100644 index 0000000000..3b7fb6fec1 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashTrackSelector.java @@ -0,0 +1,71 @@ +/* + * 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.dash; + +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; +import com.google.android.exoplayer.dash.mpd.Period; + +import java.io.IOException; + +/** + * Specifies a track selection from a {@link Period} of a media presentation description. + */ +public interface DashTrackSelector { + + /** + * Defines a selector output. + */ + interface Output { + + /** + * Outputs an adaptive track, covering the specified representations in the specified + * adaptation set. + * + * @param manifest The media presentation description being processed. + * @param periodIndex The index of the period being processed. + * @param adaptationSetIndex The index of the adaptation set within which the representations + * are located. + * @param representationIndices The indices of the track within the element. + */ + void adaptiveTrack(MediaPresentationDescription manifest, int periodIndex, + int adaptationSetIndex, int[] representationIndices); + + /** + * Outputs an fixed track corresponding to the specified representation in the specified + * adaptation set. + * + * @param manifest The media presentation description being processed. + * @param periodIndex The index of the period being processed. + * @param adaptationSetIndex The index of the adaptation set within which the track is located. + * @param representationIndex The index of the representation within the adaptation set. + */ + void fixedTrack(MediaPresentationDescription manifest, int periodIndex, int adaptationSetIndex, + int representationIndex); + + } + + /** + * Outputs a track selection for a given period. + * + * @param manifest the media presentation description to process. + * @param periodIndex The index of the period to process. + * @param output The output to receive tracks. + * @throws IOException If an error occurs processing the period. + */ + void selectTracks(MediaPresentationDescription manifest, int periodIndex, Output output) + throws IOException; + +} diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DefaultDashTrackSelector.java b/library/src/main/java/com/google/android/exoplayer/dash/DefaultDashTrackSelector.java new file mode 100644 index 0000000000..a513483312 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/dash/DefaultDashTrackSelector.java @@ -0,0 +1,100 @@ +/* + * 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.dash; + +import com.google.android.exoplayer.chunk.VideoFormatSelectorUtil; +import com.google.android.exoplayer.dash.mpd.AdaptationSet; +import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; +import com.google.android.exoplayer.dash.mpd.Period; +import com.google.android.exoplayer.util.Util; + +import android.content.Context; + +import java.io.IOException; + +/** + * A default {@link DashTrackSelector} implementation. + */ +// TODO: Add more configuration options (e.g. ability to disable adaptive track output). +public final class DefaultDashTrackSelector implements DashTrackSelector { + + private final int adaptationSetType; + + private final Context context; + private final boolean filterVideoRepresentations; + private final boolean filterProtectedHdContent; + + /** + * @param context A context. May be null if {@code filterVideoRepresentations == false}. + * @param filterVideoRepresentations Whether video representations should be filtered according to + * the capabilities of the device. It is strongly recommended to set this to {@code true}, + * unless the application has already verified that all representations are playable. + * @param filterProtectedHdContent Whether video representations that are both drm protected and + * high definition should be filtered when tracks are built. If + * {@code filterVideoRepresentations == false} then this parameter is ignored. + */ + public static DefaultDashTrackSelector newVideoInstance(Context context, + boolean filterVideoRepresentations, boolean filterProtectedHdContent) { + return new DefaultDashTrackSelector(AdaptationSet.TYPE_VIDEO, context, + filterVideoRepresentations, filterProtectedHdContent); + } + + public static DefaultDashTrackSelector newAudioInstance() { + return new DefaultDashTrackSelector(AdaptationSet.TYPE_AUDIO, null, false, false); + } + + public static DefaultDashTrackSelector newTextInstance() { + return new DefaultDashTrackSelector(AdaptationSet.TYPE_TEXT, null, false, false); + } + + private DefaultDashTrackSelector(int adaptationSetType, Context context, + boolean filterVideoRepresentations, boolean filterProtectedHdContent) { + this.adaptationSetType = adaptationSetType; + this.context = context; + this.filterVideoRepresentations = filterVideoRepresentations; + this.filterProtectedHdContent = filterProtectedHdContent; + } + + @Override + public void selectTracks(MediaPresentationDescription manifest, int periodIndex, Output output) + throws IOException { + Period period = manifest.getPeriod(periodIndex); + for (int i = 0; i < period.adaptationSets.size(); i++) { + AdaptationSet adaptationSet = period.adaptationSets.get(i); + if (adaptationSet.type == adaptationSetType) { + if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { + int[] representations; + if (filterVideoRepresentations) { + representations = VideoFormatSelectorUtil.selectVideoFormatsForDefaultDisplay( + context, adaptationSet.representations, null, + filterProtectedHdContent && adaptationSet.hasContentProtection()); + } else { + representations = Util.firstIntegersArray(adaptationSet.representations.size()); + } + output.adaptiveTrack(manifest, periodIndex, i, representations); + for (int j = 0; j < representations.length; j++) { + output.fixedTrack(manifest, periodIndex, i, representations[j]); + } + } else { + for (int j = 0; j < adaptationSet.representations.size(); j++) { + output.fixedTrack(manifest, periodIndex, i, j); + } + } + } + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 9aec0b0c4d..cfbd282c64 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -311,6 +311,20 @@ public final class Util { return stayInBounds ? Math.min(list.size() - 1, index) : index; } + /** + * Creates an integer array containing the integers from 0 to {@code length - 1}. + * + * @param length The length of the array. + * @return The array. + */ + public static int[] firstIntegersArray(int length) { + int[] firstIntegers = new int[length]; + for (int i = 0; i < length; i++) { + firstIntegers[i] = i; + } + return firstIntegers; + } + /** * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. *