mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Propagate events from secondary children in MergingMediaSource
These events are always reported with the primary child period ID, because this is the same ID used in the parent `MergingMediaSource`'s Timeline. This ensures that e.g. loading errors from sideloaded subtitles (which uses `MergingMediaSource`) are now reported via `AnalyticsListener.onLoadError`. It results in non-error events being reported from these children too, which will result in more `onLoadStarted` and `onLoadCompleted` events being reported (one for each child). Issue: androidx/media#1722 PiperOrigin-RevId: 686901439
This commit is contained in:
parent
b3290eff10
commit
191bc094a5
@ -59,6 +59,12 @@
|
|||||||
The new
|
The new
|
||||||
`DefaultLoadControl.calculateTargetBufferBytes(ExoTrackSelection[])`
|
`DefaultLoadControl.calculateTargetBufferBytes(ExoTrackSelection[])`
|
||||||
should be used instead.
|
should be used instead.
|
||||||
|
* Report `MediaSourceEventListener` events from secondary sources in
|
||||||
|
`MergingMediaSource`. This will result in load
|
||||||
|
start/error/cancelled/completed events being reported for sideloaded
|
||||||
|
subtitles (those added with
|
||||||
|
`MediaItem.LocalConfiguration.subtitleConfigurations`), which may appear
|
||||||
|
as duplicate load events emitted from `AnalyticsListener`.
|
||||||
* Transformer:
|
* Transformer:
|
||||||
* Make setting the image duration using
|
* Make setting the image duration using
|
||||||
`MediaItem.Builder.setImageDurationMs` mandatory for image export.
|
`MediaItem.Builder.setImageDurationMs` mandatory for image export.
|
||||||
|
@ -38,6 +38,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,6 +85,7 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
|||||||
private final boolean adjustPeriodTimeOffsets;
|
private final boolean adjustPeriodTimeOffsets;
|
||||||
private final boolean clipDurations;
|
private final boolean clipDurations;
|
||||||
private final MediaSource[] mediaSources;
|
private final MediaSource[] mediaSources;
|
||||||
|
private final List<List<MediaPeriodAndId>> mediaPeriods;
|
||||||
private final Timeline[] timelines;
|
private final Timeline[] timelines;
|
||||||
private final ArrayList<MediaSource> pendingTimelineSources;
|
private final ArrayList<MediaSource> pendingTimelineSources;
|
||||||
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||||
@ -161,6 +163,10 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
|||||||
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
||||||
pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources));
|
pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources));
|
||||||
periodCount = PERIOD_COUNT_UNSET;
|
periodCount = PERIOD_COUNT_UNSET;
|
||||||
|
this.mediaPeriods = new ArrayList<>(mediaSources.length);
|
||||||
|
for (int i = 0; i < mediaSources.length; i++) {
|
||||||
|
mediaPeriods.add(new ArrayList<>());
|
||||||
|
}
|
||||||
timelines = new Timeline[mediaSources.length];
|
timelines = new Timeline[mediaSources.length];
|
||||||
periodTimeOffsetsUs = new long[0][];
|
periodTimeOffsetsUs = new long[0][];
|
||||||
clippedDurationsUs = new HashMap<>();
|
clippedDurationsUs = new HashMap<>();
|
||||||
@ -203,11 +209,11 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
|||||||
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
|
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
|
||||||
int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
|
int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
|
||||||
for (int i = 0; i < periods.length; i++) {
|
for (int i = 0; i < periods.length; i++) {
|
||||||
MediaPeriodId childMediaPeriodId =
|
MediaPeriodId mediaPeriodId = id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
|
||||||
id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
|
|
||||||
periods[i] =
|
periods[i] =
|
||||||
mediaSources[i].createPeriod(
|
mediaSources[i].createPeriod(
|
||||||
childMediaPeriodId, allocator, startPositionUs - periodTimeOffsetsUs[periodIndex][i]);
|
mediaPeriodId, allocator, startPositionUs - periodTimeOffsetsUs[periodIndex][i]);
|
||||||
|
mediaPeriods.get(i).add(new MediaPeriodAndId(mediaPeriodId, periods[i]));
|
||||||
}
|
}
|
||||||
MediaPeriod mediaPeriod =
|
MediaPeriod mediaPeriod =
|
||||||
new MergingMediaPeriod(
|
new MergingMediaPeriod(
|
||||||
@ -238,6 +244,13 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
|||||||
}
|
}
|
||||||
MergingMediaPeriod mergingPeriod = (MergingMediaPeriod) mediaPeriod;
|
MergingMediaPeriod mergingPeriod = (MergingMediaPeriod) mediaPeriod;
|
||||||
for (int i = 0; i < mediaSources.length; i++) {
|
for (int i = 0; i < mediaSources.length; i++) {
|
||||||
|
List<MediaPeriodAndId> mediaPeriodsForSource = mediaPeriods.get(i);
|
||||||
|
for (int j = 0; j < mediaPeriodsForSource.size(); j++) {
|
||||||
|
if (mediaPeriodsForSource.get(j).mediaPeriod.equals(mediaPeriod)) {
|
||||||
|
mediaPeriodsForSource.remove(j);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
mediaSources[i].releasePeriod(mergingPeriod.getChildPeriod(i));
|
mediaSources[i].releasePeriod(mergingPeriod.getChildPeriod(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -286,7 +299,13 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
|||||||
@Nullable
|
@Nullable
|
||||||
protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
|
protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
|
||||||
Integer childSourceId, MediaPeriodId mediaPeriodId) {
|
Integer childSourceId, MediaPeriodId mediaPeriodId) {
|
||||||
return childSourceId == 0 ? mediaPeriodId : null;
|
List<MediaPeriodAndId> childMediaPeriodIds = mediaPeriods.get(childSourceId);
|
||||||
|
for (int i = 0; i < childMediaPeriodIds.size(); i++) {
|
||||||
|
if (childMediaPeriodIds.get(i).mediaPeriodId.equals(mediaPeriodId)) {
|
||||||
|
return mediaPeriods.get(0).get(i).mediaPeriodId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void computePeriodTimeOffsets() {
|
private void computePeriodTimeOffsets() {
|
||||||
@ -370,4 +389,14 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
|||||||
return period;
|
return period;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class MediaPeriodAndId {
|
||||||
|
private final MediaPeriodId mediaPeriodId;
|
||||||
|
private final MediaPeriod mediaPeriod;
|
||||||
|
|
||||||
|
private MediaPeriodAndId(MediaPeriodId mediaPeriodId, MediaPeriod mediaPeriod) {
|
||||||
|
this.mediaPeriodId = mediaPeriodId;
|
||||||
|
this.mediaPeriod = mediaPeriod;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,18 +15,28 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.source;
|
package androidx.media3.exoplayer.source;
|
||||||
|
|
||||||
|
import static androidx.media3.test.utils.robolectric.RobolectricUtil.DEFAULT_TIMEOUT_MS;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Timeline;
|
import androidx.media3.common.Timeline;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
|
||||||
import androidx.media3.exoplayer.source.MergingMediaSource.IllegalMergeException;
|
import androidx.media3.exoplayer.source.MergingMediaSource.IllegalMergeException;
|
||||||
|
import androidx.media3.test.utils.FakeMediaPeriod;
|
||||||
import androidx.media3.test.utils.FakeMediaSource;
|
import androidx.media3.test.utils.FakeMediaSource;
|
||||||
import androidx.media3.test.utils.FakeTimeline;
|
import androidx.media3.test.utils.FakeTimeline;
|
||||||
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
|
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
|
||||||
import androidx.media3.test.utils.MediaSourceTestRunner;
|
import androidx.media3.test.utils.MediaSourceTestRunner;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.common.collect.ConcurrentHashMultiset;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.Multiset;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
@ -112,6 +122,102 @@ public class MergingMediaSourceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that events from all child sources are propagated, but always reported with a {@link
|
||||||
|
* MediaPeriodId} that can be resolved against the {@link Timeline} exposed by the parent {@link
|
||||||
|
* MergingMediaSource} (these are period IDs from the first child source).
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void eventsFromAllChildrenPropagated_alwaysAssociatedWithPrimaryPeriodId()
|
||||||
|
throws Exception {
|
||||||
|
Multiset<Object> onLoadStartedMediaPeriodUids = ConcurrentHashMultiset.create();
|
||||||
|
Multiset<Object> onLoadCompletedMediaPeriodUids = ConcurrentHashMultiset.create();
|
||||||
|
MediaSourceEventListener mediaSourceEventListener =
|
||||||
|
new MediaSourceEventListener() {
|
||||||
|
@Override
|
||||||
|
public void onLoadStarted(
|
||||||
|
int windowIndex,
|
||||||
|
@Nullable MediaPeriodId mediaPeriodId,
|
||||||
|
LoadEventInfo loadEventInfo,
|
||||||
|
MediaLoadData mediaLoadData) {
|
||||||
|
if (mediaPeriodId != null) {
|
||||||
|
onLoadStartedMediaPeriodUids.add(mediaPeriodId.periodUid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCompleted(
|
||||||
|
int windowIndex,
|
||||||
|
@Nullable MediaPeriodId mediaPeriodId,
|
||||||
|
LoadEventInfo loadEventInfo,
|
||||||
|
MediaLoadData mediaLoadData) {
|
||||||
|
if (mediaPeriodId != null) {
|
||||||
|
onLoadCompletedMediaPeriodUids.add(mediaPeriodId.periodUid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
FakeMediaSource[] childMediaSources = new FakeMediaSource[2];
|
||||||
|
for (int i = 0; i < childMediaSources.length; i++) {
|
||||||
|
childMediaSources[i] =
|
||||||
|
new FakeMediaSource(
|
||||||
|
new FakeTimeline(
|
||||||
|
new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ i)));
|
||||||
|
}
|
||||||
|
// Delay child1's period preparation, so we can delay child1period0 preparation completion until
|
||||||
|
// after period1 has been created and prepared.
|
||||||
|
childMediaSources[1].setPeriodDefersOnPreparedCallback(true);
|
||||||
|
MergingMediaSource mergingMediaSource = new MergingMediaSource(childMediaSources);
|
||||||
|
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mergingMediaSource);
|
||||||
|
try {
|
||||||
|
testRunner.runOnPlaybackThread(
|
||||||
|
() ->
|
||||||
|
mergingMediaSource.addEventListener(
|
||||||
|
Util.createHandlerForCurrentLooper(), mediaSourceEventListener));
|
||||||
|
Timeline timeline = testRunner.prepareSource();
|
||||||
|
MediaPeriod mergedMediaPeriod0 =
|
||||||
|
testRunner.createPeriod(new MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ 0)));
|
||||||
|
FakeMediaPeriod childSource1Period0 =
|
||||||
|
(FakeMediaPeriod) childMediaSources[1].getLastCreatedActiveMediaPeriod();
|
||||||
|
MediaPeriod mergedMediaPeriod1 =
|
||||||
|
testRunner.createPeriod(new MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ 1)));
|
||||||
|
// Prepare period0 after period1 has been created to ensure that MergingMediaSource correctly
|
||||||
|
// attributes and propagates the associated onLoadStarted event.
|
||||||
|
CountDownLatch preparedLatch0 =
|
||||||
|
testRunner.preparePeriod(mergedMediaPeriod0, /* positionUs= */ 0);
|
||||||
|
CountDownLatch preparedLatch1 =
|
||||||
|
testRunner.preparePeriod(mergedMediaPeriod1, /* positionUs= */ 0);
|
||||||
|
// Complete child1period0 preparation after period1 has been created to ensure that
|
||||||
|
// MergingMediaSource correctly attributes and propagates the associated onLoadCompleted
|
||||||
|
// event.
|
||||||
|
childSource1Period0.setPreparationComplete();
|
||||||
|
((FakeMediaPeriod) childMediaSources[1].getLastCreatedActiveMediaPeriod())
|
||||||
|
.setPreparationComplete();
|
||||||
|
|
||||||
|
assertThat(preparedLatch0.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
assertThat(preparedLatch1.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
testRunner.releasePeriod(mergedMediaPeriod0);
|
||||||
|
testRunner.releasePeriod(mergedMediaPeriod1);
|
||||||
|
for (FakeMediaSource element : childMediaSources) {
|
||||||
|
assertThat(element.getCreatedMediaPeriods()).isNotEmpty();
|
||||||
|
}
|
||||||
|
testRunner.releaseSource();
|
||||||
|
ImmutableList.Builder<Object> expectedMediaPeriodUids =
|
||||||
|
ImmutableList.builderWithExpectedSize(onLoadStartedMediaPeriodUids.size());
|
||||||
|
for (int i = 0; i < timeline.getPeriodCount(); i++) {
|
||||||
|
Object periodUid = timeline.getUidOfPeriod(i);
|
||||||
|
// Add each period UID twice, because each child reports its own load events (but both are
|
||||||
|
// reported with the same MediaPeriodId out of MergingMediaSource).
|
||||||
|
expectedMediaPeriodUids.add(periodUid).add(periodUid);
|
||||||
|
}
|
||||||
|
assertThat(onLoadStartedMediaPeriodUids)
|
||||||
|
.containsExactlyElementsIn(expectedMediaPeriodUids.build());
|
||||||
|
assertThat(onLoadCompletedMediaPeriodUids)
|
||||||
|
.containsExactlyElementsIn(expectedMediaPeriodUids.build());
|
||||||
|
} finally {
|
||||||
|
testRunner.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps the specified timelines in a {@link MergingMediaSource}, prepares it and returns the
|
* Wraps the specified timelines in a {@link MergingMediaSource}, prepares it and returns the
|
||||||
* merged timeline.
|
* merged timeline.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user