diff --git a/README.md b/README.md index f1499b23dd..15a87a915c 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ accompanying demo application. To get started: ## Using Gradle ## -ExoPlayer can also be built using Gradle. You can include it as a dependent project and build from source. e.g. +ExoPlayer can also be built using Gradle. You can include it as a dependent project and build from source: ``` // settings.gradle @@ -74,3 +74,11 @@ If you want to use ExoPlayer as a jar, run: ``` and copy library.jar to the libs-folder of your new project. + +The project is also available on [jCenter](https://bintray.com/google/exoplayer/exoplayer/view): + +``` +compile 'com.google.android.exoplayer:exoplayer:rX.X.X' +``` + +Where `rX.X.X` should be replaced with the desired version. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1dfc9ac394..1f46c5c6e2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,17 @@ # Release notes # +### Current dev branch (from r1.3.3) ### + +* 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. +* (WorkInProgress) - First steps to supporting styled + positioned subtitles. + +### r1.3.3 ### + +* HLS: Fix failure when playing HLS AAC streams. +* Misc bug fixes. + ### r1.3.2 ### * DataSource improvements: `DefaultUriDataSource` now handles http://, https://, file://, asset:// diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 2c0158c23f..6ee2474b80 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ 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/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 5dd1eb8d32..822c85ec4e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -35,7 +35,8 @@ import com.google.android.exoplayer.metadata.GeobMetadata; import com.google.android.exoplayer.metadata.PrivMetadata; import com.google.android.exoplayer.metadata.TxxxMetadata; import com.google.android.exoplayer.text.CaptionStyleCompat; -import com.google.android.exoplayer.text.SubtitleView; +import com.google.android.exoplayer.text.Cue; +import com.google.android.exoplayer.text.SubtitleLayout; import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.VerboseLogUtil; @@ -43,12 +44,10 @@ import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; -import android.view.Display; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; @@ -58,7 +57,6 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnKeyListener; import android.view.View.OnTouchListener; -import android.view.WindowManager; import android.view.accessibility.CaptioningManager; import android.widget.Button; import android.widget.MediaController; @@ -67,13 +65,14 @@ import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.TextView; import android.widget.Toast; +import java.util.List; import java.util.Map; /** * An activity that plays media using {@link DemoPlayer}. */ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, - DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener, + DemoPlayer.Listener, DemoPlayer.CaptionListener, DemoPlayer.Id3MetadataListener, AudioCapabilitiesReceiver.Listener { public static final String CONTENT_TYPE_EXTRA = "content_type"; @@ -81,7 +80,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private static final String TAG = "PlayerActivity"; - private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f; private static final int MENU_GROUP_TRACKS = 1; private static final int ID_OFFSET = 2; @@ -92,7 +90,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private VideoSurfaceView surfaceView; private TextView debugTextView; private TextView playerStateTextView; - private SubtitleView subtitleView; + private SubtitleLayout subtitleLayout; private Button videoButton; private Button audioButton; private Button textButton; @@ -154,7 +152,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, debugTextView = (TextView) findViewById(R.id.debug_text_view); playerStateTextView = (TextView) findViewById(R.id.player_state_view); - subtitleView = (SubtitleView) findViewById(R.id.subtitles); + subtitleLayout = (SubtitleLayout) findViewById(R.id.subtitles); mediaController = new MediaController(this); mediaController.setAnchorView(root); @@ -256,7 +254,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, if (player == null) { player = new DemoPlayer(getRendererBuilder()); player.addListener(this); - player.setTextListener(this); + player.setCaptionListener(this); player.setMetadataListener(this); player.seekTo(playerPosition); playerNeedsPrepare = true; @@ -464,16 +462,11 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, debugRootView.setVisibility(View.VISIBLE); } - // DemoPlayer.TextListener implementation + // DemoPlayer.CaptionListener implementation @Override - public void onText(String text) { - if (TextUtils.isEmpty(text)) { - subtitleView.setVisibility(View.INVISIBLE); - } else { - subtitleView.setVisibility(View.VISIBLE); - subtitleView.setText(text); - } + public void onCues(List cues) { + subtitleLayout.setCues(cues); } // DemoPlayer.MetadataListener implementation @@ -523,24 +516,16 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private void configureSubtitleView() { CaptionStyleCompat captionStyle; - float captionTextSize = getCaptionFontSize(); + float captionFontScale; if (Util.SDK_INT >= 19) { captionStyle = getUserCaptionStyleV19(); - captionTextSize *= getUserCaptionFontScaleV19(); + captionFontScale = getUserCaptionFontScaleV19(); } else { captionStyle = CaptionStyleCompat.DEFAULT; + captionFontScale = 1.0f; } - subtitleView.setStyle(captionStyle); - subtitleView.setTextSize(captionTextSize); - } - - private float getCaptionFontSize() { - Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay(); - Point displaySize = new Point(); - display.getSize(displaySize); - return Math.max(getResources().getDimension(R.dimen.subtitle_minimum_font_size), - CAPTION_LINE_HEIGHT_RATIO * Math.min(displaySize.x, displaySize.y)); + subtitleLayout.setStyle(captionStyle); + subtitleLayout.setFontScale(captionFontScale); } @TargetApi(19) 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..95ab7293ba 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,14 +21,17 @@ 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; +import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.TextRenderer; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.util.PlayerControl; @@ -39,6 +42,8 @@ import android.os.Looper; import android.view.Surface; import java.io.IOException; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; @@ -50,7 +55,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,13 +137,14 @@ 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); } /** * A listener for receiving notifications of timed text. */ - public interface TextListener { - void onText(String text); + public interface CaptionListener { + void onCues(List cues); } /** @@ -190,7 +196,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private int[] selectedTracks; private boolean backgrounded; - private TextListener textListener; + private CaptionListener captionListener; private Id3MetadataListener id3MetadataListener; private InternalErrorListener internalErrorListener; private InfoListener infoListener; @@ -229,8 +235,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi infoListener = listener; } - public void setTextListener(TextListener listener) { - textListener = listener; + public void setCaptionListener(CaptionListener listener) { + captionListener = listener; } public void setMetadataListener(Id3MetadataListener listener) { @@ -265,8 +271,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } selectedTracks[type] = index; pushTrackSelection(type, true); - if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) { - textListener.onText(null); + if (type == TYPE_TEXT && index == DISABLED_TRACK && captionListener != null) { + captionListener.onCues(Collections.emptyList()); } } @@ -506,8 +512,15 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } @Override - public void onText(String text) { - processText(text); + public void onCues(List cues) { + processCues(cues); + } + + @Override + public void onSeekRangeChanged(TimeRange seekRange) { + if (infoListener != null) { + infoListener.onSeekRangeChanged(seekRange); + } } /* package */ MetadataTrackRenderer.MetadataRenderer> @@ -607,11 +620,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } - /* package */ void processText(String text) { - if (textListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) { + /* package */ void processCues(List cues) { + if (captionListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) { return; } - textListener.onText(text); + captionListener.onCues(cues); } private class InternalRendererBuilderCallback implements RendererBuilderCallback { 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 - + android:layout_marginBottom="32dp"/> 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/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java index 3187aa4788..387d5e42e8 100644 --- a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java @@ -217,7 +217,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { * Provides access to {@link MediaDrm#setPropertyString(String, String)}. *

