Skip to content

Commit

Permalink
Propagate events from secondary children in MergingMediaSource
Browse files Browse the repository at this point in the history
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: #1722
PiperOrigin-RevId: 686901439
  • Loading branch information
icbaker authored and copybara-github committed Oct 17, 2024
1 parent b3290ef commit 191bc09
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 4 deletions.
6 changes: 6 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
The new
`DefaultLoadControl.calculateTargetBufferBytes(ExoTrackSelection[])`
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:
* Make setting the image duration using
`MediaItem.Builder.setImageDurationMs` mandatory for image export.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
Expand Down Expand Up @@ -84,6 +85,7 @@ public IllegalMergeException(@Reason int reason) {
private final boolean adjustPeriodTimeOffsets;
private final boolean clipDurations;
private final MediaSource[] mediaSources;
private final List<List<MediaPeriodAndId>> mediaPeriods;
private final Timeline[] timelines;
private final ArrayList<MediaSource> pendingTimelineSources;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
Expand Down Expand Up @@ -161,6 +163,10 @@ public MergingMediaSource(
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources));
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];
periodTimeOffsetsUs = new long[0][];
clippedDurationsUs = new HashMap<>();
Expand Down Expand Up @@ -203,11 +209,11 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
for (int i = 0; i < periods.length; i++) {
MediaPeriodId childMediaPeriodId =
id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
MediaPeriodId mediaPeriodId = id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
periods[i] =
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 =
new MergingMediaPeriod(
Expand Down Expand Up @@ -238,6 +244,13 @@ public void releasePeriod(MediaPeriod mediaPeriod) {
}
MergingMediaPeriod mergingPeriod = (MergingMediaPeriod) mediaPeriod;
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));
}
}
Expand Down Expand Up @@ -286,7 +299,13 @@ protected void onChildSourceInfoRefreshed(
@Nullable
protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
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() {
Expand Down Expand Up @@ -370,4 +389,14 @@ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,28 @@
*/
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 java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertThrows;

import androidx.annotation.Nullable;
import androidx.media3.common.C;
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.test.utils.FakeMediaPeriod;
import androidx.media3.test.utils.FakeMediaSource;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.media3.test.utils.MediaSourceTestRunner;
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.util.concurrent.CountDownLatch;
import org.junit.Test;
import org.junit.runner.RunWith;

Expand Down Expand Up @@ -112,6 +122,102 @@ public void createPeriod_createsChildPeriods() throws Exception {
}
}

/**
* 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
* merged timeline.
Expand Down

0 comments on commit 191bc09

Please sign in to comment.