Expose the seekable window.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=129747377
This commit is contained in:
andrewlewis 2016-08-09 07:34:55 -07:00 committed by Oliver Woodman
parent 39482f244b
commit 88bf1d0739
25 changed files with 470 additions and 155 deletions

View file

@ -91,11 +91,17 @@ import java.util.Locale;
}
@Override
public void onTimelineChanged(Timeline timeline) {
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
boolean isFinal = timeline.isFinal();
int periodCount = timeline.getPeriodCount();
Log.d(TAG, "timelineChanged [" + isFinal + ", "
+ (periodCount == Timeline.UNKNOWN_PERIOD_COUNT ? "?" : periodCount) + "]");
int seekWindowCount = timeline.getSeekWindowCount();
Log.d(TAG, "sourceInfo[isFinal=" + isFinal + ", periodCount="
+ (periodCount == Timeline.UNKNOWN_PERIOD_COUNT ? "?" : periodCount) + ", seekWindows: "
+ seekWindowCount);
for (int seekWindowIndex = 0; seekWindowIndex < seekWindowCount; seekWindowIndex++) {
Log.d(TAG, " " + timeline.getSeekWindow(seekWindowIndex));
}
Log.d(TAG, "]");
}
@Override

View file

@ -410,7 +410,7 @@ public class PlayerActivity extends Activity implements OnKeyListener, OnTouchLi
}
@Override
public void onTimelineChanged(Timeline timeline) {
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
// Do nothing.
}

View file

@ -101,7 +101,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
}
@Override
public void onTimelineChanged(Timeline timeline) {
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
// Do nothing.
}

View file

@ -101,7 +101,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
}
@Override
public void onTimelineChanged(Timeline timeline) {
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
// Do nothing.
}

View file

@ -120,7 +120,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
}
@Override
public void onTimelineChanged(Timeline timeline) {
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
// Do nothing.
}

View file

