mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Support media source events for composite media sources.
This is achieved by automatically registering a listener for child sources in compositions. The composite media source has the chance to correct the reported window index and media period id in the MediaLoadData of the events. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=189575414
This commit is contained in:
parent
6b527da462
commit
e3a90a44b7
@ -21,6 +21,9 @@
|
||||
deprecated `DynamicConcatenatingMediaSource`.
|
||||
* Allow clipping of child media sources where the period and window have a
|
||||
non-zero offset with `ClippingMediaSource`.
|
||||
* Allow adding and removing `MediaSourceEventListener`s to MediaSources after
|
||||
they have been created. Listening to events is now supported for all
|
||||
media sources including composite sources.
|
||||
* Audio: Factor out `AudioTrack` position tracking from `DefaultAudioSink`.
|
||||
* Caching:
|
||||
* Add release method to Cache interface.
|
||||
|
@ -342,7 +342,6 @@ public class PlayerActivity extends Activity
|
||||
MediaSource[] mediaSources = new MediaSource[uris.length];
|
||||
for (int i = 0; i < uris.length; i++) {
|
||||
mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
|
||||
mediaSources[i].addEventListener(mainHandler, eventLogger);
|
||||
}
|
||||
mediaSource =
|
||||
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
|
||||
@ -362,6 +361,7 @@ public class PlayerActivity extends Activity
|
||||
} else {
|
||||
releaseAdsLoader();
|
||||
}
|
||||
mediaSource.addEventListener(mainHandler, eventLogger);
|
||||
}
|
||||
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
|
||||
if (haveResumePosition) {
|
||||
|
@ -190,6 +190,19 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getMediaTimeForChildMediaTime(Void id, long mediaTimeMs) {
|
||||
if (mediaTimeMs == C.TIME_UNSET) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
long startMs = C.usToMs(startUs);
|
||||
long clippedTimeMs = Math.max(0, mediaTimeMs - startMs);
|
||||
if (endUs != C.TIME_END_OF_SOURCE) {
|
||||
clippedTimeMs = Math.min(C.usToMs(endUs) - startMs, clippedTimeMs);
|
||||
}
|
||||
return clippedTimeMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a clipped view of a specified timeline.
|
||||
*/
|
||||
|
@ -15,10 +15,12 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.CallSuper;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
@ -31,7 +33,9 @@ import java.util.HashMap;
|
||||
public abstract class CompositeMediaSource<T> extends BaseMediaSource {
|
||||
|
||||
private final HashMap<T, MediaSourceAndListener> childSources;
|
||||
|
||||
private ExoPlayer player;
|
||||
private Handler eventHandler;
|
||||
|
||||
/** Create composite media source without child sources. */
|
||||
protected CompositeMediaSource() {
|
||||
@ -42,6 +46,7 @@ public abstract class CompositeMediaSource<T> extends BaseMediaSource {
|
||||
@CallSuper
|
||||
public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
|
||||
this.player = player;
|
||||
eventHandler = new Handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -57,6 +62,7 @@ public abstract class CompositeMediaSource<T> extends BaseMediaSource {
|
||||
public void releaseSourceInternal() {
|
||||
for (MediaSourceAndListener childSource : childSources.values()) {
|
||||
childSource.mediaSource.releaseSource(childSource.listener);
|
||||
childSource.mediaSource.removeEventListener(childSource.eventListener);
|
||||
}
|
||||
childSources.clear();
|
||||
player = null;
|
||||
@ -96,7 +102,9 @@ public abstract class CompositeMediaSource<T> extends BaseMediaSource {
|
||||
onChildSourceInfoRefreshed(id, source, timeline, manifest);
|
||||
}
|
||||
};
|
||||
childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener));
|
||||
MediaSourceEventListener eventListener = new ForwardingEventListener(id);
|
||||
childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener));
|
||||
mediaSource.addEventListener(eventHandler, eventListener);
|
||||
mediaSource.prepareSource(player, /* isTopLevelSource= */ false, sourceListener);
|
||||
}
|
||||
|
||||
@ -108,16 +116,148 @@ public abstract class CompositeMediaSource<T> extends BaseMediaSource {
|
||||
protected final void releaseChildSource(@Nullable T id) {
|
||||
MediaSourceAndListener removedChild = childSources.remove(id);
|
||||
removedChild.mediaSource.releaseSource(removedChild.listener);
|
||||
removedChild.mediaSource.removeEventListener(removedChild.eventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the window index in the composite source corresponding to the specified window index in
|
||||
* a child source. The default implementation does not change the window index.
|
||||
*
|
||||
* @param id The unique id used to prepare the child source.
|
||||
* @param windowIndex A window index of the child source.
|
||||
* @return The corresponding window index in the composite source.
|
||||
*/
|
||||
protected int getWindowIndexForChildWindowIndex(@Nullable T id, int windowIndex) {
|
||||
return windowIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link MediaPeriodId} in the composite source corresponding to the specified {@link
|
||||
* MediaPeriodId} in a child source. The default implementation does not change the media period
|
||||
* id.
|
||||
*
|
||||
* @param id The unique id used to prepare the child source.
|
||||
* @param mediaPeriodId A {@link MediaPeriodId} of the child source.
|
||||
* @return The corresponding {@link MediaPeriodId} in the composite source. Null if no
|
||||
* corresponding media period id can be determined.
|
||||
*/
|
||||
protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
|
||||
@Nullable T id, MediaPeriodId mediaPeriodId) {
|
||||
return mediaPeriodId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the media time in the composite source corresponding to the specified media time in a
|
||||
* child source. The default implementation does not change the media time.
|
||||
*
|
||||
* @param id The unique id used to prepare the child source.
|
||||
* @param mediaTimeMs A media time of the child source, in milliseconds.
|
||||
* @return The corresponding media time in the composite source, in milliseconds.
|
||||
*/
|
||||
protected long getMediaTimeForChildMediaTime(@Nullable T id, long mediaTimeMs) {
|
||||
return mediaTimeMs;
|
||||
}
|
||||
|
||||
private static final class MediaSourceAndListener {
|
||||
|
||||
public final MediaSource mediaSource;
|
||||
public final SourceInfoRefreshListener listener;
|
||||
public final MediaSourceEventListener eventListener;
|
||||
|
||||
public MediaSourceAndListener(MediaSource mediaSource, SourceInfoRefreshListener listener) {
|
||||
public MediaSourceAndListener(
|
||||
MediaSource mediaSource,
|
||||
SourceInfoRefreshListener listener,
|
||||
MediaSourceEventListener eventListener) {
|
||||
this.mediaSource = mediaSource;
|
||||
this.listener = listener;
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
}
|
||||
|
||||
private final class ForwardingEventListener implements MediaSourceEventListener {
|
||||
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final @Nullable T id;
|
||||
|
||||
public ForwardingEventListener(@Nullable T id) {
|
||||
this.eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadStarted(LoadEventInfo loadEventData, MediaLoadData mediaLoadData) {
|
||||
MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData);
|
||||
if (correctedMediaLoadData != null) {
|
||||
eventDispatcher.loadStarted(loadEventData, correctedMediaLoadData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(LoadEventInfo loadEventData, MediaLoadData mediaLoadData) {
|
||||
MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData);
|
||||
if (correctedMediaLoadData != null) {
|
||||
eventDispatcher.loadCompleted(loadEventData, correctedMediaLoadData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(LoadEventInfo loadEventData, MediaLoadData mediaLoadData) {
|
||||
MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData);
|
||||
if (correctedMediaLoadData != null) {
|
||||
eventDispatcher.loadCanceled(loadEventData, correctedMediaLoadData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(
|
||||
LoadEventInfo loadEventData,
|
||||
MediaLoadData mediaLoadData,
|
||||
IOException error,
|
||||
boolean wasCanceled) {
|
||||
MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData);
|
||||
if (correctedMediaLoadData != null) {
|
||||
eventDispatcher.loadError(loadEventData, correctedMediaLoadData, error, wasCanceled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpstreamDiscarded(MediaLoadData mediaLoadData) {
|
||||
MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData);
|
||||
if (correctedMediaLoadData != null) {
|
||||
eventDispatcher.upstreamDiscarded(correctedMediaLoadData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownstreamFormatChanged(MediaLoadData mediaLoadData) {
|
||||
MediaLoadData correctedMediaLoadData = correctMediaLoadData(mediaLoadData);
|
||||
if (correctedMediaLoadData != null) {
|
||||
eventDispatcher.downstreamFormatChanged(correctedMediaLoadData);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable MediaLoadData correctMediaLoadData(MediaLoadData mediaLoadData) {
|
||||
MediaPeriodId mediaPeriodId = null;
|
||||
if (mediaLoadData.mediaPeriodId != null) {
|
||||
mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, mediaLoadData.mediaPeriodId);
|
||||
if (mediaPeriodId == null) {
|
||||
// Media period not found. Ignore event.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
int windowIndex = getWindowIndexForChildWindowIndex(id, mediaLoadData.windowIndex);
|
||||
long mediaStartTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaStartTimeMs);
|
||||
long mediaEndTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaEndTimeMs);
|
||||
return new MediaLoadData(
|
||||
windowIndex,
|
||||
mediaPeriodId,
|
||||
mediaLoadData.dataType,
|
||||
mediaLoadData.trackType,
|
||||
mediaLoadData.trackFormat,
|
||||
mediaLoadData.trackSelectionReason,
|
||||
mediaLoadData.trackSelectionData,
|
||||
mediaStartTimeMs,
|
||||
mediaEndTimeMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -435,6 +435,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
updateMediaSourceInternal(mediaSourceHolder, timeline);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
|
||||
MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) {
|
||||
for (int i = 0; i < mediaSourceHolder.activeMediaPeriods.size(); i++) {
|
||||
// Ensure the reported media period id has the same window sequence number as the one created
|
||||
// by this media source. Otherwise it does not belong to this child source.
|
||||
if (mediaSourceHolder.activeMediaPeriods.get(i).id.windowSequenceNumber
|
||||
== mediaPeriodId.windowSequenceNumber) {
|
||||
return mediaPeriodId.copyWithPeriodIndex(
|
||||
mediaPeriodId.periodIndex + mediaSourceHolder.firstPeriodIndexInChild);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getWindowIndexForChildWindowIndex(
|
||||
MediaSourceHolder mediaSourceHolder, int windowIndex) {
|
||||
return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public final void handleMessage(int messageType, Object message) throws ExoPlaybackException {
|
||||
|
@ -40,8 +40,8 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
||||
}
|
||||
|
||||
public final MediaSource mediaSource;
|
||||
public final MediaPeriodId id;
|
||||
|
||||
private final MediaPeriodId id;
|
||||
private final Allocator allocator;
|
||||
|
||||
private MediaPeriod mediaPeriod;
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.CheckResult;
|
||||
import android.support.annotation.Nullable;
|
||||
@ -322,9 +323,9 @@ public interface MediaSourceEventListener {
|
||||
/** Dispatches {@link #onLoadStarted(LoadEventInfo, MediaLoadData)}. */
|
||||
public void loadStarted(final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
|
||||
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
|
||||
Handler handler = listenerAndHandler.handler;
|
||||
final MediaSourceEventListener listener = listenerAndHandler.listener;
|
||||
handler.post(
|
||||
postOrRun(
|
||||
listenerAndHandler.handler,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@ -386,9 +387,9 @@ public interface MediaSourceEventListener {
|
||||
public void loadCompleted(
|
||||
final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
|
||||
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
|
||||
Handler handler = listenerAndHandler.handler;
|
||||
final MediaSourceEventListener listener = listenerAndHandler.listener;
|
||||
handler.post(
|
||||
postOrRun(
|
||||
listenerAndHandler.handler,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@ -449,9 +450,9 @@ public interface MediaSourceEventListener {
|
||||
/** Dispatches {@link #onLoadCanceled(LoadEventInfo, MediaLoadData)}. */
|
||||
public void loadCanceled(final LoadEventInfo loadEventInfo, final MediaLoadData mediaLoadData) {
|
||||
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
|
||||
Handler handler = listenerAndHandler.handler;
|
||||
final MediaSourceEventListener listener = listenerAndHandler.listener;
|
||||
handler.post(
|
||||
postOrRun(
|
||||
listenerAndHandler.handler,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@ -524,9 +525,9 @@ public interface MediaSourceEventListener {
|
||||
final IOException error,
|
||||
final boolean wasCanceled) {
|
||||
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
|
||||
Handler handler = listenerAndHandler.handler;
|
||||
final MediaSourceEventListener listener = listenerAndHandler.listener;
|
||||
handler.post(
|
||||
postOrRun(
|
||||
listenerAndHandler.handler,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@ -554,9 +555,9 @@ public interface MediaSourceEventListener {
|
||||
/** Dispatches {@link #onUpstreamDiscarded(MediaLoadData)}. */
|
||||
public void upstreamDiscarded(final MediaLoadData mediaLoadData) {
|
||||
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
|
||||
Handler handler = listenerAndHandler.handler;
|
||||
final MediaSourceEventListener listener = listenerAndHandler.listener;
|
||||
handler.post(
|
||||
postOrRun(
|
||||
listenerAndHandler.handler,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@ -589,9 +590,9 @@ public interface MediaSourceEventListener {
|
||||
/** Dispatches {@link #onDownstreamFormatChanged(MediaLoadData)}. */
|
||||
public void downstreamFormatChanged(final MediaLoadData mediaLoadData) {
|
||||
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
|
||||
Handler handler = listenerAndHandler.handler;
|
||||
final MediaSourceEventListener listener = listenerAndHandler.listener;
|
||||
handler.post(
|
||||
postOrRun(
|
||||
listenerAndHandler.handler,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@ -606,6 +607,14 @@ public interface MediaSourceEventListener {
|
||||
return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
|
||||
}
|
||||
|
||||
private void postOrRun(Handler handler, Runnable runnable) {
|
||||
if (handler.getLooper() == Looper.myLooper()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
handler.post(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ListenerAndHandler {
|
||||
|
||||
public final Handler handler;
|
||||
|
@ -303,6 +303,14 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
|
||||
MediaPeriodId childId, MediaPeriodId mediaPeriodId) {
|
||||
// The child id for the content period is just a dummy without window sequence number. That's
|
||||
// why we need to forward the reported mediaPeriodId in this case.
|
||||
return childId.isAd() ? childId : mediaPeriodId;
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void onAdPlaybackState(AdPlaybackState adPlaybackState) {
|
||||
|
@ -18,18 +18,24 @@ package com.google.android.exoplayer2.source;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.os.Handler;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
import com.google.android.exoplayer2.Timeline.Window;
|
||||
import com.google.android.exoplayer2.source.ClippingMediaSource.IllegalClippingException;
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaPeriod;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import java.io.IOException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@ -187,13 +193,134 @@ public final class ClippingMediaSourceTest {
|
||||
TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEventTimeWithinClippedRange() throws IOException {
|
||||
MediaLoadData mediaLoadData =
|
||||
getClippingMediaSourceMediaLoadData(
|
||||
/* clippingStartUs= */ TEST_CLIP_AMOUNT_US,
|
||||
/* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
|
||||
/* eventStartUs= */ TEST_CLIP_AMOUNT_US + 1000,
|
||||
/* eventEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US - 1000);
|
||||
assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(1000);
|
||||
assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs))
|
||||
.isEqualTo(TEST_PERIOD_DURATION_US - 2 * TEST_CLIP_AMOUNT_US - 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEventTimeOutsideClippedRange() throws IOException {
|
||||
MediaLoadData mediaLoadData =
|
||||
getClippingMediaSourceMediaLoadData(
|
||||
/* clippingStartUs= */ TEST_CLIP_AMOUNT_US,
|
||||
/* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
|
||||
/* eventStartUs= */ TEST_CLIP_AMOUNT_US - 1000,
|
||||
/* eventEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US + 1000);
|
||||
assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(0);
|
||||
assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs))
|
||||
.isEqualTo(TEST_PERIOD_DURATION_US - 2 * TEST_CLIP_AMOUNT_US);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnsetEventTime() throws IOException {
|
||||
MediaLoadData mediaLoadData =
|
||||
getClippingMediaSourceMediaLoadData(
|
||||
/* clippingStartUs= */ TEST_CLIP_AMOUNT_US,
|
||||
/* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
|
||||
/* eventStartUs= */ C.TIME_UNSET,
|
||||
/* eventEndUs= */ C.TIME_UNSET);
|
||||
assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(C.TIME_UNSET);
|
||||
assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)).isEqualTo(C.TIME_UNSET);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEventTimeWithUnsetDuration() throws IOException {
|
||||
MediaLoadData mediaLoadData =
|
||||
getClippingMediaSourceMediaLoadData(
|
||||
/* clippingStartUs= */ TEST_CLIP_AMOUNT_US,
|
||||
/* clippingEndUs= */ C.TIME_END_OF_SOURCE,
|
||||
/* eventStartUs= */ TEST_CLIP_AMOUNT_US,
|
||||
/* eventEndUs= */ TEST_CLIP_AMOUNT_US + 1_000_000);
|
||||
assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(0);
|
||||
assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)).isEqualTo(1_000_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a timeline of duration {@link #TEST_PERIOD_DURATION_US} in a {@link ClippingMediaSource},
|
||||
* sends a media source event from the child source and returns the reported {@link MediaLoadData}
|
||||
* for the clipping media source.
|
||||
*
|
||||
* @param clippingStartUs The start time of the media source clipping, in microseconds.
|
||||
* @param clippingEndUs The end time of the media source clipping, in microseconds.
|
||||
* @param eventStartUs The start time of the media source event (before clipping), in
|
||||
* microseconds.
|
||||
* @param eventEndUs The end time of the media source event (before clipping), in microseconds.
|
||||
* @return The reported {@link MediaLoadData} for that event.
|
||||
*/
|
||||
private static MediaLoadData getClippingMediaSourceMediaLoadData(
|
||||
long clippingStartUs, long clippingEndUs, final long eventStartUs, final long eventEndUs)
|
||||
throws IOException {
|
||||
FakeMediaSource fakeMediaSource =
|
||||
new FakeMediaSource(
|
||||
new SinglePeriodTimeline(
|
||||
TEST_PERIOD_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false),
|
||||
/* manifest= */ null) {
|
||||
@Override
|
||||
protected FakeMediaPeriod createFakeMediaPeriod(
|
||||
MediaPeriodId id,
|
||||
TrackGroupArray trackGroupArray,
|
||||
Allocator allocator,
|
||||
EventDispatcher eventDispatcher) {
|
||||
eventDispatcher.downstreamFormatChanged(
|
||||
new MediaLoadData(
|
||||
/* windowIndex= */ 0,
|
||||
id,
|
||||
C.DATA_TYPE_MEDIA,
|
||||
C.TRACK_TYPE_UNKNOWN,
|
||||
/* trackFormat= */ null,
|
||||
C.SELECTION_REASON_UNKNOWN,
|
||||
/* trackSelectionData= */ null,
|
||||
C.usToMs(eventStartUs),
|
||||
C.usToMs(eventEndUs)));
|
||||
return super.createFakeMediaPeriod(id, trackGroupArray, allocator, eventDispatcher);
|
||||
}
|
||||
};
|
||||
final ClippingMediaSource clippingMediaSource =
|
||||
new ClippingMediaSource(fakeMediaSource, clippingStartUs, clippingEndUs);
|
||||
MediaSourceTestRunner testRunner =
|
||||
new MediaSourceTestRunner(clippingMediaSource, /* allocator= */ null);
|
||||
final MediaLoadData[] reportedMediaLoadData = new MediaLoadData[1];
|
||||
try {
|
||||
testRunner.runOnPlaybackThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
clippingMediaSource.addEventListener(
|
||||
new Handler(),
|
||||
new DefaultMediaSourceEventListener() {
|
||||
@Override
|
||||
public void onDownstreamFormatChanged(MediaLoadData mediaLoadData) {
|
||||
reportedMediaLoadData[0] = mediaLoadData;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
testRunner.prepareSource();
|
||||
// Create period to send the test event configured above.
|
||||
testRunner.createPeriod(
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0));
|
||||
assertThat(reportedMediaLoadData[0]).isNotNull();
|
||||
} finally {
|
||||
testRunner.release();
|
||||
}
|
||||
return reportedMediaLoadData[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
|
||||
*/
|
||||
private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs)
|
||||
private static Timeline getClippedTimeline(Timeline timeline, long startUs, long endUs)
|
||||
throws IOException {
|
||||
FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);
|
||||
ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs);
|
||||
ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startUs, endUs);
|
||||
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
|
||||
try {
|
||||
Timeline clippedTimeline = testRunner.prepareSource();
|
||||
|
@ -34,6 +34,7 @@ import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import org.junit.After;
|
||||
@ -134,6 +135,10 @@ public final class ConcatenatingMediaSourceTest {
|
||||
childSources[i].assertReleased();
|
||||
}
|
||||
|
||||
// Assert the correct child source preparation load events have been returned (with the
|
||||
// respective window index at the time of preparation).
|
||||
testRunner.assertCompletedManifestLoads(0, 0, 2, 1, 3, 4, 5);
|
||||
|
||||
// Assert correct next and previous indices behavior after some insertions and removals.
|
||||
TimelineAsserts.assertNextWindowIndices(
|
||||
timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET);
|
||||
@ -156,8 +161,9 @@ public final class ConcatenatingMediaSourceTest {
|
||||
assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(timeline.getWindowCount() - 1);
|
||||
assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0);
|
||||
|
||||
// Assert all periods can be prepared.
|
||||
// Assert all periods can be prepared and the respective load events are returned.
|
||||
testRunner.assertPrepareAndReleaseAllPeriods();
|
||||
assertCompletedAllMediaPeriodLoads(timeline);
|
||||
|
||||
// Remove at front of queue.
|
||||
mediaSource.removeMediaSource(0);
|
||||
@ -205,6 +211,8 @@ public final class ConcatenatingMediaSourceTest {
|
||||
timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET);
|
||||
|
||||
testRunner.assertPrepareAndReleaseAllPeriods();
|
||||
testRunner.assertCompletedManifestLoads(0, 1, 2);
|
||||
assertCompletedAllMediaPeriodLoads(timeline);
|
||||
testRunner.releaseSource();
|
||||
for (int i = 1; i < 4; i++) {
|
||||
childSources[i].assertReleased();
|
||||
@ -250,6 +258,8 @@ public final class ConcatenatingMediaSourceTest {
|
||||
TimelineAsserts.assertWindowIds(timeline, 111, 999);
|
||||
TimelineAsserts.assertWindowIsDynamic(timeline, false, false);
|
||||
testRunner.assertPrepareAndReleaseAllPeriods();
|
||||
testRunner.assertCompletedManifestLoads(0, 1);
|
||||
assertCompletedAllMediaPeriodLoads(timeline);
|
||||
|
||||
// Add further lazy and normal sources after preparation. Also remove one lazy source again to
|
||||
// check it doesn't throw or change the result.
|
||||
@ -325,6 +335,7 @@ public final class ConcatenatingMediaSourceTest {
|
||||
}));
|
||||
timeline = testRunner.assertTimelineChangeBlocking();
|
||||
TimelineAsserts.assertEmpty(timeline);
|
||||
testRunner.assertCompletedManifestLoads(/* empty */ );
|
||||
|
||||
// Insert non-empty media source to leave empty sources at the start, the end, and the middle
|
||||
// (with single and multiple empty sources in a row).
|
||||
@ -358,6 +369,8 @@ public final class ConcatenatingMediaSourceTest {
|
||||
assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2);
|
||||
assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0);
|
||||
testRunner.assertPrepareAndReleaseAllPeriods();
|
||||
testRunner.assertCompletedManifestLoads(0, 1, 2);
|
||||
assertCompletedAllMediaPeriodLoads(timeline);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -659,6 +672,8 @@ public final class ConcatenatingMediaSourceTest {
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
/* windowSequenceNumber= */ 1));
|
||||
testRunner.assertCompletedManifestLoads(0, 1);
|
||||
assertCompletedAllMediaPeriodLoads(timeline);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -787,6 +802,9 @@ public final class ConcatenatingMediaSourceTest {
|
||||
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3),
|
||||
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5),
|
||||
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 7));
|
||||
// Assert that only one manifest load is reported because the source is reused.
|
||||
testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0);
|
||||
assertCompletedAllMediaPeriodLoads(timeline);
|
||||
|
||||
testRunner.releaseSource();
|
||||
childSource.assertReleased();
|
||||
@ -816,6 +834,9 @@ public final class ConcatenatingMediaSourceTest {
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2),
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 3),
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4));
|
||||
// Assert that only one manifest load is needed because the source is reused.
|
||||
testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0);
|
||||
assertCompletedAllMediaPeriodLoads(timeline);
|
||||
|
||||
testRunner.releaseSource();
|
||||
childSource.assertReleased();
|
||||
@ -874,6 +895,47 @@ public final class ConcatenatingMediaSourceTest {
|
||||
assertThat(newPeriodId2).isEqualTo(periodId0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChildTimelineChangeWithActiveMediaPeriod() throws IOException {
|
||||
FakeMediaSource[] nestedChildSources = createMediaSources(/* count= */ 2);
|
||||
ConcatenatingMediaSource childSource = new ConcatenatingMediaSource(nestedChildSources);
|
||||
mediaSource.addMediaSource(childSource);
|
||||
|
||||
testRunner.prepareSource();
|
||||
MediaPeriod mediaPeriod =
|
||||
testRunner.createPeriod(
|
||||
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0));
|
||||
childSource.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 1);
|
||||
testRunner.assertTimelineChangeBlocking();
|
||||
testRunner.preparePeriod(mediaPeriod, /* positionUs= */ 0);
|
||||
|
||||
testRunner.assertCompletedMediaPeriodLoads(
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0));
|
||||
}
|
||||
|
||||
private void assertCompletedAllMediaPeriodLoads(Timeline timeline) {
|
||||
Timeline.Period period = new Timeline.Period();
|
||||
Timeline.Window window = new Timeline.Window();
|
||||
ArrayList<MediaPeriodId> expectedMediaPeriodIds = new ArrayList<>();
|
||||
for (int windowIndex = 0; windowIndex < timeline.getWindowCount(); windowIndex++) {
|
||||
timeline.getWindow(windowIndex, window);
|
||||
for (int periodIndex = window.firstPeriodIndex;
|
||||
periodIndex <= window.lastPeriodIndex;
|
||||
periodIndex++) {
|
||||
timeline.getPeriod(periodIndex, period);
|
||||
expectedMediaPeriodIds.add(new MediaPeriodId(periodIndex, windowIndex));
|
||||
for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) {
|
||||
for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) {
|
||||
expectedMediaPeriodIds.add(
|
||||
new MediaPeriodId(periodIndex, adGroupIndex, adIndex, windowIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
testRunner.assertCompletedMediaPeriodLoads(
|
||||
expectedMediaPeriodIds.toArray(new MediaPeriodId[0]));
|
||||
}
|
||||
|
||||
private static FakeMediaSource[] createMediaSources(int count) {
|
||||
FakeMediaSource[] sources = new FakeMediaSource[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user