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:
ibaker 2024-10-17 07:07:09 -07:00 committed by Copybara-Service
parent b3290eff10
commit 191bc094a5
3 changed files with 145 additions and 4 deletions

View File

@ -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.

View File

@ -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;
}
}
} }

View File

@ -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.