@ -140,11 +140,12 @@ public interface ExoPlayer {
void onPositionDiscontinuity(int periodIndex, long positionMs);
/**
* Called when the timeline changes.
* Called when manifest and/or timeline has been refreshed.
*
* @param timeline The new timeline.
* @param timeline The source's timeline.
* @param manifest The loaded manifest.
*/
void onTimelineChanged(Timeline timeline);
void onSourceInfoRefreshed(Timeline timeline, Object manifest);
/**
* Called when an error occurs. The playback state will transition to
@ -375,6 +376,12 @@ public interface ExoPlayer {
*/
Timeline getCurrentTimeline();
/**
* Returns the current manifest. The type depends on the {@link MediaSource} passed to
* {@link #setMediaSource(MediaSource)} or {@link #setMediaSource(MediaSource, boolean)}.
*/
Object getCurrentManifest();
/**
* Returns an estimate of the absolute position in milliseconds up to which data is buffered,
* or {@link ExoPlayer#UNKNOWN_TIME} if no estimate is available.

View file

@ -20,6 +20,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.Timeline;
@ -44,6 +45,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private int pendingSeekAcks;
private boolean isLoading;
private Timeline timeline;
private Object manifest;
// Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo;
@ -206,6 +208,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
return timeline;
}
@Override
public Object getCurrentManifest() {
return manifest;
}
@Override
public long getBufferedPosition() {
if (pendingSeekAcks == 0) {
@ -272,10 +279,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
break;
}
case ExoPlayerImplInternal.MSG_TIMELINE_CHANGED: {
timeline = (Timeline) msg.obj;
case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: {
@SuppressWarnings("unchecked")
Pair<Timeline, Object> timelineAndManifest = (Pair<Timeline, Object>) msg.obj;
timeline = timelineAndManifest.first;
manifest = timelineAndManifest.second;
for (EventListener listener : listeners) {
listener.onTimelineChanged(timeline);
listener.onSourceInfoRefreshed(timeline, manifest);
}
break;
}

View file

@ -43,7 +43,7 @@ import java.util.ArrayList;
* Implements the internal behavior of {@link ExoPlayerImpl}.
*/
/* package */ final class ExoPlayerImplInternal implements Handler.Callback, MediaPeriod.Callback,
TrackSelector.InvalidationListener, MediaSource.InvalidationListener {
TrackSelector.InvalidationListener, MediaSource.Listener {
/**
* Playback position information which is read on the application's thread by
@ -73,7 +73,7 @@ import java.util.ArrayList;
public static final int MSG_SET_PLAY_WHEN_READY_ACK = 3;
public static final int MSG_SEEK_ACK = 4;
public static final int MSG_POSITION_DISCONTINUITY = 5;
public static final int MSG_TIMELINE_CHANGED = 6;
public static final int MSG_SOURCE_INFO_REFRESHED = 6;
public static final int MSG_ERROR = 7;
// Internal messages
@ -309,12 +309,14 @@ import java.util.ArrayList;
}
}
// MediaSource.InvalidationListener implementation.
// MediaSource.Listener implementation.
@Override
public void onTimelineChanged(Timeline timeline) {
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
try {
handleSourceInvalidated(timeline);
eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, Pair.create(timeline, manifest))
.sendToTarget();
handleTimelineRefreshed(timeline);
} catch (ExoPlaybackException | IOException e) {
Log.e(TAG, "Error handling timeline change.", e);
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
@ -802,10 +804,9 @@ import java.util.ArrayList;
}
}
public void handleSourceInvalidated(Timeline timeline) throws ExoPlaybackException, IOException {
public void handleTimelineRefreshed(Timeline timeline) throws ExoPlaybackException, IOException {
Timeline oldTimeline = this.timeline;
this.timeline = timeline;
eventHandler.obtainMessage(MSG_TIMELINE_CHANGED, timeline).sendToTarget();
// Update the loaded periods to take into account the new timeline.
if (playingPeriod != null) {

View file

@ -404,6 +404,11 @@ public final class SimpleExoPlayer implements ExoPlayer {
return player.getCurrentTimeline();
}
@Override
public Object getCurrentManifest() {
return player.getCurrentManifest();
}
@Override
public long getBufferedPosition() {
return player.getBufferedPosition();

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
/**
@ -26,6 +27,7 @@ public final class ConcatenatingMediaSource implements MediaSource {
private final MediaSource[] mediaSources;
private final Timeline[] timelines;
private final Object[] manifests;
private ConcatenatedTimeline timeline;
@ -35,25 +37,29 @@ public final class ConcatenatingMediaSource implements MediaSource {
public ConcatenatingMediaSource(MediaSource... mediaSources) {
this.mediaSources = mediaSources;
timelines = new Timeline[mediaSources.length];
manifests = new Object[mediaSources.length];
}
@Override
public void prepareSource(final InvalidationListener listener) {
public void prepareSource(final Listener listener) {
for (int i = 0; i < mediaSources.length; i++) {
final int index = i;
mediaSources[i].prepareSource(new InvalidationListener() {
mediaSources[i].prepareSource(new Listener() {
@Override
public void onTimelineChanged(Timeline timeline) {
timelines[index] = timeline;
public void onSourceInfoRefreshed(Timeline sourceTimeline, Object manifest) {
timelines[index] = sourceTimeline;
manifests[index] = manifest;
for (int i = 0; i < timelines.length; i++) {
if (timelines[i] == null) {
// Don't invoke the listener until all sources have timelines.
return;
}
}
ConcatenatingMediaSource.this.timeline = new ConcatenatedTimeline(timelines.clone());
listener.onTimelineChanged(ConcatenatingMediaSource.this.timeline);
timeline = new ConcatenatedTimeline(timelines.clone());
listener.onSourceInfoRefreshed(timeline, manifests.clone());
}
});
}
}
@ -100,45 +106,44 @@ public final class ConcatenatingMediaSource implements MediaSource {
private static final class ConcatenatedTimeline implements Timeline {
private final Timeline[] timelines;
private final Object[] manifests;
private final int count;
private final boolean isFinal;
private int[] sourceOffsets;
private final int[] sourceOffsets;
private final SeekWindow[] seekWindows;
public ConcatenatedTimeline(Timeline[] timelines) {
this.timelines = timelines;
int[] sourceOffsets = new int[timelines.length];
int sourceIndexOffset = 0;
for (int i = 0; i < timelines.length; i++) {
int periodCount = timelines[i].getPeriodCount();
if (periodCount == Timeline.UNKNOWN_PERIOD_COUNT) {
sourceOffsets = Arrays.copyOf(sourceOffsets, i);
break;
}
sourceIndexOffset += periodCount;
sourceOffsets[i] = sourceIndexOffset;
}
this.sourceOffsets = sourceOffsets;
count = sourceOffsets.length == timelines.length ? sourceOffsets[sourceOffsets.length - 1]
: UNKNOWN_PERIOD_COUNT;
boolean isFinal = true;
manifests = new Object[timelines.length];
int[] sourceOffsets = new int[timelines.length];
int sourceOffset = 0;
ArrayList<SeekWindow> concatenatedSeekWindows = new ArrayList<>();
for (int i = 0; i < timelines.length; i++) {
Timeline timeline = timelines[i];
if (timeline != null) {
manifests[i] = timeline.getManifest();
if (!timeline.isFinal()) {
isFinal = false;
}
isFinal &= timeline.isFinal();
// Offset the seek windows so they are relative to the source.
int seekWindowCount = timeline.getSeekWindowCount();
for (int j = 0; j < seekWindowCount; j++) {
SeekWindow sourceSeekWindow = timeline.getSeekWindow(j);
concatenatedSeekWindows.add(sourceSeekWindow.copyOffsetByPeriodCount(sourceOffset));
}
int periodCount = timeline.getPeriodCount();
if (periodCount == Timeline.UNKNOWN_PERIOD_COUNT) {
sourceOffsets = Arrays.copyOf(sourceOffsets, i);
isFinal = false;
break;
}
sourceOffset += periodCount;
sourceOffsets[i] = sourceOffset;
}
this.timelines = timelines;
this.isFinal = isFinal;
this.sourceOffsets = sourceOffsets;
seekWindows =
concatenatedSeekWindows.toArray(new SeekWindow[concatenatedSeekWindows.size()]);
}
@Override
public int getPeriodCount() {
return count;
return sourceOffsets.length == timelines.length ? sourceOffsets[sourceOffsets.length - 1]
: UNKNOWN_PERIOD_COUNT;
}
@Override
@ -172,8 +177,13 @@ public final class ConcatenatingMediaSource implements MediaSource {
}
@Override
public Object getManifest() {
return manifests;
public int getSeekWindowCount() {
return seekWindows.length;
}
@Override
public SeekWindow getSeekWindow(int index) {
return seekWindows[index];
}
private int getSourceIndexForPeriod(int periodIndex) {

View file

@ -114,6 +114,7 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource,
private final Handler eventHandler;
private final EventListener eventListener;
private MediaSource.Listener sourceListener;
private DataSource dataSource;
private Loader loader;
private ExtractorHolder extractorHolder;
@ -178,8 +179,9 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource,
// MediaSource implementation.
@Override
public void prepareSource(InvalidationListener listener) {
listener.onTimelineChanged(new SinglePeriodTimeline(this));
public void prepareSource(MediaSource.Listener listener) {
sourceListener = listener;
listener.onSourceInfoRefreshed(SinglePeriodTimeline.createNonFinalTimeline(this), null);
}
@Override
@ -200,7 +202,7 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource,
@Override
public void releaseSource() {
// do nothing
sourceListener = null;
}
// MediaPeriod implementation.
@ -500,6 +502,9 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource,
tracks = new TrackGroupArray(trackArray);
prepared = true;
callback.onPeriodPrepared(this);
sourceListener.onSourceInfoRefreshed(seekMap.isSeekable()
? SinglePeriodTimeline.createSeekableFinalTimeline(this, durationUs)
: SinglePeriodTimeline.createUnseekableFinalTimeline(this, durationUs), null);
}
private void copyLengthFromLoader(ExtractingLoadable loadable) {

View file

@ -23,18 +23,17 @@ import java.io.IOException;
public interface MediaSource {
/**
* Listener for invalidation events.
* Listener for source events.
*/
interface InvalidationListener {
interface Listener {
/**
* Called when the timeline is invalidated.
* <p>
* May only be called on the player's thread.
* Called when manifest and/or timeline has been refreshed.
*
* @param timeline The new timeline.
* @param timeline The source's timeline.
* @param manifest The loaded manifest.
*/
void onTimelineChanged(Timeline timeline);
void onSourceInfoRefreshed(Timeline timeline, Object manifest);
}
@ -68,14 +67,15 @@ public interface MediaSource {
this.periodIndex = periodIndex;
this.positionUs = positionUs;
}
}
/**
* Starts preparation of the source.
*
* @param listener The listener for source invalidation events.
* @param listener The listener for source events.
*/
void prepareSource(InvalidationListener listener);
void prepareSource(Listener listener);
/**
* Returns the period index to play in this source's new timeline.

View file

@ -38,22 +38,26 @@ public final class MergingMediaSource implements MediaSource {
}
@Override
public void prepareSource(final InvalidationListener listener) {
mediaSources[0].prepareSource(new InvalidationListener() {
public void prepareSource(final Listener listener) {
mediaSources[0].prepareSource(new Listener() {
@Override
public void onTimelineChanged(Timeline timeline) {
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
checkConsistentTimeline(timeline);
// All source timelines must match.
listener.onTimelineChanged(timeline);
listener.onSourceInfoRefreshed(timeline, manifest);
}
});
for (int i = 1; i < mediaSources.length; i++) {
mediaSources[i].prepareSource(new InvalidationListener() {
mediaSources[i].prepareSource(new Listener() {
@Override
public void onTimelineChanged(Timeline timeline) {
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
checkConsistentTimeline(timeline);
}
});
}
}

View file

@ -0,0 +1,114 @@
/*
* Copyright (C) 2016 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.exoplayer2.source;
/**
* A window of times the player can seek to.
*/
public final class SeekWindow {
public static final SeekWindow UNSEEKABLE = new SeekWindow(0);
/**
* The period index at the start of the window.
*/
public final int startPeriodIndex;
/**
* The time at the start of the window relative to the start of the period at
* {@link #startPeriodIndex}, in microseconds.
*/
public final long startTimeUs;
/**
* The period index at the end of the window.
*/
public final int endPeriodIndex;
/**
* The time at the end of the window relative to the start of the period at
* {@link #endPeriodIndex}, in microseconds.
*/
public final long endTimeUs;
/**
* Constructs a new {@link SeekWindow} containing times from zero up to {@code durationUs} in the
* first period.
*
* @param durationUs The duration of the window, in microseconds.
*/
public SeekWindow(long durationUs) {
this(0, 0, 0, durationUs);
}
/**
* Constructs a new {@link SeekWindow} representing the specified time range.
*
* @param startPeriodIndex The index of the period containing the start of the window.
* @param startTimeUs The start time of the window in microseconds, relative to the start of the
* specified start period.
* @param endPeriodIndex The index of the period containing the end of the window.
* @param endTimeUs The end time of the window in microseconds, relative to the start of the
* specified end period.
*/
public SeekWindow(int startPeriodIndex, long startTimeUs, int endPeriodIndex, long endTimeUs) {
this.startPeriodIndex = startPeriodIndex;
this.startTimeUs = startTimeUs;
this.endPeriodIndex = endPeriodIndex;
this.endTimeUs = endTimeUs;
}
/**
* Returns a new seek window that is offset by the specified number of periods.
*
* @param periodCount The number of periods to add to {@link #startPeriodIndex} and
* {@link #endPeriodIndex} when constructing the copy.
* @return A new seek window that is offset by the specified number of periods.
*/
public SeekWindow copyOffsetByPeriodCount(int periodCount) {
return new SeekWindow(startPeriodIndex + periodCount, startTimeUs, endPeriodIndex + periodCount,
endTimeUs);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + startPeriodIndex;
result = 31 * result + (int) startTimeUs;
result = 31 * result + endPeriodIndex;
result = 31 * result + (int) endTimeUs;
return result;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
SeekWindow other = (SeekWindow) obj;
return other.startPeriodIndex == startPeriodIndex
&& other.startTimeUs == startTimeUs
&& other.endPeriodIndex == endPeriodIndex
&& other.endTimeUs == endTimeUs;
}
@Override
public String toString() {
return "SeekWindow[" + startPeriodIndex + ", " + startTimeUs + ", " + endPeriodIndex + ", "
+ endTimeUs + "]";
}
}

View file

@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
/**
@ -23,41 +23,50 @@ import com.google.android.exoplayer2.util.Assertions;
*/
public final class SinglePeriodTimeline implements Timeline {
/**
* Returns a new timeline with one period of unknown duration and no seek window.
*
* @param id The identifier for the period.
* @return A new timeline with one period of unknown duration.
*/
public static Timeline createNonFinalTimeline(Object id) {
return new SinglePeriodTimeline(id, false, C.UNSET_TIME_US);
}
/**
* Creates a final timeline with one period of known duration and an empty seek window.
*
* @param id The identifier for the period.
* @param durationUs The duration of the period, in microseconds.
* @return A new, unseekable, final timeline with one period.
*/
public static Timeline createUnseekableFinalTimeline(Object id, long durationUs) {
return new SinglePeriodTimeline(id, true, durationUs, SeekWindow.UNSEEKABLE);
}
/**
* Creates a final timeline with one period of known duration and a seek window extending from
* zero to its duration.
*
* @param id The identifier for the period.
* @param durationUs The duration of the period, in microseconds.
* @return A new, seekable, final timeline with one period.
*/
public static Timeline createSeekableFinalTimeline(Object id, long durationUs) {
return new SinglePeriodTimeline(id, true, durationUs, new SeekWindow(durationUs));
}
private final Object id;
private final Object manifest;
private final boolean isFinal;
private final long duration;
private final SeekWindow[] seekWindows;
/**
* Creates a new timeline with one period of unknown duration.
*
* @param id The identifier for the period.
*/
public SinglePeriodTimeline(Object id) {
this(id, null);
}
/**
* Creates a new timeline with one period of unknown duration providing an optional manifest.
*
* @param id The identifier for the period.
* @param manifest The source-specific manifest that defined the period, or {@code null}.
*/
public SinglePeriodTimeline(Object id, Object manifest) {
this(id, manifest, ExoPlayer.UNKNOWN_TIME);
}
/**
* Creates a new timeline with one period of the specified duration providing an optional
* manifest.
*
* @param id The identifier for the period.
* @param manifest The source-specific manifest that defined the period, or {@code null}.
* @param duration The duration of the period in milliseconds.
*/
public SinglePeriodTimeline(Object id, Object manifest, long duration) {
private SinglePeriodTimeline(Object id, boolean isFinal, long duration,
SeekWindow... seekWindows) {
this.id = Assertions.checkNotNull(id);
this.manifest = manifest;
this.isFinal = isFinal;
this.duration = duration;
this.seekWindows = seekWindows;
}
@Override
@ -67,7 +76,7 @@ public final class SinglePeriodTimeline implements Timeline {
@Override
public boolean isFinal() {
return true;
return isFinal;
}
@Override
@ -89,8 +98,13 @@ public final class SinglePeriodTimeline implements Timeline {
}
@Override
public Object getManifest() {
return manifest;
public int getSeekWindowCount() {
return seekWindows.length;
}
@Override
public SeekWindow getSeekWindow(int index) {
return seekWindows[index];
}
}

View file

@ -113,8 +113,9 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource,
// MediaSource implementation.
@Override
public void prepareSource(InvalidationListener listener) {
listener.onTimelineChanged(new SinglePeriodTimeline(this));
public void prepareSource(MediaSource.Listener listener) {
Timeline timeline = SinglePeriodTimeline.createSeekableFinalTimeline(this, durationUs);
listener.onSourceInfoRefreshed(timeline, null);
}
@Override
@ -135,7 +136,7 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource,
@Override
public void releaseSource() {
// do nothing
// Do nothing.
}
// MediaPeriod implementation.

View file

@ -40,7 +40,7 @@ public interface Timeline {
int getPeriodCount();
/**
* Returns whether the timeline is final, which means it will not be invalidated again.
* Returns whether the timeline is final, which means it will not change.
*/
boolean isFinal();
@ -57,7 +57,7 @@ public interface Timeline {
* Returns a unique identifier for the period at {@code index}, or {@code null} if the period at
* {@code index} is not known. The identifier is stable across {@link Timeline} instances.
* <p>
* When a source is invalidated the player uses period identifiers to determine what periods are
* When the timeline changes the player uses period identifiers to determine what periods are
* unchanged. Implementations that associate an object with each period can return the object for
* the provided index to guarantee uniqueness. Other implementations must be careful to return
* identifiers that can't clash with (for example) identifiers used by other timelines that may be
@ -78,8 +78,15 @@ public interface Timeline {
int getIndexOfPeriod(Object id);
/**
* Returns the immutable manifest corresponding to this timeline.
* Returns the number of seek windows that can be accessed via {@link #getSeekWindow(int)}.
*/
Object getManifest();
int getSeekWindowCount();
/**
* Returns the {@link SeekWindow} at {@code index}, which represents positions that can be seeked
* to in the timeline. The seek windows may change when
* {@link MediaSource.Listener#onSourceInfoRefreshed(Timeline, Object)} is called.
*/
SeekWindow getSeekWindow(int index);
}

View file

@ -78,7 +78,6 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
mediaChunks = new LinkedList<>();
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
sampleQueue = new DefaultTrackOutput(allocator);
pendingResetPositionUs = C.UNSET_TIME_US;
lastSeekPositionUs = positionUs;
pendingResetPositionUs = positionUs;
}

View file

@ -124,8 +124,7 @@ import java.util.List;
newSampleStreamArray(newEnabledSourceCount);
int newEnabledSourceIndex = 0;
// Iterate over currently enabled streams, either releasing them or adding them to the new
// list.
// Iterate over currently enabled streams, either releasing them or adding them to the new list.
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
if (oldStreams.contains(sampleStream)) {
sampleStream.release();
@ -137,8 +136,7 @@ import java.util.List;
// Instantiate and return new streams.
SampleStream[] streamsToReturn = new SampleStream[newSelections.size()];
for (int i = 0; i < newSelections.size(); i++) {
newSampleStreams[newEnabledSourceIndex] =
buildSampleStream(newSelections.get(i), positionUs);
newSampleStreams[newEnabledSourceIndex] = buildSampleStream(newSelections.get(i), positionUs);
streamsToReturn[i] = newSampleStreams[newEnabledSourceIndex];
newEnabledSourceIndex++;
}

View file

@ -25,9 +25,11 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SeekWindow;
import com.google.android.exoplayer2.source.Timeline;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.Period;
import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.Loader;
@ -52,6 +54,17 @@ public final class DashMediaSource implements MediaSource {
* The default minimum number of times to retry loading data prior to failing.
*/
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
/**
* The interval in milliseconds between invocations of
* {@link MediaSource.Listener#onSourceInfoRefreshed(Timeline, Object)} when the source's
* {@link SeekWindow} is changing dynamically (for example, for incomplete live streams).
*/
private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000;
/**
* The offset in microseconds subtracted from the live edge position when calculating the default
* position returned by {@link #getDefaultStartPosition(int)}.
*/
private static final long LIVE_EDGE_OFFSET_US = 30000000;
private static final String TAG = "DashMediaSource";
@ -62,8 +75,9 @@ public final class DashMediaSource implements MediaSource {
private final DashManifestParser manifestParser;
private final ManifestCallback manifestCallback;
private final Object manifestUriLock;
private final Runnable refreshSourceInfoRunnable;
private MediaSource.InvalidationListener invalidationListener;
private MediaSource.Listener sourceListener;
private DataSource dataSource;
private Loader loader;
@ -71,9 +85,10 @@ public final class DashMediaSource implements MediaSource {
private long manifestLoadStartTimestamp;
private long manifestLoadEndTimestamp;
private DashManifest manifest;
private Handler manifestRefreshHandler;
private Handler handler;
private ArrayList<DashMediaPeriod> periods;
private long elapsedRealtimeOffset;
private SeekWindow seekWindow;
private long elapsedRealtimeOffsetMs;
public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory,
DashChunkSource.Factory chunkSourceFactory, Handler eventHandler,
@ -93,6 +108,12 @@ public final class DashMediaSource implements MediaSource {
manifestParser = new DashManifestParser();
manifestCallback = new ManifestCallback();
manifestUriLock = new Object();
refreshSourceInfoRunnable = new Runnable() {
@Override
public void run() {
refreshSourceInfo();
}
};
}
/**
@ -109,11 +130,11 @@ public final class DashMediaSource implements MediaSource {
// MediaSource implementation.
@Override
public void prepareSource(InvalidationListener listener) {
invalidationListener = listener;
public void prepareSource(MediaSource.Listener listener) {
sourceListener = listener;
dataSource = manifestDataSourceFactory.createDataSource();
loader = new Loader("Loader:DashMediaSource");
manifestRefreshHandler = new Handler();
handler = new Handler();
startLoadingManifest();
}
@ -137,10 +158,21 @@ public final class DashMediaSource implements MediaSource {
@Override
public Position getDefaultStartPosition(int index) {
if (seekWindow == null) {
return null;
}
if (index == 0 && manifest.dynamic) {
// The stream is live, so jump to the live edge.
// TODO[playlists]: Actually jump to the live edge, rather than the start of the last period.
return new Position(periods.size() - 1, 0);
// The stream is live, so return a position a position offset from the live edge.
int periodIndex = seekWindow.endPeriodIndex;
long positionUs = seekWindow.endTimeUs - LIVE_EDGE_OFFSET_US;
while (positionUs < 0 && periodIndex > seekWindow.startPeriodIndex) {
periodIndex--;
positionUs += manifest.getPeriodDuration(periodIndex) * 1000;
}
positionUs = Math.max(positionUs,
periodIndex == seekWindow.startPeriodIndex ? seekWindow.startTimeUs : 0);
return new Position(periodIndex, positionUs);
}
return new Position(index, 0);
}
@ -164,11 +196,11 @@ public final class DashMediaSource implements MediaSource {
manifestLoadStartTimestamp = 0;
manifestLoadEndTimestamp = 0;
manifest = null;
if (manifestRefreshHandler != null) {
manifestRefreshHandler.removeCallbacksAndMessages(null);
manifestRefreshHandler = null;
if (handler != null) {
handler.removeCallbacksAndMessages(null);
handler = null;
}
elapsedRealtimeOffset = 0;
elapsedRealtimeOffsetMs = 0;
}
// Loadable callbacks.
@ -312,7 +344,7 @@ public final class DashMediaSource implements MediaSource {
}
private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) {
this.elapsedRealtimeOffset = elapsedRealtimeOffsetMs;
this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;
finishManifestProcessing();
}
@ -329,13 +361,49 @@ public final class DashMediaSource implements MediaSource {
int periodCount = manifest.getPeriodCount();
for (int i = periods.size(); i < periodCount; i++) {
periods.add(new DashMediaPeriod(manifest, i, chunkSourceFactory, minLoadableRetryCount,
eventDispatcher, elapsedRealtimeOffset, loader));
eventDispatcher, elapsedRealtimeOffsetMs, loader));
}
invalidationListener.onTimelineChanged(new DashTimeline(manifest,
periods.toArray(new DashMediaPeriod[periods.size()])));
handler.removeCallbacks(refreshSourceInfoRunnable);
refreshSourceInfo();
scheduleManifestRefresh();
}
private void refreshSourceInfo() {
// Update the seek window.
int periodCount = manifest.getPeriodCount();
int lastPeriodIndex = periodCount - 1;
PeriodSeekInfo firstPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo(manifest.getPeriod(0),
manifest.getPeriodDuration(0) * 1000);
PeriodSeekInfo lastPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo(
manifest.getPeriod(lastPeriodIndex), manifest.getPeriodDuration(lastPeriodIndex) * 1000);
long currentStartTimeUs;
long currentEndTimeUs;
if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) {
// The seek window is changing so post a Runnable to update it.
handler.postDelayed(refreshSourceInfoRunnable, NOTIFY_MANIFEST_INTERVAL_MS);
long minStartPositionUs = firstPeriodSeekInfo.availableStartTimeUs;
long maxEndPositionUs = lastPeriodSeekInfo.availableEndTimeUs;
long timeShiftBufferDepthUs = manifest.timeShiftBufferDepth == -1 ? -1
: manifest.timeShiftBufferDepth * 1000;
currentEndTimeUs = Math.min(maxEndPositionUs,
getNowUnixTimeUs() - manifest.availabilityStartTime * 1000);
currentStartTimeUs = timeShiftBufferDepthUs == -1 ? minStartPositionUs
: Math.max(minStartPositionUs, currentEndTimeUs - timeShiftBufferDepthUs);
} else {
handler.removeCallbacks(refreshSourceInfoRunnable);
currentStartTimeUs = firstPeriodSeekInfo.availableStartTimeUs;
currentEndTimeUs = lastPeriodSeekInfo.availableEndTimeUs;
}
seekWindow = new SeekWindow(0, currentStartTimeUs, lastPeriodIndex, currentEndTimeUs);
DashMediaPeriod[] mediaPeriods =
periods.toArray(new DashMediaPeriod[manifest.getPeriodCount()]);
sourceListener.onSourceInfoRefreshed(new DashTimeline(manifest, mediaPeriods, seekWindow),
manifest);
}
private void scheduleManifestRefresh() {
if (!manifest.dynamic) {
return;
@ -350,7 +418,7 @@ public final class DashMediaSource implements MediaSource {
}
long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriod;
long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime());
manifestRefreshHandler.postDelayed(new Runnable() {
handler.postDelayed(new Runnable() {
@Override
public void run() {
startLoadingManifest();
@ -364,14 +432,65 @@ public final class DashMediaSource implements MediaSource {
eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);
}
private long getNowUnixTimeUs() {
if (elapsedRealtimeOffsetMs != 0) {
return SystemClock.elapsedRealtime() * 1000 + elapsedRealtimeOffsetMs;
} else {
return System.currentTimeMillis() * 1000;
}
}
private static final class PeriodSeekInfo {
public static PeriodSeekInfo createPeriodSeekInfo(Period period, long durationUs) {
int adaptationSetCount = period.adaptationSets.size();
long availableStartTimeUs = 0;
long availableEndTimeUs = Long.MAX_VALUE;
boolean isIndexExplicit = false;
for (int i = 0; i < adaptationSetCount; i++) {
DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex();
if (index == null) {
return new PeriodSeekInfo(true, 0, durationUs);
}
int firstSegmentNum = index.getFirstSegmentNum();
int lastSegmentNum = index.getLastSegmentNum(durationUs);
isIndexExplicit |= index.isExplicit();
long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum);
availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs);
if (lastSegmentNum != DashSegmentIndex.INDEX_UNBOUNDED) {
long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum)
+ index.getDurationUs(lastSegmentNum, durationUs);
availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs);
} else {
// The available end time is unmodified, because this index is unbounded.
}
}
return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs);
}
public final boolean isIndexExplicit;
public final long availableStartTimeUs;
public final long availableEndTimeUs;
private PeriodSeekInfo(boolean isIndexExplicit, long availableStartTimeUs,
long availableEndTimeUs) {
this.isIndexExplicit = isIndexExplicit;
this.availableStartTimeUs = availableStartTimeUs;
this.availableEndTimeUs = availableEndTimeUs;
}
}
private static final class DashTimeline implements Timeline {
private final DashManifest manifest;
private final DashMediaPeriod[] periods;
private final SeekWindow[] seekWindows;
public DashTimeline(DashManifest manifest, DashMediaPeriod[] periods) {
public DashTimeline(DashManifest manifest, DashMediaPeriod[] periods, SeekWindow seekWindow) {
this.manifest = manifest;
this.periods = periods;
seekWindows = new SeekWindow[] {seekWindow};
}
@Override
@ -405,8 +524,13 @@ public final class DashMediaSource implements MediaSource {
}
@Override
public Object getManifest() {
return manifest;
public int getSeekWindowCount() {
return seekWindows.length;
}
@Override
public SeekWindow getSeekWindow(int index) {
return seekWindows[index];
}
}

View file

@ -516,7 +516,6 @@ public class DashManifestParser extends DefaultHandler
protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, String baseUrl,
SegmentTemplate parent) throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
parent != null ? parent.presentationTimeOffset : 0);

View file

@ -67,6 +67,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
private final PtsTimestampAdjusterProvider timestampAdjusterProvider;
private final HlsPlaylistParser manifestParser;
private MediaSource.Listener sourceListener;
private DataSource manifestDataSource;
private Loader manifestFetcher;
@ -75,6 +76,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
private long preparePositionUs;
private int pendingPrepareCount;
private HlsPlaylist playlist;
private boolean seenFirstTrackSelection;
private long durationUs;
private long pendingDiscontinuityPositionUs;
@ -108,9 +110,10 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
// MediaSource implementation.
@Override
public void prepareSource(InvalidationListener listener) {
public void prepareSource(MediaSource.Listener listener) {
sourceListener = listener;
// TODO: Defer until the playlist has been loaded.
listener.onTimelineChanged(new SinglePeriodTimeline(this));
listener.onSourceInfoRefreshed(SinglePeriodTimeline.createNonFinalTimeline(this), null);
}
@Override
@ -132,7 +135,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
@Override
public void releaseSource() {
// do nothing
sourceListener = null;
}
// MediaPeriod implementation.
@ -255,6 +258,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
allocator = null;
preparePositionUs = 0;
pendingPrepareCount = 0;
playlist = null;
seenFirstTrackSelection = false;
durationUs = 0;
isLive = false;
@ -277,8 +281,8 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
long loadDurationMs) {
eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
HlsPlaylist playlist = loadable.getResult();
List<HlsSampleStreamWrapper> sampleStreamWrapperList = buildSampleStreamWrappers(playlist);
playlist = loadable.getResult();
List<HlsSampleStreamWrapper> sampleStreamWrapperList = buildSampleStreamWrappers();
sampleStreamWrappers = new HlsSampleStreamWrapper[sampleStreamWrapperList.size()];
sampleStreamWrapperList.toArray(sampleStreamWrappers);
selectedTrackCounts = new int[sampleStreamWrappers.length];
@ -330,6 +334,12 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
}
trackGroups = new TrackGroupArray(trackGroupArray);
callback.onPeriodPrepared(this);
// TODO[playlists]: Calculate the seek window.
Timeline timeline =
isLive ? SinglePeriodTimeline.createUnseekableFinalTimeline(this, durationUs)
: SinglePeriodTimeline.createSeekableFinalTimeline(this, durationUs);
sourceListener.onSourceInfoRefreshed(timeline, playlist);
}
@Override
@ -343,7 +353,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
// Internal methods.
private List<HlsSampleStreamWrapper> buildSampleStreamWrappers(HlsPlaylist playlist) {
private List<HlsSampleStreamWrapper> buildSampleStreamWrappers() {
ArrayList<HlsSampleStreamWrapper> sampleStreamWrappers = new ArrayList<>();
String baseUri = playlist.baseUri;

View file

@ -55,7 +55,7 @@ public final class SsMediaSource implements MediaSource,
private final EventDispatcher eventDispatcher;
private final SsManifestParser manifestParser;
private MediaSource.InvalidationListener invalidationListener;
private MediaSource.Listener sourceListener;
private DataSource manifestDataSource;
private Loader manifestLoader;
@ -87,8 +87,8 @@ public final class SsMediaSource implements MediaSource,
// MediaSource implementation.
@Override
public void prepareSource(InvalidationListener listener) {
this.invalidationListener = listener;
public void prepareSource(MediaSource.Listener listener) {
sourceListener = listener;
manifestDataSource = dataSourceFactory.createDataSource();
manifestLoader = new Loader("Loader:Manifest");
manifestRefreshHandler = new Handler();
@ -114,6 +114,7 @@ public final class SsMediaSource implements MediaSource,
@Override
public void releaseSource() {
sourceListener = null;
period = null;
manifest = null;
manifestDataSource = null;
@ -140,13 +141,13 @@ public final class SsMediaSource implements MediaSource,
if (period == null) {
period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount,
eventDispatcher, manifestLoader);
Timeline timeline = manifest.durationUs == C.UNSET_TIME_US
? new SinglePeriodTimeline(this, manifest)
: new SinglePeriodTimeline(this, manifest, manifest.durationUs / 1000);
invalidationListener.onTimelineChanged(timeline);
} else {
period.updateManifest(manifest);
}
Timeline timeline = manifest.isLive || manifest.durationUs == C.UNSET_TIME_US
? SinglePeriodTimeline.createUnseekableFinalTimeline(this, C.UNSET_TIME_US)
: SinglePeriodTimeline.createSeekableFinalTimeline(this, manifest.durationUs);
sourceListener.onSourceInfoRefreshed(timeline, manifest);
scheduleManifestRefresh();
}

View file

@ -162,7 +162,7 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe
}
@Override
public void onTimelineChanged(Timeline timeline) {
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
// Do nothing.
}

View file

@ -217,7 +217,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen
}
@Override
public final void onTimelineChanged(Timeline timeline) {
public final void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
// Do nothing.
}