* This method may be called when the manager is in any state. - * + * * @param key The property to write. * @param value The value to write. */ 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/hls/HlsExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsExtractorWrapper.java index b189a12bd2..f6c784a5e3 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsExtractorWrapper.java @@ -61,7 +61,6 @@ public final class HlsExtractorWrapper implements ExtractorOutput { this.extractor = extractor; this.shouldSpliceIn = shouldSpliceIn; sampleQueues = new SparseArray(); - extractor.init(this); } /** @@ -71,6 +70,7 @@ public final class HlsExtractorWrapper implements ExtractorOutput { */ public void init(Allocator allocator) { this.allocator = allocator; + this.extractor.init(this); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java index 4d0b163cb0..3ac98a07af 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java @@ -72,7 +72,7 @@ public final class HlsPlaylistParser implements UriLoadable.Parser private static final Pattern CODECS_ATTR_REGEX = Pattern.compile(CODECS_ATTR + "=\"(.+?)\""); private static final Pattern RESOLUTION_ATTR_REGEX = - Pattern.compile(RESOLUTION_ATTR + "=(\\d+(\\.\\d+)?x\\d+(\\.\\d+)?)"); + Pattern.compile(RESOLUTION_ATTR + "=(\\d+x\\d+)"); private static final Pattern MEDIA_DURATION_REGEX = Pattern.compile(MEDIA_DURATION_TAG + ":([\\d.]+),"); private static final Pattern MEDIA_SEQUENCE_REGEX = @@ -168,12 +168,12 @@ public final class HlsPlaylistParser implements UriLoadable.Parser RESOLUTION_ATTR_REGEX); if (resolutionString != null) { String[] widthAndHeight = resolutionString.split("x"); - width = Math.round(Float.parseFloat(widthAndHeight[0])); + width = Integer.parseInt(widthAndHeight[0]); if (width <= 0) { // Width was invalid. width = -1; } - height = Math.round(Float.parseFloat(widthAndHeight[1])); + height = Integer.parseInt(widthAndHeight[1]); if (height <= 0) { // Height was invalid. height = -1; diff --git a/library/src/main/java/com/google/android/exoplayer/text/Cue.java b/library/src/main/java/com/google/android/exoplayer/text/Cue.java new file mode 100644 index 0000000000..f9476b5a5a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/Cue.java @@ -0,0 +1,53 @@ +/* + * 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.text; + +import android.text.Layout.Alignment; + +/** + * Contains information about a specific cue, including textual content and formatting data. + */ +public class Cue { + + /** + * Used by some methods to indicate that no value is set. + */ + public static final int UNSET_VALUE = -1; + + public final CharSequence text; + + public final int line; + public final int position; + public final Alignment alignment; + public final int size; + + public Cue() { + this(null); + } + + public Cue(CharSequence text) { + this(text, UNSET_VALUE, UNSET_VALUE, null, UNSET_VALUE); + } + + public Cue(CharSequence text, int line, int position, Alignment alignment, int size) { + this.text = text; + this.line = line; + this.position = position; + this.alignment = alignment; + this.size = size; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/Subtitle.java b/library/src/main/java/com/google/android/exoplayer/text/Subtitle.java index ff542cfb96..2b2e1ad44b 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/Subtitle.java +++ b/library/src/main/java/com/google/android/exoplayer/text/Subtitle.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.text; +import java.util.List; + /** * A subtitle that contains textual data associated with time indices. */ @@ -39,8 +41,8 @@ public interface Subtitle { public int getNextEventTimeIndex(long timeUs); /** - * Gets the number of event times, where events are defined as points in time at which the text - * returned by {@link #getText(long)} changes. + * Gets the number of event times, where events are defined as points in time at which the cues + * returned by {@link #getCues(long)} changes. * * @return The number of event times. */ @@ -62,11 +64,11 @@ public interface Subtitle { public long getLastEventTime(); /** - * Retrieve the subtitle text that should be displayed at a given time. + * Retrieve the subtitle cues that should be displayed at a given time. * * @param timeUs The time in microseconds. - * @return The text that should be displayed, or null. + * @return A list of cues that should be displayed, possibly empty. */ - public String getText(long timeUs); + public List getCues(long timeUs); } diff --git a/library/src/main/java/com/google/android/exoplayer/text/SubtitleLayout.java b/library/src/main/java/com/google/android/exoplayer/text/SubtitleLayout.java new file mode 100644 index 0000000000..e82b8f6121 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/SubtitleLayout.java @@ -0,0 +1,189 @@ +/* + * 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.text; + +import android.content.Context; +import android.text.Layout.Alignment; +import android.util.AttributeSet; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * A view for rendering rich-formatted captions. + */ +public final class SubtitleLayout extends ViewGroup { + + /** + * Use the same line height ratio as WebVtt to match the display with the preview. + * WebVtt specifies line height as 5.3% of the viewport height. + */ + private static final float LINE_HEIGHT_RATIO = 0.0533f; + + private final List subtitleViews; + + private List subtitleCues; + private int viewsInUse; + + private float fontScale; + private float textSize; + private CaptionStyleCompat captionStyle; + + public SubtitleLayout(Context context) { + this(context, null); + } + + public SubtitleLayout(Context context, AttributeSet attrs) { + super(context, attrs); + subtitleViews = new ArrayList(); + fontScale = 1; + captionStyle = CaptionStyleCompat.DEFAULT; + } + + /** + * Sets the cues to be displayed by the view. + * + * @param cues The cues to display. + */ + public void setCues(List cues) { + subtitleCues = cues; + int size = (cues == null) ? 0 : cues.size(); + + // create new subtitle views if necessary + if (size > subtitleViews.size()) { + for (int i = subtitleViews.size(); i < size; i++) { + SubtitleView newView = createSubtitleView(); + subtitleViews.add(newView); + } + } + + // add the views we currently need, if necessary + for (int i = viewsInUse; i < size; i++) { + addView(subtitleViews.get(i)); + } + + // remove the views we don't currently need, if necessary + for (int i = size; i < viewsInUse; i++) { + removeView(subtitleViews.get(i)); + } + + viewsInUse = size; + + for (int i = 0; i < size; i++) { + subtitleViews.get(i).setText(cues.get(i).text); + } + + requestLayout(); + } + + /** + * Sets the scale of the font. + * + * @param scale The scale of the font. + */ + public void setFontScale(float scale) { + fontScale = scale; + updateSubtitlesTextSize(); + + for (SubtitleView subtitleView : subtitleViews) { + subtitleView.setTextSize(textSize); + } + requestLayout(); + } + + /** + * Configures the view according to the given style. + * + * @param captionStyle A style for the view. + */ + public void setStyle(CaptionStyleCompat captionStyle) { + this.captionStyle = captionStyle; + + for (SubtitleView subtitleView : subtitleViews) { + subtitleView.setStyle(captionStyle); + } + requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(width, height); + + updateSubtitlesTextSize(); + + for (int i = 0; i < viewsInUse; i++) { + subtitleViews.get(i).setTextSize(textSize); + subtitleViews.get(i).measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int width = right - left; + int height = bottom - top; + + for (int i = 0; i < viewsInUse; i++) { + SubtitleView subtitleView = subtitleViews.get(i); + Cue subtitleCue = subtitleCues.get(i); + + int viewLeft = (width - subtitleView.getMeasuredWidth()) / 2; + int viewRight = viewLeft + subtitleView.getMeasuredWidth(); + int viewTop = bottom - subtitleView.getMeasuredHeight(); + int viewBottom = bottom; + + if (subtitleCue.alignment != null) { + subtitleView.setTextAlignment(subtitleCue.alignment); + } else { + subtitleView.setTextAlignment(Alignment.ALIGN_CENTER); + } + if (subtitleCue.position != Cue.UNSET_VALUE) { + if (subtitleCue.alignment == Alignment.ALIGN_OPPOSITE) { + viewRight = (int) ((width * (double) subtitleCue.position) / 100) + left; + viewLeft = Math.max(viewRight - subtitleView.getMeasuredWidth(), left); + } else { + viewLeft = (int) ((width * (double) subtitleCue.position) / 100) + left; + viewRight = Math.min(viewLeft + subtitleView.getMeasuredWidth(), right); + } + } + if (subtitleCue.line != Cue.UNSET_VALUE) { + viewTop = (int) (height * (double) subtitleCue.line / 100) + top; + viewBottom = viewTop + subtitleView.getMeasuredHeight(); + if (viewBottom > bottom) { + viewTop = bottom - subtitleView.getMeasuredHeight(); + viewBottom = bottom; + } + } + + subtitleView.layout(viewLeft, viewTop, viewRight, viewBottom); + } + } + + private void updateSubtitlesTextSize() { + textSize = LINE_HEIGHT_RATIO * getHeight() * fontScale; + } + + private SubtitleView createSubtitleView() { + SubtitleView view = new SubtitleView(getContext()); + view.setStyle(captionStyle); + view.setTextSize(textSize); + return view; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java b/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java index 7b977aa7cc..c317508dd4 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java +++ b/library/src/main/java/com/google/android/exoplayer/text/SubtitleView.java @@ -28,6 +28,7 @@ import android.graphics.Paint.Join; import android.graphics.Paint.Style; import android.graphics.RectF; import android.graphics.Typeface; +import android.text.Layout.Alignment; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; @@ -35,10 +36,7 @@ import android.util.DisplayMetrics; import android.view.View; /** - * A view for rendering captions. - *

- * The caption style and text size can be configured using {@link #setStyle(CaptionStyleCompat)} and - * {@link #setTextSize(float)} respectively. + * A view for rendering a single caption. */ public class SubtitleView extends View { @@ -52,11 +50,6 @@ public class SubtitleView extends View { */ private final RectF lineBounds = new RectF(); - /** - * Reusable string builder used for holding text. - */ - private final StringBuilder textBuilder = new StringBuilder(); - // Styled dimensions. private final float cornerRadius; private final float outlineWidth; @@ -66,6 +59,8 @@ public class SubtitleView extends View { private TextPaint textPaint; private Paint paint; + private CharSequence text; + private int foregroundColor; private int backgroundColor; private int edgeColor; @@ -75,10 +70,15 @@ public class SubtitleView extends View { private int lastMeasuredWidth; private StaticLayout layout; + private Alignment alignment; private float spacingMult; private float spacingAdd; private int innerPaddingX; + public SubtitleView(Context context) { + this(context, null); + } + public SubtitleView(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -107,6 +107,8 @@ public class SubtitleView extends View { textPaint.setAntiAlias(true); textPaint.setSubpixelText(true); + alignment = Alignment.ALIGN_CENTER; + paint = new Paint(); paint.setAntiAlias(true); @@ -116,10 +118,6 @@ public class SubtitleView extends View { setStyle(CaptionStyleCompat.DEFAULT); } - public SubtitleView(Context context) { - this(context, null); - } - @Override public void setBackgroundColor(int color) { backgroundColor = color; @@ -132,8 +130,7 @@ public class SubtitleView extends View { * @param text The text to display. */ public void setText(CharSequence text) { - textBuilder.setLength(0); - textBuilder.append(text); + this.text = text; forceUpdate(true); } @@ -150,6 +147,15 @@ public class SubtitleView extends View { } } + /** + * Sets the text alignment. + * + * @param textAlignment The text alignment. + */ + public void setTextAlignment(Alignment textAlignment) { + alignment = textAlignment; + } + /** * Configures the view according to the given style. * @@ -227,8 +233,7 @@ public class SubtitleView extends View { hasMeasurements = true; lastMeasuredWidth = maxWidth; - layout = new StaticLayout(textBuilder, textPaint, maxWidth, null, spacingMult, spacingAdd, - true); + layout = new StaticLayout(text, textPaint, maxWidth, alignment, spacingMult, spacingAdd, true); return true; } diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java index 8b0b1ae6dc..03899c0b0c 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextRenderer.java @@ -15,16 +15,18 @@ */ package com.google.android.exoplayer.text; +import java.util.List; + /** * An interface for components that render text. */ public interface TextRenderer { /** - * Invoked each time there is a change in the text to be rendered. + * Invoked each time there is a change in the {@link Cue}s to be rendered. * - * @param text The text to render, or null if no text is to be rendered. + * @param cues The {@link Cue}s to be rendered, or an empty list if no cues are to be rendered. */ - void onText(String text); + void onCues(List cues); } diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 3827e88a26..4a12b09777 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -30,6 +30,8 @@ import android.os.Looper; import android.os.Message; import java.io.IOException; +import java.util.Collections; +import java.util.List; /** * A {@link TrackRenderer} for textual subtitles. The actual rendering of each line of text to a @@ -255,34 +257,36 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { } private void updateTextRenderer(long positionUs) { - String text = subtitle.getText(positionUs); + List cues = subtitle.getCues(positionUs); if (textRendererHandler != null) { - textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, text).sendToTarget(); + textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, cues).sendToTarget(); } else { - invokeRendererInternal(text); + invokeRendererInternalCues(cues); } } private void clearTextRenderer() { if (textRendererHandler != null) { - textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, null).sendToTarget(); + textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, Collections.emptyList()) + .sendToTarget(); } else { - invokeRendererInternal(null); + invokeRendererInternalCues(Collections.emptyList()); } } + @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_UPDATE_OVERLAY: - invokeRendererInternal((String) msg.obj); + invokeRendererInternalCues((List) msg.obj); return true; } return false; } - private void invokeRendererInternal(String text) { - textRenderer.onText(text); + private void invokeRendererInternalCues(List cues) { + textRenderer.onCues(cues); } } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java index 48438ce1b4..d2f49d4038 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.TextRenderer; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; @@ -31,6 +32,7 @@ import android.os.Looper; import android.os.Message; import java.io.IOException; +import java.util.Collections; import java.util.TreeSet; /** @@ -227,8 +229,9 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback { return false; } - private void invokeRendererInternal(String text) { - textRenderer.onText(text); + private void invokeRendererInternal(String cueText) { + Cue cue = new Cue(cueText); + textRenderer.onCues(Collections.singletonList(cue)); } private void maybeParsePendingSample() { diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java index a0c4da091e..f010a747c3 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java @@ -15,9 +15,13 @@ */ package com.google.android.exoplayer.text.ttml; +import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.util.Util; +import java.util.Collections; +import java.util.List; + /** * A representation of a TTML subtitle. */ @@ -60,8 +64,14 @@ public final class TtmlSubtitle implements Subtitle { } @Override - public String getText(long timeUs) { - return root.getText(timeUs - startTimeUs); + public List getCues(long timeUs) { + String cueText = root.getText(timeUs - startTimeUs); + if (cueText == null) { + return Collections.emptyList(); + } else { + Cue cue = new Cue(cueText); + return Collections.singletonList(cue); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCue.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCue.java new file mode 100644 index 0000000000..1d6d3c554a --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCue.java @@ -0,0 +1,55 @@ +/* + * 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.text.webvtt; + +import com.google.android.exoplayer.text.Cue; + +import android.text.Layout.Alignment; + +/** + * A representation of a WebVTT cue. + */ +/* package */ final class WebvttCue extends Cue { + + public final long startTime; + public final long endTime; + + public WebvttCue(CharSequence text) { + this(Cue.UNSET_VALUE, Cue.UNSET_VALUE, text); + } + + public WebvttCue(long startTime, long endTime, CharSequence text) { + this(startTime, endTime, text, Cue.UNSET_VALUE, Cue.UNSET_VALUE, null, Cue.UNSET_VALUE); + } + + public WebvttCue(long startTime, long endTime, CharSequence text, int line, int position, + Alignment alignment, int size) { + super(text, line, position, alignment, size); + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * Returns whether or not this cue should be placed in the default position and rolled-up with + * the other "normal" cues. + * + * @return True if this cue should be placed in the default position; false otherwise. + */ + public boolean isNormalCue() { + return (line == UNSET_VALUE && position == UNSET_VALUE); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java index 5d331a78b2..93b52c48aa 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java @@ -17,9 +17,14 @@ package com.google.android.exoplayer.text.webvtt; import com.google.android.exoplayer.C; import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.SubtitleParser; import com.google.android.exoplayer.util.MimeTypes; +import android.text.Html; +import android.text.Layout.Alignment; +import android.util.Log; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -35,6 +40,8 @@ import java.util.regex.Pattern; */ public class WebvttParser implements SubtitleParser { + static final String TAG = "WebvttParser"; + /** * This parser allows a custom header to be prepended to the WebVTT data, in the form of a text * line starting with this string. @@ -63,21 +70,26 @@ public class WebvttParser implements SubtitleParser { private static final String WEBVTT_TIMESTAMP_STRING = "(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}"; private static final Pattern WEBVTT_TIMESTAMP = Pattern.compile(WEBVTT_TIMESTAMP_STRING); + private static final String WEBVTT_CUE_SETTING_STRING = "\\S*:\\S*"; + private static final Pattern WEBVTT_CUE_SETTING = Pattern.compile(WEBVTT_CUE_SETTING_STRING); + private static final Pattern MEDIA_TIMESTAMP_OFFSET = Pattern.compile(OFFSET + "\\d+"); private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:\\d+"); - private static final String WEBVTT_CUE_TAG_STRING = "\\<.*?>"; + private static final String NON_NUMERIC_STRING = ".*[^0-9].*"; + + private final StringBuilder textBuilder; private final boolean strictParsing; - private final boolean filterTags; public WebvttParser() { - this(true, true); + this(true); } - public WebvttParser(boolean strictParsing, boolean filterTags) { + public WebvttParser(boolean strictParsing) { this.strictParsing = strictParsing; - this.filterTags = filterTags; + + textBuilder = new StringBuilder(); } @Override @@ -145,6 +157,7 @@ public class WebvttParser implements SubtitleParser { // process the cues and text while ((line = webvttData.readLine()) != null) { + // parse the cue identifier (if present) { Matcher matcher = WEBVTT_CUE_IDENTIFIER.matcher(line); if (matcher.find()) { @@ -152,11 +165,16 @@ public class WebvttParser implements SubtitleParser { line = webvttData.readLine(); } + long startTime = Cue.UNSET_VALUE; + long endTime = Cue.UNSET_VALUE; + CharSequence text = null; + int lineNum = Cue.UNSET_VALUE; + int position = Cue.UNSET_VALUE; + Alignment alignment = null; + int size = Cue.UNSET_VALUE; + // parse the cue timestamps matcher = WEBVTT_TIMESTAMP.matcher(line); - long startTime; - long endTime; - String text = ""; // parse start timestamp if (!matcher.find()) { @@ -166,36 +184,76 @@ public class WebvttParser implements SubtitleParser { } // parse end timestamp + String endTimeString; if (!matcher.find()) { throw new ParserException("Expected cue end time: " + line); } else { - endTime = parseTimestampUs(matcher.group()) + mediaTimestampUs; + endTimeString = matcher.group(); + endTime = parseTimestampUs(endTimeString) + mediaTimestampUs; + } + + // parse the (optional) cue setting list + line = line.substring(line.indexOf(endTimeString) + endTimeString.length()); + matcher = WEBVTT_CUE_SETTING.matcher(line); + while (matcher.find()) { + String match = matcher.group(); + String[] parts = match.split(":", 2); + String name = parts[0]; + String value = parts[1]; + + try { + if ("line".equals(name)) { + if (value.endsWith("%")) { + lineNum = parseIntPercentage(value); + } else if (value.matches(NON_NUMERIC_STRING)) { + Log.w(TAG, "Invalid line value: " + value); + } else { + lineNum = Integer.parseInt(value); + } + } else if ("align".equals(name)) { + // TODO: handle for RTL languages + if ("start".equals(value)) { + alignment = Alignment.ALIGN_NORMAL; + } else if ("middle".equals(value)) { + alignment = Alignment.ALIGN_CENTER; + } else if ("end".equals(value)) { + alignment = Alignment.ALIGN_OPPOSITE; + } else if ("left".equals(value)) { + alignment = Alignment.ALIGN_NORMAL; + } else if ("right".equals(value)) { + alignment = Alignment.ALIGN_OPPOSITE; + } else { + Log.w(TAG, "Invalid align value: " + value); + } + } else if ("position".equals(name)) { + position = parseIntPercentage(value); + } else if ("size".equals(name)) { + size = parseIntPercentage(value); + } else { + Log.w(TAG, "Unknown cue setting " + name + ":" + value); + } + } catch (NumberFormatException e) { + Log.w(TAG, name + " contains an invalid value " + value, e); + } } // parse text + textBuilder.setLength(0); while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) { - text += processCueText(line.trim()) + "\n"; + if (textBuilder.length() > 0) { + textBuilder.append("
"); + } + textBuilder.append(line.trim()); } + text = Html.fromHtml(textBuilder.toString()); - WebvttCue cue = new WebvttCue(startTime, endTime, text); + WebvttCue cue = new WebvttCue(startTime, endTime, text, lineNum, position, alignment, size); subtitles.add(cue); } webvttData.close(); inputStream.close(); - - // copy WebvttCue data into arrays for WebvttSubtitle constructor - String[] cueText = new String[subtitles.size()]; - long[] cueTimesUs = new long[2 * subtitles.size()]; - for (int subtitleIndex = 0; subtitleIndex < subtitles.size(); subtitleIndex++) { - int arrayIndex = subtitleIndex * 2; - WebvttCue cue = subtitles.get(subtitleIndex); - cueTimesUs[arrayIndex] = cue.startTime; - cueTimesUs[arrayIndex + 1] = cue.endTime; - cueText[subtitleIndex] = cue.text; - } - - WebvttSubtitle subtitle = new WebvttSubtitle(cueText, mediaTimestampUs, cueTimesUs); + WebvttSubtitle subtitle = new WebvttSubtitle(subtitles, mediaTimestampUs); return subtitle; } @@ -208,25 +266,29 @@ public class WebvttParser implements SubtitleParser { return startTimeUs; } - protected String processCueText(String line) { - if (filterTags) { - line = line.replaceAll(WEBVTT_CUE_TAG_STRING, ""); - line = line.replaceAll("<", "<"); - line = line.replaceAll(">", ">"); - line = line.replaceAll(" ", " "); - line = line.replaceAll("&", "&"); - return line; - } else { - return line; - } - } - protected void handleNoncompliantLine(String line) throws ParserException { if (strictParsing) { throw new ParserException("Unexpected line: " + line); } } + private static int parseIntPercentage(String s) throws NumberFormatException { + if (!s.endsWith("%")) { + throw new NumberFormatException(s + " doesn't end with '%'"); + } + + s = s.substring(0, s.length() - 1); + if (s.matches(NON_NUMERIC_STRING)) { + throw new NumberFormatException(s + " contains an invalid character"); + } + + int value = Integer.parseInt(s); + if (value < 0 || value > 100) { + throw new NumberFormatException(value + " is out of range [0-100]"); + } + return value; + } + private static long parseTimestampUs(String s) throws NumberFormatException { if (!s.matches(WEBVTT_TIMESTAMP_STRING)) { throw new NumberFormatException("has invalid format"); @@ -240,16 +302,4 @@ public class WebvttParser implements SubtitleParser { return (value * 1000 + Long.parseLong(parts[1])) * 1000; } - private static class WebvttCue { - public final long startTime; - public final long endTime; - public final String text; - - public WebvttCue(long startTime, long endTime, String text) { - this.startTime = startTime; - this.endTime = endTime; - this.text = text; - } - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java index cc6bdc4ef4..e339782ab2 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java @@ -15,32 +15,46 @@ */ package com.google.android.exoplayer.text.webvtt; +import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; +import android.text.SpannableStringBuilder; + +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * A representation of a WebVTT subtitle. */ public class WebvttSubtitle implements Subtitle { - private final String[] cueText; + private final List cues; + private final int numCues; private final long startTimeUs; private final long[] cueTimesUs; private final long[] sortedCueTimesUs; /** - * @param cueText Text to be displayed during each cue. + * @param cues A list of the cues in this subtitle. * @param startTimeUs The start time of the subtitle. - * @param cueTimesUs Cue event times, where cueTimesUs[2 * i] and cueTimesUs[(2 * i) + 1] are - * the start and end times, respectively, corresponding to cueText[i]. */ - public WebvttSubtitle(String[] cueText, long startTimeUs, long[] cueTimesUs) { - this.cueText = cueText; + public WebvttSubtitle(List cues, long startTimeUs) { + this.cues = cues; + numCues = cues.size(); this.startTimeUs = startTimeUs; - this.cueTimesUs = cueTimesUs; + + this.cueTimesUs = new long[2 * numCues]; + for (int cueIndex = 0; cueIndex < numCues; cueIndex++) { + WebvttCue cue = cues.get(cueIndex); + int arrayIndex = cueIndex * 2; + cueTimesUs[arrayIndex] = cue.startTime; + cueTimesUs[arrayIndex + 1] = cue.endTime; + } + this.sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length); Arrays.sort(sortedCueTimesUs); } @@ -78,22 +92,47 @@ public class WebvttSubtitle implements Subtitle { } @Override - public String getText(long timeUs) { - StringBuilder stringBuilder = new StringBuilder(); + public List getCues(long timeUs) { + ArrayList list = null; + WebvttCue firstNormalCue = null; + SpannableStringBuilder normalCueTextBuilder = null; - for (int i = 0; i < cueTimesUs.length; i += 2) { - if ((cueTimesUs[i] <= timeUs) && (timeUs < cueTimesUs[i + 1])) { - stringBuilder.append(cueText[i / 2]); + for (int i = 0; i < numCues; i++) { + if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) { + if (list == null) { + list = new ArrayList(); + } + WebvttCue cue = cues.get(i); + if (cue.isNormalCue()) { + // we want to merge all of the normal cues into a single cue to ensure they are drawn + // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple + // normal cues, otherwise we can just append the single normal cue + if (firstNormalCue == null) { + firstNormalCue = cue; + } else if (normalCueTextBuilder == null) { + normalCueTextBuilder = new SpannableStringBuilder(); + normalCueTextBuilder.append(firstNormalCue.text).append("\n").append(cue.text); + } else { + normalCueTextBuilder.append("\n").append(cue.text); + } + } else { + list.add(cue); + } } } - - int stringLength = stringBuilder.length(); - if (stringLength > 0 && stringBuilder.charAt(stringLength - 1) == '\n') { - // Adjust the length to remove the trailing newline character. - stringLength -= 1; + if (normalCueTextBuilder != null) { + // there were multiple normal cues, so create a new cue with all of the text + list.add(new WebvttCue(normalCueTextBuilder)); + } else if (firstNormalCue != null) { + // there was only a single normal cue, so just add it to the list + list.add(firstNormalCue); } - return stringLength == 0 ? null : stringBuilder.substring(0, stringLength); + if (list != null) { + return list; + } else { + return Collections.emptyList(); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java index e3cbb7b84d..63f9023cc9 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java @@ -36,28 +36,6 @@ import java.io.IOException; */ public final class DefaultUriDataSource implements UriDataSource { - /** - * Thrown when a {@link DefaultUriDataSource} is opened for a URI with an unsupported scheme. - */ - public static final class UnsupportedSchemeException extends IOException { - - /** - * The unsupported scheme. - */ - public final String scheme; - - /** - * @param scheme The unsupported scheme. - */ - public UnsupportedSchemeException(String scheme) { - super("Unsupported URI scheme: " + scheme); - this.scheme = scheme; - } - - } - - private static final String SCHEME_HTTP = "http"; - private static final String SCHEME_HTTPS = "https"; private static final String SCHEME_FILE = "file"; private static final String SCHEME_ASSET = "asset"; private static final String SCHEME_CONTENT = "content"; @@ -141,9 +119,7 @@ public final class DefaultUriDataSource implements UriDataSource { Assertions.checkState(dataSource == null); // Choose the correct source for the scheme. String scheme = dataSpec.uri.getScheme(); - if (SCHEME_HTTP.equals(scheme) || SCHEME_HTTPS.equals(scheme)) { - dataSource = httpDataSource; - } else if (SCHEME_FILE.equals(scheme) || TextUtils.isEmpty(scheme)) { + if (SCHEME_FILE.equals(scheme) || TextUtils.isEmpty(scheme)) { if (dataSpec.uri.getPath().startsWith("/android_asset/")) { dataSource = assetDataSource; } else { @@ -154,7 +130,7 @@ public final class DefaultUriDataSource implements UriDataSource { } else if (SCHEME_CONTENT.equals(scheme)) { dataSource = contentDataSource; } else { - throw new UnsupportedSchemeException(scheme); + dataSource = httpDataSource; } // Open the source and return. return dataSource.open(dataSpec); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java b/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java index 6eb477fd0d..d3ce6bdc9c 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java @@ -128,21 +128,6 @@ public final class Loader { startLoading(myLooper, loadable, callback); } - /** - * Invokes {@link #startLoading(Looper, Loadable, Callback)}, using the {@link Looper} - * associated with the calling thread. Loading is delayed by {@code delayMs}. - * - * @param loadable The {@link Loadable} to load. - * @param callback A callback to invoke when the load ends. - * @param delayMs Number of milliseconds to wait before calling {@link Loadable#load()}. - * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}. - */ - public void startLoading(Loadable loadable, Callback callback, int delayMs) { - Looper myLooper = Looper.myLooper(); - Assertions.checkState(myLooper != null); - startLoading(myLooper, loadable, callback, delayMs); - } - /** * Start loading a {@link Loadable}. *

@@ -154,24 +139,9 @@ public final class Loader { * @param callback A callback to invoke when the load ends. */ public void startLoading(Looper looper, Loadable loadable, Callback callback) { - startLoading(looper, loadable, callback, 0); - } - - /** - * Start loading a {@link Loadable} after {@code delayMs} has elapsed. - *

- * A {@link Loader} instance can only load one {@link Loadable} at a time, and so this method - * must not be called when another load is in progress. - * - * @param looper The looper of the thread on which the callback should be invoked. - * @param loadable The {@link Loadable} to load. - * @param callback A callback to invoke when the load ends. - * @param delayMs Number of milliseconds to wait before calling {@link Loadable#load()}. - */ - public void startLoading(Looper looper, Loadable loadable, Callback callback, int delayMs) { Assertions.checkState(!loading); loading = true; - currentTask = new LoadTask(looper, loadable, callback, delayMs); + currentTask = new LoadTask(looper, loadable, callback); downloadExecutorService.submit(currentTask); } @@ -213,15 +183,13 @@ public final class Loader { private final Loadable loadable; private final Loader.Callback callback; - private final int delayMs; private volatile Thread executorThread; - public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback, int delayMs) { + public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback) { super(looper); this.loadable = loadable; this.callback = callback; - this.delayMs = delayMs; } public void quit() { @@ -235,9 +203,6 @@ public final class Loader { public void run() { try { executorThread = Thread.currentThread(); - if (delayMs > 0) { - Thread.sleep(delayMs); - } if (!loadable.isLoadCanceled()) { loadable.load(); } 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/assets/webvtt/typical_with_tags b/library/src/test/assets/webvtt/typical_with_tags index 36e630e240..aecf1cb2b7 100644 --- a/library/src/test/assets/webvtt/typical_with_tags +++ b/library/src/test/assets/webvtt/typical_with_tags @@ -11,4 +11,4 @@ This is the second subtitle. This is the third subtitle. 00:06.000 --> 00:07.000 -This is the <fourth> &subtitle. +This is the <fourth> &subtitle. 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); } } diff --git a/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java index 426c5152df..e5bbf7b331 100644 --- a/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java +++ b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java @@ -59,13 +59,13 @@ public class WebvttParserTest extends InstrumentationTestCase { // test first cue assertEquals(startTimeUs, subtitle.getEventTime(0)); assertEquals("This is the first subtitle.", - subtitle.getText(subtitle.getEventTime(0))); + subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()); assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1)); // test second cue assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2)); assertEquals("This is the second subtitle.", - subtitle.getText(subtitle.getEventTime(2))); + subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()); assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3)); } @@ -84,13 +84,13 @@ public class WebvttParserTest extends InstrumentationTestCase { // test first cue assertEquals(startTimeUs, subtitle.getEventTime(0)); assertEquals("This is the first subtitle.", - subtitle.getText(subtitle.getEventTime(0))); + subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()); assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1)); // test second cue assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2)); assertEquals("This is the second subtitle.", - subtitle.getText(subtitle.getEventTime(2))); + subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()); assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3)); } @@ -109,25 +109,25 @@ public class WebvttParserTest extends InstrumentationTestCase { // test first cue assertEquals(startTimeUs, subtitle.getEventTime(0)); assertEquals("This is the first subtitle.", - subtitle.getText(subtitle.getEventTime(0))); + subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()); assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1)); // test second cue assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2)); assertEquals("This is the second subtitle.", - subtitle.getText(subtitle.getEventTime(2))); + subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()); assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3)); // test third cue assertEquals(startTimeUs + 4000000, subtitle.getEventTime(4)); assertEquals("This is the third subtitle.", - subtitle.getText(subtitle.getEventTime(4))); + subtitle.getCues(subtitle.getEventTime(4)).get(0).text.toString()); assertEquals(startTimeUs + 5000000, subtitle.getEventTime(5)); // test fourth cue assertEquals(startTimeUs + 6000000, subtitle.getEventTime(6)); assertEquals("This is the &subtitle.", - subtitle.getText(subtitle.getEventTime(6))); + subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString()); assertEquals(startTimeUs + 7000000, subtitle.getEventTime(7)); } diff --git a/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitleTest.java b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitleTest.java index e95482f0fb..fc2ac13de7 100644 --- a/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitleTest.java +++ b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitleTest.java @@ -15,8 +15,13 @@ */ package com.google.android.exoplayer.text.webvtt; +import com.google.android.exoplayer.text.Cue; + import junit.framework.TestCase; +import java.util.ArrayList; +import java.util.List; + /** * Unit test for {@link WebvttSubtitle}. */ @@ -25,21 +30,39 @@ public class WebvttSubtitleTest extends TestCase { private static final String FIRST_SUBTITLE_STRING = "This is the first subtitle."; private static final String SECOND_SUBTITLE_STRING = "This is the second subtitle."; private static final String FIRST_AND_SECOND_SUBTITLE_STRING = - FIRST_SUBTITLE_STRING + SECOND_SUBTITLE_STRING; + FIRST_SUBTITLE_STRING + "\n" + SECOND_SUBTITLE_STRING; - private WebvttSubtitle emptySubtitle = new WebvttSubtitle(new String[] {}, 0, new long[] {}); + private WebvttSubtitle emptySubtitle = new WebvttSubtitle(new ArrayList(), 0); - private WebvttSubtitle simpleSubtitle = new WebvttSubtitle( - new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0, - new long[] {1000000, 2000000, 3000000, 4000000}); + private ArrayList simpleSubtitleCues = new ArrayList(); + { + WebvttCue firstCue = new WebvttCue(1000000, 2000000, FIRST_SUBTITLE_STRING); + simpleSubtitleCues.add(firstCue); - private WebvttSubtitle overlappingSubtitle = new WebvttSubtitle( - new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0, - new long[] {1000000, 3000000, 2000000, 4000000}); + WebvttCue secondCue = new WebvttCue(3000000, 4000000, SECOND_SUBTITLE_STRING); + simpleSubtitleCues.add(secondCue); + } + private WebvttSubtitle simpleSubtitle = new WebvttSubtitle(simpleSubtitleCues, 0); - private WebvttSubtitle nestedSubtitle = new WebvttSubtitle( - new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0, - new long[] {1000000, 4000000, 2000000, 3000000}); + private ArrayList overlappingSubtitleCues = new ArrayList(); + { + WebvttCue firstCue = new WebvttCue(1000000, 3000000, FIRST_SUBTITLE_STRING); + overlappingSubtitleCues.add(firstCue); + + WebvttCue secondCue = new WebvttCue(2000000, 4000000, SECOND_SUBTITLE_STRING); + overlappingSubtitleCues.add(secondCue); + } + private WebvttSubtitle overlappingSubtitle = new WebvttSubtitle(overlappingSubtitleCues, 0); + + private ArrayList nestedSubtitleCues = new ArrayList(); + { + WebvttCue firstCue = new WebvttCue(1000000, 4000000, FIRST_SUBTITLE_STRING); + nestedSubtitleCues.add(firstCue); + + WebvttCue secondCue = new WebvttCue(2000000, 3000000, SECOND_SUBTITLE_STRING); + nestedSubtitleCues.add(secondCue); + } + private WebvttSubtitle nestedSubtitle = new WebvttSubtitle(nestedSubtitleCues, 0); public void testEventCount() { assertEquals(0, emptySubtitle.getEventTimeCount()); @@ -72,29 +95,29 @@ public class WebvttSubtitleTest extends TestCase { public void testSimpleSubtitleText() { // Test before first subtitle - assertNull(simpleSubtitle.getText(0)); - assertNull(simpleSubtitle.getText(500000)); - assertNull(simpleSubtitle.getText(999999)); + assertSingleCueEmpty(simpleSubtitle.getCues(0)); + assertSingleCueEmpty(simpleSubtitle.getCues(500000)); + assertSingleCueEmpty(simpleSubtitle.getCues(999999)); // Test first subtitle - assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1000000)); - assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1500000)); - assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1000000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1500000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1999999)); // Test after first subtitle, before second subtitle - assertNull(simpleSubtitle.getText(2000000)); - assertNull(simpleSubtitle.getText(2500000)); - assertNull(simpleSubtitle.getText(2999999)); + assertSingleCueEmpty(simpleSubtitle.getCues(2000000)); + assertSingleCueEmpty(simpleSubtitle.getCues(2500000)); + assertSingleCueEmpty(simpleSubtitle.getCues(2999999)); // Test second subtitle - assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3000000)); - assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3500000)); - assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3999999)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3000000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3500000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3999999)); // Test after second subtitle - assertNull(simpleSubtitle.getText(4000000)); - assertNull(simpleSubtitle.getText(4500000)); - assertNull(simpleSubtitle.getText(Long.MAX_VALUE)); + assertSingleCueEmpty(simpleSubtitle.getCues(4000000)); + assertSingleCueEmpty(simpleSubtitle.getCues(4500000)); + assertSingleCueEmpty(simpleSubtitle.getCues(Long.MAX_VALUE)); } public void testOverlappingSubtitleEventTimes() { @@ -107,29 +130,32 @@ public class WebvttSubtitleTest extends TestCase { public void testOverlappingSubtitleText() { // Test before first subtitle - assertNull(overlappingSubtitle.getText(0)); - assertNull(overlappingSubtitle.getText(500000)); - assertNull(overlappingSubtitle.getText(999999)); + assertSingleCueEmpty(overlappingSubtitle.getCues(0)); + assertSingleCueEmpty(overlappingSubtitle.getCues(500000)); + assertSingleCueEmpty(overlappingSubtitle.getCues(999999)); // Test first subtitle - assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1000000)); - assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1500000)); - assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1000000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1500000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1999999)); // Test after first and second subtitle - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2000000)); - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2500000)); - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2999999)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, + overlappingSubtitle.getCues(2000000)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, + overlappingSubtitle.getCues(2500000)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, + overlappingSubtitle.getCues(2999999)); // Test second subtitle - assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3000000)); - assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3500000)); - assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3999999)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3000000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3500000)); + assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3999999)); // Test after second subtitle - assertNull(overlappingSubtitle.getText(4000000)); - assertNull(overlappingSubtitle.getText(4500000)); - assertNull(overlappingSubtitle.getText(Long.MAX_VALUE)); + assertSingleCueEmpty(overlappingSubtitle.getCues(4000000)); + assertSingleCueEmpty(overlappingSubtitle.getCues(4500000)); + assertSingleCueEmpty(overlappingSubtitle.getCues(Long.MAX_VALUE)); } public void testNestedSubtitleEventTimes() { @@ -142,29 +168,29 @@ public class WebvttSubtitleTest extends TestCase { public void testNestedSubtitleText() { // Test before first subtitle - assertNull(nestedSubtitle.getText(0)); - assertNull(nestedSubtitle.getText(500000)); - assertNull(nestedSubtitle.getText(999999)); + assertSingleCueEmpty(nestedSubtitle.getCues(0)); + assertSingleCueEmpty(nestedSubtitle.getCues(500000)); + assertSingleCueEmpty(nestedSubtitle.getCues(999999)); // Test first subtitle - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1000000)); - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1500000)); - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1000000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1500000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1999999)); // Test after first and second subtitle - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2000000)); - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2500000)); - assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2999999)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2000000)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2500000)); + assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2999999)); // Test first subtitle - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3000000)); - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3500000)); - assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3999999)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3000000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3500000)); + assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3999999)); // Test after second subtitle - assertNull(nestedSubtitle.getText(4000000)); - assertNull(nestedSubtitle.getText(4500000)); - assertNull(nestedSubtitle.getText(Long.MAX_VALUE)); + assertSingleCueEmpty(nestedSubtitle.getCues(4000000)); + assertSingleCueEmpty(nestedSubtitle.getCues(4500000)); + assertSingleCueEmpty(nestedSubtitle.getCues(Long.MAX_VALUE)); } private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) { @@ -201,4 +227,13 @@ public class WebvttSubtitleTest extends TestCase { assertEquals(-1, subtitle.getNextEventTimeIndex(Long.MAX_VALUE)); } + private void assertSingleCueEmpty(List cues) { + assertTrue(cues.size() == 0); + } + + private void assertSingleCueTextEquals(String expected, List cues) { + assertTrue(cues.size() == 1); + assertEquals(expected, cues.get(0).text.toString()); + } + }