From 709fc7735b9038ab8ea8399b4e2308587d98bd80 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 19 May 2015 15:16:29 +0100 Subject: [PATCH] First steps toward implementing bounded live seek. --- RELEASENOTES.md | 8 +- .../android/exoplayer/demo/EventLogger.java | 11 +- .../demo/player/DashRendererBuilder.java | 10 +- .../demo/player/DebugTrackRenderer.java | 24 +- .../exoplayer/demo/player/DemoPlayer.java | 12 +- .../demo/player/ExtractorRendererBuilder.java | 7 +- .../demo/player/HlsRendererBuilder.java | 2 +- .../SmoothStreamingRendererBuilder.java | 2 +- .../google/android/exoplayer/TimeRange.java | 102 ++++ .../exoplayer/dash/DashChunkSource.java | 191 +++++--- .../exoplayer/extractor/ts/H264Reader.java | 160 ++++++- .../exoplayer/extractor/ts/TsExtractor.java | 24 +- .../upstream/MulticastDataSource.java | 140 ++++++ .../exoplayer/util/ParsableBitArray.java | 95 +++- .../google/android/exoplayer/util/Util.java | 16 + .../android/exoplayer/TimeRangeTest.java | 36 ++ .../exoplayer/dash/DashChunkSourceTest.java | 449 +++++++++++++----- 17 files changed, 1042 insertions(+), 247 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/TimeRange.java create mode 100644 library/src/main/java/com/google/android/exoplayer/upstream/MulticastDataSource.java create mode 100644 library/src/test/java/com/google/android/exoplayer/TimeRangeTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1dfc9ac394..14b30937ed 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,12 @@ # Release notes # -### r1.3.2 ### +### Current dev branch (from r1.3.2) ### + +* Add option to TsExtractor to allow non-IDR keyframes. +* Added MulticastDataSource for connecting to multicast streams. +* (WorkInProgress) - First steps to supporting seeking in DASH DVR window. + +### r1.3.2 (from r1.3.1) ### * DataSource improvements: `DefaultUriDataSource` now handles http://, https://, file://, asset:// and content:// URIs automatically. It also handles file:///android_asset/* URIs, and file paths diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java index ffdf43a4d2..d0201fbc61 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer.demo; import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.TimeRange; import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.demo.player.DemoPlayer; @@ -46,6 +47,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener private long sessionStartTimeMs; private long[] loadStartTimeMs; + private long[] seekRangeValuesUs; public EventLogger() { loadStartTimeMs = new long[DemoPlayer.RENDERER_COUNT]; @@ -163,7 +165,14 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener @Override public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { - Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + "]"); + Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); + } + + @Override + public void onSeekRangeChanged(TimeRange seekRange) { + seekRangeValuesUs = seekRange.getCurrentBoundsUs(seekRangeValuesUs); + Log.d(TAG, "seekRange [ " + seekRange.type + ", " + seekRangeValuesUs[0] + ", " + + seekRangeValuesUs[1] + "]"); } private void printInternalError(String type, Exception e) { 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 bde74928a0..150496f2a7 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 @@ -235,14 +235,15 @@ public class DashRendererBuilder implements RendererBuilder, DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex, videoRepresentationIndices, videoDataSource, - new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset); + new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset, + mainHandler, player); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_VIDEO); videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50); debugRenderer = debugTextView != null - ? new DebugTrackRenderer(debugTextView, player, videoRenderer) : null; + ? new DebugTrackRenderer(debugTextView, player, videoRenderer, bandwidthMeter) : null; } // Build the audio chunk sources. @@ -259,7 +260,7 @@ public class DashRendererBuilder implements RendererBuilder, format.audioSamplingRate + "Hz)"); audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS, - elapsedRealtimeOffset)); + elapsedRealtimeOffset, mainHandler, player)); codecs.add(format.codecs); } @@ -316,7 +317,8 @@ public class DashRendererBuilder implements RendererBuilder, 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)); + textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset, + mainHandler, player)); } } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java index 2a846f46b2..7f7f4aefd3 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaCodecTrackRenderer; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.Format; +import com.google.android.exoplayer.upstream.BandwidthMeter; import android.widget.TextView; @@ -31,15 +32,22 @@ import android.widget.TextView; private final TextView textView; private final DemoPlayer player; private final MediaCodecTrackRenderer renderer; + private final BandwidthMeter bandwidthMeter; private volatile boolean pendingFailure; private volatile long currentPositionUs; public DebugTrackRenderer(TextView textView, DemoPlayer player, MediaCodecTrackRenderer renderer) { + this(textView, player, renderer, null); + } + + public DebugTrackRenderer(TextView textView, DemoPlayer player, MediaCodecTrackRenderer renderer, + BandwidthMeter bandwidthMeter) { this.textView = textView; this.player = player; this.renderer = renderer; + this.bandwidthMeter = bandwidthMeter; } public void injectFailure() { @@ -77,7 +85,12 @@ import android.widget.TextView; } private String getRenderString() { - return getQualityString() + " " + renderer.codecCounters.getDebugString(); + return getTimeString() + " " + getQualityString() + " " + getBandwidthString() + " " + + renderer.codecCounters.getDebugString(); + } + + private String getTimeString() { + return "ms(" + (currentPositionUs / 1000) + ")"; } private String getQualityString() { @@ -86,6 +99,15 @@ import android.widget.TextView; : "id:" + format.id + " br:" + format.bitrate + " h:" + format.height; } + private String getBandwidthString() { + if (bandwidthMeter == null + || bandwidthMeter.getBitrateEstimate() == BandwidthMeter.NO_ESTIMATE) { + return "bw:?"; + } else { + return "bw:" + (bandwidthMeter.getBitrateEstimate() / 1000); + } + } + @Override protected long getCurrentPositionUs() { return currentPositionUs; 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 dd83694659..2d34222ced 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 @@ -21,11 +21,13 @@ import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.TimeRange; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.MultiTrackChunkSource; +import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; @@ -50,7 +52,7 @@ import java.util.concurrent.CopyOnWriteArrayList; public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, - StreamingDrmSessionManager.EventListener, TextRenderer { + StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer { /** * Builds renderers for the player. @@ -132,6 +134,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs); void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, long initializationDurationMs); + void onSeekRangeChanged(TimeRange seekRange); } /** @@ -510,6 +513,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi processText(text); } + @Override + public void onSeekRangeChanged(TimeRange seekRange) { + if (infoListener != null) { + infoListener.onSeekRangeChanged(seekRange); + } + } + /* package */ MetadataTrackRenderer.MetadataRenderer> getId3MetadataRenderer() { return new MetadataTrackRenderer.MetadataRenderer>() { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java index 3437678e04..d2c0f8bee9 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallba import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.ExtractorSampleSource; import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.DefaultUriDataSource; import android.content.Context; @@ -55,7 +56,9 @@ public class ExtractorRendererBuilder implements RendererBuilder { @Override public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { // Build the video and audio renderers. - DataSource dataSource = new DefaultUriDataSource(context, userAgent); + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(player.getMainHandler(), + null); + DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, extractor, 2, BUFFER_SIZE); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, @@ -66,7 +69,7 @@ public class ExtractorRendererBuilder implements RendererBuilder { // Build the debug renderer. TrackRenderer debugRenderer = debugTextView != null - ? new DebugTrackRenderer(debugTextView, player, videoRenderer) : null; + ? new DebugTrackRenderer(debugTextView, player, videoRenderer, bandwidthMeter) : null; // Invoke the callback. TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java index 8eb762e218..a2671554a3 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java @@ -121,7 +121,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback manifestFetcher, int adaptationSetIndex, int[] representationIndices, DataSource dataSource, - FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs) { + FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs, + Handler eventHandler, EventListener eventListener) { this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices, dataSource, formatEvaluator, new SystemClock(), liveEdgeLatencyMs * 1000, - elapsedRealtimeOffsetMs * 1000); + elapsedRealtimeOffsetMs * 1000, eventHandler, eventListener); } /* package */ DashChunkSource(ManifestFetcher manifestFetcher, MediaPresentationDescription initialManifest, int adaptationSetIndex, int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator, - Clock systemClock, long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs) { + Clock systemClock, long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs, + Handler eventHandler, EventListener eventListener) { this.manifestFetcher = manifestFetcher; this.currentManifest = initialManifest; this.adaptationSetIndex = adaptationSetIndex; @@ -189,8 +218,11 @@ public class DashChunkSource implements ChunkSource { this.systemClock = systemClock; this.liveEdgeLatencyUs = liveEdgeLatencyUs; this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetUs; + this.eventHandler = eventHandler; + this.eventListener = eventListener; this.evaluation = new Evaluation(); this.headerBuilder = new StringBuilder(); + this.seekRangeValues = new long[2]; drmInitData = getDrmInitData(currentManifest, adaptationSetIndex); Representation[] representations = getFilteredRepresentations(currentManifest, @@ -229,12 +261,27 @@ public class DashChunkSource implements ChunkSource { return trackInfo; } + // VisibleForTesting + /* package */ TimeRange getSeekRange() { + return seekRange; + } + @Override public void enable() { evaluator.enable(); if (manifestFetcher != null) { manifestFetcher.enable(); } + DashSegmentIndex segmentIndex = + representationHolders.get(formats[0].id).representation.getIndex(); + if (segmentIndex == null) { + seekRange = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, currentManifest.duration * 1000); + notifySeekRangeChanged(seekRange); + } else { + long nowUs = getNowUs(); + updateAvailableSegmentBounds(segmentIndex, nowUs); + updateSeekRange(segmentIndex, nowUs); + } } @Override @@ -243,6 +290,7 @@ public class DashChunkSource implements ChunkSource { if (manifestFetcher != null) { manifestFetcher.disable(); } + seekRange = null; } @Override @@ -268,6 +316,10 @@ public class DashChunkSource implements ChunkSource { } currentManifest = newManifest; finishedCurrentManifest = false; + + long nowUs = getNowUs(); + updateAvailableSegmentBounds(newRepresentations[0].getIndex(), nowUs); + updateSeekRange(newRepresentations[0].getIndex(), nowUs); } // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where @@ -334,36 +386,21 @@ public class DashChunkSource implements ChunkSource { return; } - long nowUs; - if (elapsedRealtimeOffsetUs != 0) { - nowUs = (systemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs; - } else { - nowUs = System.currentTimeMillis() * 1000; - } - - int firstAvailableSegmentNum = segmentIndex.getFirstSegmentNum(); - int lastAvailableSegmentNum = segmentIndex.getLastSegmentNum(); - boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; - if (indexUnbounded) { - // The index is itself unbounded. We need to use the current time to calculate the range of - // available segments. - long liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; - if (currentManifest.timeShiftBufferDepth != -1) { - long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000; - firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, - segmentIndex.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs)); - } - // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the - // index of the last completed segment. - lastAvailableSegmentNum = segmentIndex.getSegmentNum(liveEdgeTimestampUs) - 1; - } - int segmentNum; + boolean indexUnbounded = segmentIndex.getLastSegmentNum() == DashSegmentIndex.INDEX_UNBOUNDED; if (queue.isEmpty()) { if (currentManifest.dynamic) { - seekPositionUs = getLiveSeekPosition(nowUs, indexUnbounded, segmentIndex.isExplicit()); + seekRangeValues = seekRange.getCurrentBoundsUs(seekRangeValues); + seekPositionUs = Math.max(seekPositionUs, seekRangeValues[0]); + seekPositionUs = Math.min(seekPositionUs, seekRangeValues[1]); } segmentNum = segmentIndex.getSegmentNum(seekPositionUs); + + // if the index is unbounded then the result of getSegmentNum isn't clamped to ensure that + // it doesn't exceed the last available segment. Clamp it here. + if (indexUnbounded) { + segmentNum = Math.min(segmentNum, lastAvailableSegmentNum); + } } else { MediaChunk previous = queue.get(out.queueSize - 1); segmentNum = previous.isLastChunk ? -1 @@ -432,6 +469,59 @@ public class DashChunkSource implements ChunkSource { // Do nothing. } + private void updateAvailableSegmentBounds(DashSegmentIndex segmentIndex, long nowUs) { + int indexFirstAvailableSegmentNum = segmentIndex.getFirstSegmentNum(); + int indexLastAvailableSegmentNum = segmentIndex.getLastSegmentNum(); + if (indexLastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) { + // The index is itself unbounded. We need to use the current time to calculate the range of + // available segments. + long liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; + if (currentManifest.timeShiftBufferDepth != -1) { + long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000; + indexFirstAvailableSegmentNum = Math.max(indexFirstAvailableSegmentNum, + segmentIndex.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs)); + } + // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the + // index of the last completed segment. + indexLastAvailableSegmentNum = segmentIndex.getSegmentNum(liveEdgeTimestampUs) - 1; + } + firstAvailableSegmentNum = indexFirstAvailableSegmentNum; + lastAvailableSegmentNum = indexLastAvailableSegmentNum; + } + + private void updateSeekRange(DashSegmentIndex segmentIndex, long nowUs) { + long earliestSeekPosition = segmentIndex.getTimeUs(firstAvailableSegmentNum); + long latestSeekPosition = segmentIndex.getTimeUs(lastAvailableSegmentNum) + + segmentIndex.getDurationUs(lastAvailableSegmentNum); + if (currentManifest.dynamic) { + long liveEdgeTimestampUs; + if (segmentIndex.getLastSegmentNum() == DashSegmentIndex.INDEX_UNBOUNDED) { + liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; + } else { + liveEdgeTimestampUs = segmentIndex.getTimeUs(segmentIndex.getLastSegmentNum()) + + segmentIndex.getDurationUs(segmentIndex.getLastSegmentNum()); + if (!segmentIndex.isExplicit()) { + // Some segments defined by the index may not be available yet. Bound the calculated live + // edge based on the elapsed time since the manifest became available. + liveEdgeTimestampUs = Math.min(liveEdgeTimestampUs, + nowUs - currentManifest.availabilityStartTime * 1000); + } + } + + // it's possible that the live edge latency actually puts our latest position before + // the earliest position in the case of a DVR-like stream that's just starting up, so + // in that case just return the earliest position instead + latestSeekPosition = Math.max(earliestSeekPosition, liveEdgeTimestampUs - liveEdgeLatencyUs); + } + + TimeRange newSeekRange = new TimeRange(TimeRange.TYPE_SNAPSHOT, earliestSeekPosition, + latestSeekPosition); + if (seekRange == null || !seekRange.equals(newSeekRange)) { + seekRange = newSeekRange; + notifySeekRangeChanged(seekRange); + } + } + private static boolean mimeTypeIsWebm(String mimeType) { return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); } @@ -491,36 +581,12 @@ public class DashChunkSource implements ChunkSource { } } - /** - * For live playbacks, determines the seek position that snaps playback to be - * {@link #liveEdgeLatencyUs} behind the live edge of the current manifest - * - * @param nowUs An estimate of the current server time, in microseconds. - * @param indexUnbounded True if the segment index for this source is unbounded. False otherwise. - * @param indexExplicit True if the segment index is explicit. False otherwise. - * @return The seek position in microseconds. - */ - private long getLiveSeekPosition(long nowUs, boolean indexUnbounded, boolean indexExplicit) { - long liveEdgeTimestampUs; - if (indexUnbounded) { - liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; + private long getNowUs() { + if (elapsedRealtimeOffsetUs != 0) { + return (systemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs; } else { - liveEdgeTimestampUs = Long.MIN_VALUE; - for (RepresentationHolder representationHolder : representationHolders.values()) { - DashSegmentIndex segmentIndex = representationHolder.segmentIndex; - int lastSegmentNum = segmentIndex.getLastSegmentNum(); - long indexLiveEdgeTimestampUs = segmentIndex.getTimeUs(lastSegmentNum) - + segmentIndex.getDurationUs(lastSegmentNum); - liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, indexLiveEdgeTimestampUs); - } - if (!indexExplicit) { - // Some segments defined by the index may not be available yet. Bound the calculated live - // edge based on the elapsed time since the manifest became available. - liveEdgeTimestampUs = Math.min(liveEdgeTimestampUs, - nowUs - currentManifest.availabilityStartTime * 1000); - } + return System.currentTimeMillis() * 1000; } - return liveEdgeTimestampUs - liveEdgeLatencyUs; } private static Representation[] getFilteredRepresentations(MediaPresentationDescription manifest, @@ -571,6 +637,17 @@ public class DashChunkSource implements ChunkSource { Collections.singletonList(period)); } + private void notifySeekRangeChanged(final TimeRange seekRange) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onSeekRangeChanged(seekRange); + } + }); + } + } + private static class RepresentationHolder { public final Representation representation; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java index c8b023e8dc..2b539d01e7 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java @@ -37,11 +37,15 @@ import java.util.List; private static final String TAG = "H264Reader"; - private static final int NAL_UNIT_TYPE_IDR = 5; - private static final int NAL_UNIT_TYPE_SEI = 6; - private static final int NAL_UNIT_TYPE_SPS = 7; - private static final int NAL_UNIT_TYPE_PPS = 8; - private static final int NAL_UNIT_TYPE_AUD = 9; + private static final int FRAME_TYPE_I = 2; + private static final int FRAME_TYPE_ALL_I = 7; + + private static final int NAL_UNIT_TYPE_IFR = 1; // Coded slice of a non-IDR picture + private static final int NAL_UNIT_TYPE_IDR = 5; // Coded slice of an IDR picture + private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information + private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set + private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set + private static final int NAL_UNIT_TYPE_AUD = 9; // Access unit delimiter private static final int EXTENDED_SAR = 0xFF; private static final float[] ASPECT_RATIO_IDC_VALUES = new float[] { 1f /* Unspecified. Assume square */, @@ -69,6 +73,7 @@ import java.util.List; // State that should be reset on seek. private final SeiReader seiReader; private final boolean[] prefixFlags; + private final IfrParserBuffer ifrParserBuffer; private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer sei; @@ -84,10 +89,11 @@ import java.util.List; private final ParsableByteArray seiWrapper; private int[] scratchEscapePositions; - public H264Reader(TrackOutput output, SeiReader seiReader) { + public H264Reader(TrackOutput output, SeiReader seiReader, boolean idrKeyframesOnly) { super(output); this.seiReader = seiReader; prefixFlags = new boolean[3]; + ifrParserBuffer = (idrKeyframesOnly) ? null : new IfrParserBuffer(); sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); @@ -102,6 +108,9 @@ import java.util.List; sps.reset(); pps.reset(); sei.reset(); + if (ifrParserBuffer != null) { + ifrParserBuffer.reset(); + } writingSample = false; totalBytesWritten = 0; } @@ -132,22 +141,30 @@ import java.util.List; int nalUnitType = H264Util.getNalUnitType(dataArray, nextNalUnitOffset); int bytesWrittenPastNalUnit = limit - nextNalUnitOffset; - if (nalUnitType == NAL_UNIT_TYPE_AUD) { - if (writingSample) { - if (isKeyframe && !hasOutputFormat && sps.isCompleted() && pps.isCompleted()) { - parseMediaFormat(sps, pps); + switch (nalUnitType) { + case NAL_UNIT_TYPE_IDR: + isKeyframe = true; + break; + case NAL_UNIT_TYPE_AUD: + if (writingSample) { + if (ifrParserBuffer != null && ifrParserBuffer.isCompleted()) { + int sliceType = ifrParserBuffer.getSliceType(); + isKeyframe |= (sliceType == FRAME_TYPE_I || sliceType == FRAME_TYPE_ALL_I); + ifrParserBuffer.reset(); + } + if (isKeyframe && !hasOutputFormat && sps.isCompleted() && pps.isCompleted()) { + parseMediaFormat(sps, pps); + } + int flags = isKeyframe ? C.SAMPLE_FLAG_SYNC : 0; + int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastNalUnit; + output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastNalUnit, null); + writingSample = false; } - int flags = isKeyframe ? C.SAMPLE_FLAG_SYNC : 0; - int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastNalUnit; - output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastNalUnit, null); - writingSample = false; - } - writingSample = true; - isKeyframe = false; - sampleTimeUs = pesTimeUs; - samplePosition = totalBytesWritten - bytesWrittenPastNalUnit; - } else if (nalUnitType == NAL_UNIT_TYPE_IDR) { - isKeyframe = true; + writingSample = true; + samplePosition = totalBytesWritten - bytesWrittenPastNalUnit; + sampleTimeUs = pesTimeUs; + isKeyframe = false; + break; } // If the length to the start of the unit is negative then we wrote too many bytes to the @@ -171,6 +188,9 @@ import java.util.List; } private void feedNalUnitTargetBuffersStart(int nalUnitType) { + if (ifrParserBuffer != null) { + ifrParserBuffer.startNalUnit(nalUnitType); + } if (!hasOutputFormat) { sps.startNalUnit(nalUnitType); pps.startNalUnit(nalUnitType); @@ -179,6 +199,9 @@ import java.util.List; } private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) { + if (ifrParserBuffer != null) { + ifrParserBuffer.appendToNalUnit(dataArray, offset, limit); + } if (!hasOutputFormat) { sps.appendToNalUnit(dataArray, offset, limit); pps.appendToNalUnit(dataArray, offset, limit); @@ -461,4 +484,99 @@ import java.util.List; } + /** + * A buffer specifically for IFR units that can be used to parse the IFR's slice type. + */ + private static final class IfrParserBuffer { + + private static final int DEFAULT_BUFFER_SIZE = 128; + private static final int NOT_SET = -1; + + private final ParsableBitArray scratchSliceType; + + private byte[] ifrData; + private int ifrLength; + private boolean isFilling; + private int sliceType; + + public IfrParserBuffer() { + ifrData = new byte[DEFAULT_BUFFER_SIZE]; + scratchSliceType = new ParsableBitArray(ifrData); + reset(); + } + + /** + * Resets the buffer, clearing any data that it holds. + */ + public void reset() { + isFilling = false; + ifrLength = 0; + sliceType = NOT_SET; + } + + /** + * True if enough data was added to the buffer that the slice type was determined. + */ + public boolean isCompleted() { + return sliceType != NOT_SET; + } + + /** + * Invoked to indicate that a NAL unit has started, and if it is an IFR then the buffer will + * start. + */ + public void startNalUnit(int nalUnitType) { + if (nalUnitType == NAL_UNIT_TYPE_IFR) { + reset(); + isFilling = true; + } + } + + /** + * Invoked to pass stream data. The data passed should not include 4 byte NAL unit prefixes. + * + * @param data Holds the data being passed. + * @param offset The offset of the data in {@code data}. + * @param limit The limit (exclusive) of the data in {@code data}. + */ + public void appendToNalUnit(byte[] data, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (ifrData.length < ifrLength + readLength) { + ifrData = Arrays.copyOf(ifrData, (ifrLength + readLength) * 2); + } + System.arraycopy(data, offset, ifrData, ifrLength, readLength); + ifrLength += readLength; + + scratchSliceType.reset(ifrData, ifrLength); + // first_mb_in_slice + int len = scratchSliceType.peekExpGolombCodedNumLength(); + if ((len == -1) || (len > scratchSliceType.bitsLeft())) { + // Not enough yet + return; + } + + scratchSliceType.skipBits(len); + // slice_type + len = scratchSliceType.peekExpGolombCodedNumLength(); + if ((len == -1) || (len > scratchSliceType.bitsLeft())) { + // Not enough yet + return; + } + sliceType = scratchSliceType.readUnsignedExpGolombCodedInt(); + + isFilling = false; + } + + /** + * @return the slice type of the IFR. + */ + public int getSliceType() { + return sliceType; + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java index 684ada760f..5a67127aee 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java @@ -53,6 +53,7 @@ public final class TsExtractor implements Extractor, SeekMap { private final ParsableByteArray tsPacketBuffer; private final ParsableBitArray tsScratch; + private final boolean idrKeyframesOnly; private final long firstSampleTimestampUs; /* package */ final SparseBooleanArray streamTypes; /* package */ final SparseBooleanArray allowedPassthroughStreamTypes; @@ -65,11 +66,21 @@ public final class TsExtractor implements Extractor, SeekMap { /* package */ Id3Reader id3Reader; public TsExtractor() { - this(0, null); + this(0); + } + + public TsExtractor(long firstSampleTimestampUs) { + this(firstSampleTimestampUs, null); } public TsExtractor(long firstSampleTimestampUs, AudioCapabilities audioCapabilities) { + this(firstSampleTimestampUs, audioCapabilities, true); + } + + public TsExtractor(long firstSampleTimestampUs, AudioCapabilities audioCapabilities, + boolean idrKeyframesOnly) { this.firstSampleTimestampUs = firstSampleTimestampUs; + this.idrKeyframesOnly = idrKeyframesOnly; tsScratch = new ParsableBitArray(new byte[3]); tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); streamTypes = new SparseBooleanArray(); @@ -103,6 +114,8 @@ public final class TsExtractor implements Extractor, SeekMap { return RESULT_END_OF_INPUT; } + // Note: see ISO/IEC 13818-1, section 2.4.3.2 for detailed information on the format of + // the header. tsPacketBuffer.setPosition(0); tsPacketBuffer.setLimit(TS_PACKET_SIZE); int syncByte = tsPacketBuffer.readUnsignedByte(); @@ -292,6 +305,8 @@ public final class TsExtractor implements Extractor, SeekMap { data.skipBytes(pointerField); } + // Note: see ISO/IEC 13818-1, section 2.4.4.8 for detailed information on the format of + // the header. data.readBytes(pmtScratch, 3); pmtScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) int sectionLength = pmtScratch.readBits(12); @@ -347,7 +362,8 @@ public final class TsExtractor implements Extractor, SeekMap { break; case TS_STREAM_TYPE_H264: SeiReader seiReader = new SeiReader(output.track(TS_STREAM_TYPE_EIA608)); - pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264), seiReader); + pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264), seiReader, + idrKeyframesOnly); break; case TS_STREAM_TYPE_ID3: pesPayloadReader = id3Reader; @@ -502,6 +518,8 @@ public final class TsExtractor implements Extractor, SeekMap { } private boolean parseHeader() { + // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of + // the header. pesScratch.setPosition(0); int startCodePrefix = pesScratch.readBits(24); if (startCodePrefix != 0x000001) { @@ -534,7 +552,7 @@ public final class TsExtractor implements Extractor, SeekMap { pesScratch.setPosition(0); timeUs = 0; if (ptsFlag) { - pesScratch.skipBits(4); // '0010' + pesScratch.skipBits(4); // '0010' or '0011' long pts = (long) pesScratch.readBits(3) << 30; pesScratch.skipBits(1); // marker_bit pts |= pesScratch.readBits(15) << 15; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/MulticastDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/MulticastDataSource.java new file mode 100644 index 0000000000..87a8802984 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/upstream/MulticastDataSource.java @@ -0,0 +1,140 @@ +/* + * 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.upstream; + +import com.google.android.exoplayer.C; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; + +/** + * A multicast {@link DataSource}. + */ +public class MulticastDataSource implements UriDataSource { + + /** + * Thrown when an error is encountered when trying to read from a {@link MulticastDataSource}. + */ + public static final class MulticastDataSourceException extends IOException { + + public MulticastDataSourceException(String message) { + super(message); + } + + public MulticastDataSourceException(IOException cause) { + super(cause); + } + + } + + public static final int DEFAULT_MAX_PACKET_SIZE = 2000; + + public static final int TRANSFER_LISTENER_PACKET_INTERVAL = 1000; + + private final TransferListener transferListener; + private final DatagramPacket packet; + + private DataSpec dataSpec; + private MulticastSocket socket; + private boolean opened; + + private int packetsReceived; + private byte[] packetBuffer; + private int packetRemaining; + + public MulticastDataSource(TransferListener transferListener) { + this(transferListener, DEFAULT_MAX_PACKET_SIZE); + } + + public MulticastDataSource(TransferListener transferListener, int maxPacketSize) { + this.transferListener = transferListener; + + packetBuffer = new byte[maxPacketSize]; + packet = new DatagramPacket(packetBuffer, 0, maxPacketSize); + } + + @Override + public long open(DataSpec dataSpec) throws MulticastDataSourceException { + this.dataSpec = dataSpec; + String uri = dataSpec.uri.toString(); + String host = uri.substring(0, uri.indexOf(':')); + int port = Integer.parseInt(uri.substring(uri.indexOf(':') + 1)); + + try { + socket = new MulticastSocket(port); + socket.joinGroup(InetAddress.getByName(host)); + } catch (IOException e) { + throw new MulticastDataSourceException(e); + } + + opened = true; + transferListener.onTransferStart(); + return C.LENGTH_UNBOUNDED; + } + + @Override + public void close() { + if (opened) { + socket.close(); + socket = null; + transferListener.onTransferEnd(); + packetRemaining = 0; + packetsReceived = 0; + opened = false; + } + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws MulticastDataSourceException { + // if we've read all the data, get another packet + if (packetRemaining == 0) { + if (packetsReceived == TRANSFER_LISTENER_PACKET_INTERVAL) { + transferListener.onTransferEnd(); + transferListener.onTransferStart(); + packetsReceived = 0; + } + + try { + socket.receive(packet); + } catch (IOException e) { + throw new MulticastDataSourceException(e); + } + + packetRemaining = packet.getLength(); + transferListener.onBytesTransferred(packetRemaining); + packetsReceived++; + } + + // don't try to read too much + if (packetRemaining < readLength) { + readLength = packetRemaining; + } + + int packetOffset = packet.getLength() - packetRemaining; + System.arraycopy(packetBuffer, packetOffset, buffer, offset, readLength); + packetRemaining -= readLength; + + return readLength; + } + + @Override + public String getUri() { + return dataSpec == null ? null : dataSpec.uri.toString(); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java index b8dd63228a..a2407d7c64 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java @@ -26,6 +26,7 @@ public final class ParsableBitArray { // byte (from 0 to 7). private int byteOffset; private int bitOffset; + private int byteLimit; /** Creates a new instance that initially has no backing data. */ public ParsableBitArray() {} @@ -36,7 +37,18 @@ public final class ParsableBitArray { * @param data The data to wrap. */ public ParsableBitArray(byte[] data) { + this(data, data.length); + } + + /** + * Creates a new instance that wraps an existing array. + * + * @param data The data to wrap. + * @param limit The limit in bytes. + */ + public ParsableBitArray(byte[] data, int limit) { this.data = data; + byteLimit = limit; } /** @@ -45,9 +57,27 @@ public final class ParsableBitArray { * @param data The array to wrap. */ public void reset(byte[] data) { + reset(data, data.length); + } + + /** + * Updates the instance to wrap {@code data}, and resets the position to zero. + * + * @param data The array to wrap. + * @param limit The limit in bytes. + */ + public void reset(byte[] data, int limit) { this.data = data; byteOffset = 0; bitOffset = 0; + byteLimit = limit; + } + + /** + * Returns the number of bits yet to be read. + */ + public int bitsLeft() { + return (byteLimit - byteOffset) * 8 - bitOffset; } /** @@ -67,6 +97,7 @@ public final class ParsableBitArray { public void setPosition(int position) { byteOffset = position / 8; bitOffset = position - (byteOffset * 8); + assertValidOffset(); } /** @@ -81,6 +112,7 @@ public final class ParsableBitArray { byteOffset++; bitOffset -= 8; } + assertValidOffset(); } /** @@ -103,12 +135,20 @@ public final class ParsableBitArray { return 0; } - int retval = 0; + int returnValue = 0; // While n >= 8, read whole bytes. while (n >= 8) { + int byteValue; + if (bitOffset != 0) { + byteValue = ((data[byteOffset] & 0xFF) << bitOffset) + | ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset)); + } else { + byteValue = data[byteOffset]; + } n -= 8; - retval |= (readUnsignedByte() << n); + returnValue |= (byteValue & 0xFF) << n; + byteOffset++; } if (n > 0) { @@ -117,12 +157,12 @@ public final class ParsableBitArray { if (nextBit > 8) { // Combine bits from current byte and next byte. - retval |= (((getUnsignedByte(byteOffset) << (nextBit - 8) - | (getUnsignedByte(byteOffset + 1) >> (16 - nextBit))) & writeMask)); + returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8) + | ((data[byteOffset + 1] & 0xFF) >> (16 - nextBit))) & writeMask)); byteOffset++; } else { // Bits to be read only within current byte. - retval |= ((getUnsignedByte(byteOffset) >> (8 - nextBit)) & writeMask); + returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask); if (nextBit == 8) { byteOffset++; } @@ -131,7 +171,27 @@ public final class ParsableBitArray { bitOffset = nextBit % 8; } - return retval; + assertValidOffset(); + return returnValue; + } + + /** + * Peeks the length of an Exp-Golomb-coded integer (signed or unsigned) starting from the current + * offset, returning the length or -1 if the limit is reached. + * + * @return The length of the Exp-Golob-coded integer, or -1. + */ + public int peekExpGolombCodedNumLength() { + int initialByteOffset = byteOffset; + int initialBitOffset = bitOffset; + int leadingZeros = 0; + while (byteOffset < byteLimit && !readBit()) { + leadingZeros++; + } + boolean hitLimit = byteOffset == byteLimit; + byteOffset = initialByteOffset; + bitOffset = initialBitOffset; + return hitLimit ? -1 : leadingZeros * 2 + 1; } /** @@ -153,22 +213,6 @@ public final class ParsableBitArray { return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2); } - private int readUnsignedByte() { - int value; - if (bitOffset != 0) { - value = ((data[byteOffset] & 0xFF) << bitOffset) - | ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset)); - } else { - value = data[byteOffset]; - } - byteOffset++; - return value & 0xFF; - } - - private int getUnsignedByte(int offset) { - return data[offset] & 0xFF; - } - private int readExpGolombCodeNum() { int leadingZeros = 0; while (!readBit()) { @@ -177,4 +221,11 @@ public final class ParsableBitArray { return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0); } + private void assertValidOffset() { + // It is fine for position to be at the end of the array, but no further. + Assertions.checkState(byteOffset >= 0 + && (bitOffset >= 0 && bitOffset < 8) + && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0))); + } + } 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 c2b19c6d99..03fc20564d 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 @@ -541,6 +541,22 @@ public final class Util { return result; } + /** + * Returns a hex string representation of the data provided. + * + * @param data The byte array containing the data to be turned into a hex string. + * @param beginIndex The begin index, inclusive. + * @param endIndex The end index, exclusive. + * @return A string containing the hex representation of the data provided. + */ + public static String getHexStringFromBytes(byte[] data, int beginIndex, int endIndex) { + StringBuffer dataStringBuffer = new StringBuffer(endIndex - beginIndex); + for (int i = beginIndex; i < endIndex; i++) { + dataStringBuffer.append(String.format("%02X", data[i])); + } + return dataStringBuffer.toString(); + } + /** * Returns a user agent string based on the given application name and the library version. * diff --git a/library/src/test/java/com/google/android/exoplayer/TimeRangeTest.java b/library/src/test/java/com/google/android/exoplayer/TimeRangeTest.java new file mode 100644 index 0000000000..5c93130de6 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/TimeRangeTest.java @@ -0,0 +1,36 @@ +/* + * 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; + +import junit.framework.TestCase; + +/** + * Unit test for {@link TimeRange}. + */ +public class TimeRangeTest extends TestCase { + + public void testEquals() { + TimeRange timeRange1 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 30000000); + assertTrue(timeRange1.equals(timeRange1)); + + TimeRange timeRange2 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 30000000); + assertTrue(timeRange1.equals(timeRange2)); + + TimeRange timeRange3 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 60000000); + assertFalse(timeRange1.equals(timeRange3)); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java b/library/src/test/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java index fc343bbc41..cc155827dd 100644 --- a/library/src/test/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java +++ b/library/src/test/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer.dash; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.TimeRange; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.Format; @@ -55,12 +57,19 @@ public class DashChunkSourceTest extends InstrumentationTestCase { private static final FormatEvaluator EVALUATOR = new FixedEvaluator(); - private static final long AVAILABILITY_START_TIME = 0; - private static final long AVAILABILITY_LATENCY = 5000; - private static final long AVAILABILITY_REALTIME_OFFSET = 1000; - private static final long AVAILABILITY_CURRENT_TIME = - AVAILABILITY_START_TIME + AVAILABILITY_LATENCY - AVAILABILITY_REALTIME_OFFSET; - private static final FakeClock AVAILABILITY_CLOCK = new FakeClock(AVAILABILITY_CURRENT_TIME); + private static final long VOD_DURATION = 30000; + + private static final long LIVE_SEGMENT_COUNT = 5; + private static final long LIVE_SEGMENT_DURATION_MS = 1000; + private static final long LIVE_TIMESHIFT_BUFFER_DEPTH_MS = + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS; + + private static final long AVAILABILITY_START_TIME_MS = 60000; + private static final long AVAILABILITY_REALTIME_OFFSET_MS = 1000; + private static final long AVAILABILITY_CURRENT_TIME_MS = + AVAILABILITY_START_TIME_MS + LIVE_TIMESHIFT_BUFFER_DEPTH_MS - AVAILABILITY_REALTIME_OFFSET_MS; + + private static final long LIVE_SEEK_BEYOND_EDGE_MS = 60000; private static final int TALL_HEIGHT = 200; private static final int WIDE_WIDTH = 400; @@ -90,6 +99,21 @@ public class DashChunkSourceTest extends InstrumentationTestCase { assertEquals(TALL_HEIGHT, out.getMaxVideoHeight()); } + public void testGetSeekRangeOnVod() { + DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), AdaptationSet.TYPE_VIDEO, + null, null, mock(FormatEvaluator.class)); + chunkSource.enable(); + TimeRange seekRange = chunkSource.getSeekRange(); + + long[] seekRangeValuesUs = seekRange.getCurrentBoundsUs(null); + assertEquals(0, seekRangeValuesUs[0]); + assertEquals(VOD_DURATION * 1000, seekRangeValuesUs[1]); + + long[] seekRangeValuesMs = seekRange.getCurrentBoundsMs(null); + assertEquals(0, seekRangeValuesMs[0]); + assertEquals(VOD_DURATION, seekRangeValuesMs[1]); + } + public void testMaxVideoDimensionsLegacy() { SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4"); Representation representation1 = @@ -107,147 +131,254 @@ public class DashChunkSourceTest extends InstrumentationTestCase { assertEquals(TALL_HEIGHT, out.getMaxVideoHeight()); } - public void testLiveEdgeNoLatencyWithTimeline() { - DashChunkSource chunkSource = setupLiveEdgeTimelineTest(0L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdgeNoLatency() { + long startTimeMs = 0; + long liveEdgeLatencyMs = 0; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 0; + long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 4000; + long chunkEndTimeMs = 5000; - assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdge500msLatencyWithTimeline() { - DashChunkSource chunkSource = setupLiveEdgeTimelineTest(500L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdgeAlmostNoLatency() { + long startTimeMs = 0; + long liveEdgeLatencyMs = 1; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 0; + long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 4000; + long chunkEndTimeMs = 5000; - assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdge1000msLatencyWithTimeline() { - DashChunkSource chunkSource = setupLiveEdgeTimelineTest(1000L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdge500msLatency() { + long startTimeMs = 0; + long liveEdgeLatencyMs = 500; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 0; + long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 4000; + long chunkEndTimeMs = 5000; - assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdge1001msLatencyWithTimeline() { - DashChunkSource chunkSource = setupLiveEdgeTimelineTest(1001L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdge1000msLatency() { + long startTimeMs = 0; + long liveEdgeLatencyMs = 1000; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 0; + long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 4000; + long chunkEndTimeMs = 5000; - assertEquals(3000000L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(4000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdge2500msLatencyWithTimeline() { - DashChunkSource chunkSource = setupLiveEdgeTimelineTest(2500L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdge1001msLatency() { + long startTimeMs = 0; + long liveEdgeLatencyMs = 1001; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 0; + long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 3000; + long chunkEndTimeMs = 4000; - assertEquals(2000000L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(3000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdgeVeryHighLatencyWithTimeline() { - DashChunkSource chunkSource = setupLiveEdgeTimelineTest(10000L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdge2500msLatency() { + long startTimeMs = 0; + long liveEdgeLatencyMs = 2500; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 0; + long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 2000; + long chunkEndTimeMs = 3000; - assertEquals(0L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(1000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdgeNoLatencyWithTemplate() { - DashChunkSource chunkSource = setupLiveEdgeTemplateTest(0L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdgeVeryHighLatency() { + long startTimeMs = 0; + long liveEdgeLatencyMs = 10000; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 0; + long seekRangeEndMs = 0; + long chunkStartTimeMs = 0; + long chunkEndTimeMs = 1000; - // this should actually return the "5th" segment, but it currently returns the "6th", which - // doesn't actually exist yet; this will be resolved in a subsequent cl (cl/87518875). - //assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); - //assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdgeAlmostNoLatencyWithTemplate() { - DashChunkSource chunkSource = setupLiveEdgeTemplateTest(1L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdgeNoLatencyInProgress() { + long startTimeMs = 3000; + long liveEdgeLatencyMs = 0; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 3000; + long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 7000; + long chunkEndTimeMs = 8000; - assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdge500msLatencyWithTemplate() { - DashChunkSource chunkSource = setupLiveEdgeTemplateTest(500L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdgeAlmostNoLatencyInProgress() { + long startTimeMs = 3000; + long liveEdgeLatencyMs = 1; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 3000; + long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 7000; + long chunkEndTimeMs = 8000; - assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdge1000msLatencyWithTemplate() { - DashChunkSource chunkSource = setupLiveEdgeTemplateTest(1000L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdge500msLatencyInProgress() { + long startTimeMs = 3000; + long liveEdgeLatencyMs = 500; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 3000; + long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 7000; + long chunkEndTimeMs = 8000; - assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdge1001msLatencyWithTemplate() { - DashChunkSource chunkSource = setupLiveEdgeTemplateTest(1001L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdge1000msLatencyInProgress() { + long startTimeMs = 3000; + long liveEdgeLatencyMs = 1000; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 3000; + long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 7000; + long chunkEndTimeMs = 8000; - assertEquals(3000000L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(4000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdge2500msLatencyWithTemplate() { - DashChunkSource chunkSource = setupLiveEdgeTemplateTest(2500L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdge1001msLatencyInProgress() { + long startTimeMs = 3000; + long liveEdgeLatencyMs = 1001; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 3000; + long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 6000; + long chunkEndTimeMs = 7000; - assertEquals(2000000L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(3000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdgeVeryHighLatencyWithTemplate() { - DashChunkSource chunkSource = setupLiveEdgeTemplateTest(10000L); - List queue = new ArrayList(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, 0, 0, out); + public void testLiveEdge2500msLatencyInProgress() { + long startTimeMs = 3000; + long liveEdgeLatencyMs = 2500; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 3000; + long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; + long chunkStartTimeMs = 5000; + long chunkEndTimeMs = 6000; - assertEquals(0L, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(1000000L, ((MediaChunk) out.chunk).endTimeUs); + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + } + + public void testLiveEdgeVeryHighLatencyInProgress() { + long startTimeMs = 3000; + long liveEdgeLatencyMs = 10000; + long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; + long seekRangeStartMs = 3000; + long seekRangeEndMs = 3000; + long chunkStartTimeMs = 3000; + long chunkEndTimeMs = 4000; + + checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, 0, 0, 1000); + checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, + seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } private static MediaPresentationDescription generateMpd(boolean live, - List representations) { + List representations, boolean limitTimeshiftBuffer) { Representation firstRepresentation = representations.get(0); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); Period period = new Period(null, firstRepresentation.periodStartMs, firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet)); long duration = (live) ? TrackRenderer.UNKNOWN_TIME_US : firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs; - return new MediaPresentationDescription(AVAILABILITY_START_TIME, duration, -1, live, -1, -1, + return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, duration, -1, live, -1, + (limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1, null, Collections.singletonList(period)); } @@ -256,72 +387,126 @@ public class DashChunkSourceTest extends InstrumentationTestCase { SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4"); Representation representation1 = - Representation.newInstance(0, 0, null, 0, TALL_VIDEO, segmentBase1); + Representation.newInstance(0, VOD_DURATION, null, 0, TALL_VIDEO, segmentBase1); representations.add(representation1); SingleSegmentBase segmentBase2 = new SingleSegmentBase("https://example.com/2.mp4"); Representation representation2 = - Representation.newInstance(0, 0, null, 0, WIDE_VIDEO, segmentBase2); + Representation.newInstance(0, VOD_DURATION, null, 0, WIDE_VIDEO, segmentBase2); representations.add(representation2); - return generateMpd(false, representations); + return generateMpd(false, representations, false); } - private static MediaPresentationDescription generateLiveMpdWithTimeline() { + private static MediaPresentationDescription generateLiveMpdWithTimeline(long startTime) { List representations = new ArrayList(); List segmentTimeline = new ArrayList(); - segmentTimeline.add(new SegmentTimelineElement(0L, 1000L)); - segmentTimeline.add(new SegmentTimelineElement(1000L, 1000L)); - segmentTimeline.add(new SegmentTimelineElement(2000L, 1000L)); - segmentTimeline.add(new SegmentTimelineElement(3000L, 1000L)); - segmentTimeline.add(new SegmentTimelineElement(4000L, 1000L)); List mediaSegments = new ArrayList(); - mediaSegments.add(new RangedUri("", "", 0L, 500L)); - mediaSegments.add(new RangedUri("", "", 500L, 500L)); - mediaSegments.add(new RangedUri("", "", 1000L, 500L)); - mediaSegments.add(new RangedUri("", "", 1500L, 500L)); - mediaSegments.add(new RangedUri("", "", 2000L, 500L)); + long byteStart = 0; + for (int i = 0; i < LIVE_SEGMENT_COUNT; i++) { + segmentTimeline.add(new SegmentTimelineElement(startTime, LIVE_SEGMENT_DURATION_MS)); + mediaSegments.add(new RangedUri("", "", byteStart, 500L)); + startTime += LIVE_SEGMENT_DURATION_MS; + byteStart += 500; + } MultiSegmentBase segmentBase = new SegmentList(null, 1000, 0, - TrackRenderer.UNKNOWN_TIME_US, 1, TrackRenderer.UNKNOWN_TIME_US, segmentTimeline, + TrackRenderer.UNKNOWN_TIME_US, 0, TrackRenderer.UNKNOWN_TIME_US, segmentTimeline, mediaSegments); - Representation representation = Representation.newInstance(0, TrackRenderer.UNKNOWN_TIME_US, - null, 0, REGULAR_VIDEO, segmentBase); + Representation representation = Representation.newInstance(startTime, + TrackRenderer.UNKNOWN_TIME_US, null, 0, REGULAR_VIDEO, segmentBase); representations.add(representation); - return generateMpd(true, representations); + return generateMpd(true, representations, false); } - private static MediaPresentationDescription generateLiveMpdWithTemplate() { + private static MediaPresentationDescription generateLiveMpdWithTemplate( + boolean limitTimeshiftBuffer) { List representations = new ArrayList(); UrlTemplate initializationTemplate = null; UrlTemplate mediaTemplate = UrlTemplate.compile("$RepresentationID$/$Number$"); MultiSegmentBase segmentBase = new SegmentTemplate(null, 1000, 0, - TrackRenderer.UNKNOWN_TIME_US, 1, 1000, null, + TrackRenderer.UNKNOWN_TIME_US, 0, LIVE_SEGMENT_DURATION_MS, null, initializationTemplate, mediaTemplate, "http://www.youtube.com"); Representation representation = Representation.newInstance(0, TrackRenderer.UNKNOWN_TIME_US, null, 0, REGULAR_VIDEO, segmentBase); representations.add(representation); - return generateMpd(true, representations); + return generateMpd(true, representations, limitTimeshiftBuffer); } - private DashChunkSource setupLiveEdgeTimelineTest(long liveEdgeLatencyMs) { - MediaPresentationDescription manifest = generateLiveMpdWithTimeline(); + private DashChunkSource setupLiveEdgeTimelineTest(long startTime, long liveEdgeLatencyMs) { + MediaPresentationDescription manifest = generateLiveMpdWithTimeline(startTime); when(mockManifestFetcher.getManifest()).thenReturn(manifest); - return new DashChunkSource(mockManifestFetcher, manifest, AdaptationSet.TYPE_VIDEO, null, - mockDataSource, EVALUATOR, AVAILABILITY_CLOCK, liveEdgeLatencyMs * 1000, - AVAILABILITY_REALTIME_OFFSET * 1000); + DashChunkSource chunkSource = new DashChunkSource(mockManifestFetcher, manifest, + AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR, + new FakeClock(AVAILABILITY_CURRENT_TIME_MS + startTime), liveEdgeLatencyMs * 1000, + AVAILABILITY_REALTIME_OFFSET_MS * 1000, null, null); + chunkSource.enable(); + return chunkSource; } - private DashChunkSource setupLiveEdgeTemplateTest(long liveEdgeLatencyMs) { - MediaPresentationDescription manifest = generateLiveMpdWithTemplate(); + private DashChunkSource setupLiveEdgeTemplateTest(long startTime, long liveEdgeLatencyMs, + boolean limitTimeshiftBuffer) { + MediaPresentationDescription manifest = generateLiveMpdWithTemplate(limitTimeshiftBuffer); when(mockManifestFetcher.getManifest()).thenReturn(manifest); - return new DashChunkSource(mockManifestFetcher, manifest, AdaptationSet.TYPE_VIDEO, null, - mockDataSource, EVALUATOR, AVAILABILITY_CLOCK, liveEdgeLatencyMs * 1000, - AVAILABILITY_REALTIME_OFFSET * 1000); + DashChunkSource chunkSource = new DashChunkSource(mockManifestFetcher, manifest, + AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR, + new FakeClock(AVAILABILITY_CURRENT_TIME_MS + startTime), liveEdgeLatencyMs * 1000, + AVAILABILITY_REALTIME_OFFSET_MS * 1000, null, null); + chunkSource.enable(); + return chunkSource; + } + + private void checkLiveEdgeLatencyWithTimeline(long startTimeMs, long liveEdgeLatencyMs, + long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, long chunkStartTimeMs, + long chunkEndTimeMs) { + DashChunkSource chunkSource = setupLiveEdgeTimelineTest(startTimeMs, liveEdgeLatencyMs); + List queue = new ArrayList(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out); + TimeRange seekRange = chunkSource.getSeekRange(); + + assertNotNull(out.chunk); + long[] seekRangeValuesUs = seekRange.getCurrentBoundsUs(null); + assertEquals(seekRangeStartMs * 1000, seekRangeValuesUs[0]); + assertEquals(seekRangeEndMs * 1000, seekRangeValuesUs[1]); + assertEquals(chunkStartTimeMs * 1000, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(chunkEndTimeMs * 1000, ((MediaChunk) out.chunk).endTimeUs); + } + + private void checkLiveEdgeLatencyWithTemplate(long startTimeMs, long liveEdgeLatencyMs, + long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, long chunkStartTimeMs, + long chunkEndTimeMs, boolean limitTimeshiftBuffer) { + DashChunkSource chunkSource = setupLiveEdgeTemplateTest(startTimeMs, liveEdgeLatencyMs, + limitTimeshiftBuffer); + List queue = new ArrayList(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out); + TimeRange seekRange = chunkSource.getSeekRange(); + + assertNotNull(out.chunk); + long[] seekRangeValuesUs = seekRange.getCurrentBoundsUs(null); + assertEquals(seekRangeStartMs * 1000, seekRangeValuesUs[0]); + assertEquals(seekRangeEndMs * 1000, seekRangeValuesUs[1]); + assertEquals(chunkStartTimeMs * 1000, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(chunkEndTimeMs * 1000, ((MediaChunk) out.chunk).endTimeUs); + } + + private void checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(long startTimeMs, + long liveEdgeLatencyMs, long seekPositionMs, long seekRangeEndMs, + long chunkStartTimeMs, long chunkEndTimeMs) { + checkLiveEdgeLatencyWithTemplate(startTimeMs, liveEdgeLatencyMs, seekPositionMs, 0, + seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs, false); + } + + private void checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(long startTimeMs, + long liveEdgeLatencyMs, long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, + long chunkStartTimeMs, long chunkEndTimeMs) { + checkLiveEdgeLatencyWithTemplate(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs, true); } }