mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Compare commits
9 Commits
3ab484d93f
...
cf3faf9cff
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cf3faf9cff | ||
![]() |
812e078310 | ||
![]() |
3fddf4376c | ||
![]() |
ce3754a740 | ||
![]() |
209ecce6b3 | ||
![]() |
c95544156d | ||
![]() |
f2d644b7b4 | ||
![]() |
595b75b7d3 | ||
![]() |
2141d9ef9c |
@ -3,6 +3,9 @@
|
||||
### Unreleased changes
|
||||
|
||||
* Common Library:
|
||||
* Add `PlaybackParameters.withPitch(float)` method for easily copying a
|
||||
`PlaybackParameters` with a new `pitch` value
|
||||
([#2257](https://github.com/androidx/media/issues/2257)).
|
||||
* ExoPlayer:
|
||||
* Fix sending `CmcdData` in manifest requests for DASH, HLS, and
|
||||
SmoothStreaming ([#2253](https://github.com/androidx/media/pull/2253)).
|
||||
@ -10,6 +13,10 @@
|
||||
error during initialization of the next media item
|
||||
([#2229](https://github.com/androidx/media/issues/2229)).
|
||||
* Transformer:
|
||||
* Filling an initial gap (added via `addGap()`) with silent audio now
|
||||
requires explicitly setting `setForceAudioTrack(true)` in
|
||||
`EditedMediaItemSequence.Builder`. If the gap is in the middle of the
|
||||
sequence, then this flag is not required.
|
||||
* Track Selection:
|
||||
* Extractors:
|
||||
* MP3: Use duration and data size from unseekable Xing, VBRI and similar
|
||||
|
@ -88,6 +88,18 @@ public final class PlaybackParameters {
|
||||
return new PlaybackParameters(speed, pitch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy with the given pitch.
|
||||
*
|
||||
* @param pitch The new pitch. Must be greater than zero.
|
||||
* @return The copied playback parameters.
|
||||
*/
|
||||
@UnstableApi
|
||||
@CheckResult
|
||||
public PlaybackParameters withPitch(@FloatRange(from = 0, fromInclusive = false) float pitch) {
|
||||
return new PlaybackParameters(speed, pitch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
|
@ -590,10 +590,10 @@ import java.util.List;
|
||||
newPeriodInfo.copyWithRequestedContentPositionUs(
|
||||
oldPeriodInfo.requestedContentPositionUs);
|
||||
|
||||
if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) {
|
||||
// The period duration changed. Remove all subsequent periods and check whether we read
|
||||
// beyond the new duration.
|
||||
if (oldPeriodInfo.durationUs != newPeriodInfo.durationUs) {
|
||||
// The period duration changed.
|
||||
periodHolder.updateClipping();
|
||||
// Check whether we've read beyond the new duration.
|
||||
long newDurationInRendererTime =
|
||||
newPeriodInfo.durationUs == C.TIME_UNSET
|
||||
? Long.MAX_VALUE
|
||||
@ -607,12 +607,19 @@ import java.util.List;
|
||||
periodHolder == prewarming
|
||||
&& (maxRendererPrewarmingPositionUs == C.TIME_END_OF_SOURCE
|
||||
|| maxRendererPrewarmingPositionUs >= newDurationInRendererTime);
|
||||
// Remove all subsequent periods.
|
||||
@MediaPeriodQueue.UpdatePeriodQueueResult int removeAfterResult = removeAfter(periodHolder);
|
||||
if (removeAfterResult != 0) {
|
||||
return removeAfterResult;
|
||||
}
|
||||
boolean isLivePeriodClippedForAd =
|
||||
oldPeriodInfo.durationUs == C.TIME_UNSET
|
||||
&& oldPeriodInfo.endPositionUs == C.TIME_END_OF_SOURCE
|
||||
&& newPeriodInfo.endPositionUs != C.TIME_UNSET
|
||||
&& newPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE;
|
||||
int result = 0;
|
||||
if (isReadingAndReadBeyondNewDuration) {
|
||||
if (isReadingAndReadBeyondNewDuration
|
||||
&& (oldPeriodInfo.durationUs != C.TIME_UNSET || isLivePeriodClippedForAd)) {
|
||||
result |= UPDATE_PERIOD_QUEUE_ALTERED_READING_PERIOD;
|
||||
}
|
||||
if (isPrewarmingAndReadBeyondNewDuration) {
|
||||
@ -667,7 +674,7 @@ import java.util.List;
|
||||
isFollowedByTransitionToSameStream,
|
||||
isLastInPeriod,
|
||||
isLastInWindow,
|
||||
isLastInTimeline);
|
||||
/* isFinal= */ isLastInTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1225,8 +1232,6 @@ import java.util.List;
|
||||
boolean isPrecededByTransitionFromSameStream) {
|
||||
timeline.getPeriodByUid(periodUid, period);
|
||||
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
|
||||
boolean isNextAdGroupPostrollPlaceholder =
|
||||
nextAdGroupIndex != C.INDEX_UNSET && period.isLivePostrollPlaceholder(nextAdGroupIndex);
|
||||
boolean clipPeriodAtContentDuration = false;
|
||||
if (nextAdGroupIndex == C.INDEX_UNSET) {
|
||||
// Clip SSAI streams when at the end of the period.
|
||||
@ -1248,9 +1253,13 @@ import java.util.List;
|
||||
boolean isFollowedByTransitionToSameStream =
|
||||
nextAdGroupIndex != C.INDEX_UNSET
|
||||
&& period.isServerSideInsertedAdGroup(nextAdGroupIndex)
|
||||
&& !isNextAdGroupPostrollPlaceholder;
|
||||
&& !period.isLivePostrollPlaceholder(nextAdGroupIndex);
|
||||
boolean isFollowedByServerSidePostRollPlaceholder =
|
||||
nextAdGroupIndex != C.INDEX_UNSET
|
||||
&& period.isLivePostrollPlaceholder(nextAdGroupIndex)
|
||||
&& period.isServerSideInsertedAdGroup(nextAdGroupIndex);
|
||||
long endPositionUs =
|
||||
nextAdGroupIndex != C.INDEX_UNSET && !isNextAdGroupPostrollPlaceholder
|
||||
nextAdGroupIndex != C.INDEX_UNSET && !isFollowedByServerSidePostRollPlaceholder
|
||||
? period.getAdGroupTimeUs(nextAdGroupIndex)
|
||||
: clipPeriodAtContentDuration ? period.durationUs : C.TIME_UNSET;
|
||||
long durationUs =
|
||||
|
@ -368,7 +368,9 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
||||
public VideoSink getSink(int inputIndex) {
|
||||
checkState(!contains(inputVideoSinks, inputIndex));
|
||||
InputVideoSink inputVideoSink = new InputVideoSink(context, inputIndex);
|
||||
addListener(inputVideoSink);
|
||||
if (inputIndex == PRIMARY_SEQUENCE_INDEX) {
|
||||
addListener(inputVideoSink);
|
||||
}
|
||||
inputVideoSinks.put(inputIndex, inputVideoSink);
|
||||
return inputVideoSink;
|
||||
}
|
||||
@ -536,6 +538,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
||||
maybeSetOutputSurfaceInfo(surface, size.getWidth(), size.getHeight());
|
||||
}
|
||||
defaultVideoSink.initialize(sourceFormat);
|
||||
defaultVideoSink.setListener(new DefaultVideoSinkListener(), /* executor= */ handler::post);
|
||||
state = STATE_INITIALIZED;
|
||||
} else {
|
||||
if (!isInitialized()) {
|
||||
@ -550,8 +553,6 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
||||
throw new VideoSink.VideoSinkException(e, sourceFormat);
|
||||
}
|
||||
registeredVideoInputCount++;
|
||||
defaultVideoSink.setListener(
|
||||
new DefaultVideoSinkListener(), /* executor= */ checkNotNull(handler)::post);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -637,6 +638,11 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
||||
defaultVideoSink.setBufferTimestampAdjustmentUs(bufferTimestampAdjustmentUs);
|
||||
}
|
||||
|
||||
private void setChangeFrameRateStrategy(
|
||||
@C.VideoChangeFrameRateStrategy int changeFrameRateStrategy) {
|
||||
defaultVideoSink.setChangeFrameRateStrategy(changeFrameRateStrategy);
|
||||
}
|
||||
|
||||
private boolean shouldRenderToInputVideoSink() {
|
||||
return totalVideoInputCount != C.LENGTH_UNSET
|
||||
&& totalVideoInputCount == registeredVideoInputCount;
|
||||
@ -850,12 +856,16 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
||||
@Override
|
||||
public void setVideoFrameMetadataListener(
|
||||
VideoFrameMetadataListener videoFrameMetadataListener) {
|
||||
PlaybackVideoGraphWrapper.this.setVideoFrameMetadataListener(videoFrameMetadataListener);
|
||||
if (inputIndex == PRIMARY_SEQUENCE_INDEX) {
|
||||
PlaybackVideoGraphWrapper.this.setVideoFrameMetadataListener(videoFrameMetadataListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeed(@FloatRange(from = 0, fromInclusive = false) float speed) {
|
||||
PlaybackVideoGraphWrapper.this.setPlaybackSpeed(speed);
|
||||
if (inputIndex == PRIMARY_SEQUENCE_INDEX) {
|
||||
PlaybackVideoGraphWrapper.this.setPlaybackSpeed(speed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -892,7 +902,9 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
||||
@Override
|
||||
public void setChangeFrameRateStrategy(
|
||||
@C.VideoChangeFrameRateStrategy int changeFrameRateStrategy) {
|
||||
defaultVideoSink.setChangeFrameRateStrategy(changeFrameRateStrategy);
|
||||
if (inputIndex == PRIMARY_SEQUENCE_INDEX) {
|
||||
PlaybackVideoGraphWrapper.this.setChangeFrameRateStrategy(changeFrameRateStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -10595,7 +10595,6 @@ public final class ExoPlayerTest {
|
||||
player.release();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Checking old volume commands
|
||||
@Test
|
||||
public void isCommandAvailable_isTrueForAvailableCommands() {
|
||||
ExoPlayer player = parameterizeTestExoPlayerBuilder(new TestExoPlayerBuilder(context)).build();
|
||||
@ -14139,35 +14138,24 @@ public final class ExoPlayerTest {
|
||||
.build();
|
||||
// Live stream timeline with unassigned next ad group.
|
||||
AdPlaybackState initialAdPlaybackState =
|
||||
new AdPlaybackState(
|
||||
/* adsId= */ new Object(), /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdDurationsUs(new long[][] {new long[] {10 * C.MICROS_PER_SECOND}});
|
||||
new AdPlaybackState(/* adsId= */ new Object())
|
||||
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true);
|
||||
// Updated timeline with ad group at 18 seconds.
|
||||
long firstSampleTimeUs = TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
||||
Timeline initialTimeline =
|
||||
new FakeTimeline(
|
||||
new TimelineWindowDefinition(
|
||||
/* periodCount= */ 1,
|
||||
/* id= */ 0,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ true,
|
||||
/* durationUs= */ C.TIME_UNSET,
|
||||
initialAdPlaybackState));
|
||||
TimelineWindowDefinition initialTimelineWindowDefinition =
|
||||
new TimelineWindowDefinition.Builder()
|
||||
.setDynamic(true)
|
||||
.setDurationUs(C.TIME_UNSET)
|
||||
.setUid(0)
|
||||
.setAdPlaybackStates(ImmutableList.of(initialAdPlaybackState))
|
||||
.build();
|
||||
Timeline initialTimeline = new FakeTimeline(initialTimelineWindowDefinition);
|
||||
AdPlaybackState updatedAdPlaybackState =
|
||||
initialAdPlaybackState.withAdGroupTimeUs(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adGroupTimeUs= */ firstSampleTimeUs + 18 * C.MICROS_PER_SECOND);
|
||||
Timeline updatedTimeline =
|
||||
new FakeTimeline(
|
||||
new TimelineWindowDefinition(
|
||||
/* periodCount= */ 1,
|
||||
/* id= */ 0,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ true,
|
||||
/* durationUs= */ C.TIME_UNSET,
|
||||
updatedAdPlaybackState));
|
||||
initialAdPlaybackState
|
||||
.withNewAdGroup(0, firstSampleTimeUs + 18 * C.MICROS_PER_SECOND)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, new long[] {10 * C.MICROS_PER_SECOND});
|
||||
// Add samples to allow player to load and start playing (but no EOS as this is a live stream).
|
||||
FakeMediaSource mediaSource =
|
||||
new FakeMediaSource(
|
||||
@ -14181,16 +14169,26 @@ public final class ExoPlayerTest {
|
||||
|
||||
// Set updated ad group once we reach 20 seconds, and then continue playing until 40 seconds.
|
||||
player
|
||||
.createMessage((message, payload) -> mediaSource.setNewSourceInfo(updatedTimeline))
|
||||
.setPosition(20_000)
|
||||
.createMessage(
|
||||
(message, payload) ->
|
||||
mediaSource.setNewSourceInfo(
|
||||
new FakeTimeline(
|
||||
initialTimelineWindowDefinition
|
||||
.buildUpon()
|
||||
.setAdPlaybackStates(ImmutableList.of(updatedAdPlaybackState))
|
||||
.build())))
|
||||
.setPosition(20_000L)
|
||||
.send();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 40_000);
|
||||
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 40_000L);
|
||||
Timeline timeline = player.getCurrentTimeline();
|
||||
player.release();
|
||||
|
||||
// Assert that the renderer hasn't been reset despite the inserted ad group.
|
||||
assertThat(videoRenderer.get().positionResetCount).isEqualTo(1);
|
||||
assertThat(timeline.getPeriod(0, new Timeline.Period()).adPlaybackState.adGroupCount)
|
||||
.isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -27,7 +27,9 @@ import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.D
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.os.Looper;
|
||||
@ -49,7 +51,9 @@ import androidx.media3.exoplayer.analytics.PlayerId;
|
||||
import androidx.media3.exoplayer.source.MediaPeriod;
|
||||
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
|
||||
import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller;
|
||||
import androidx.media3.exoplayer.source.SampleStream;
|
||||
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
|
||||
import androidx.media3.exoplayer.source.TrackGroupArray;
|
||||
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource;
|
||||
import androidx.media3.exoplayer.source.ads.SinglePeriodAdTimeline;
|
||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||
@ -488,6 +492,38 @@ public final class MediaPeriodQueueTest {
|
||||
/* nextAdGroupIndex= */ C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
getNextMediaPeriodInfo_singlePeriodLiveTimelineWithPostRollPlaceholder_returnsCorrectMediaPeriodInfo() {
|
||||
SinglePeriodTimeline liveContentTimeline =
|
||||
new SinglePeriodTimeline(
|
||||
C.TIME_UNSET,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ true,
|
||||
/* useLiveConfiguration= */ true,
|
||||
/* manifest= */ null,
|
||||
AD_MEDIA_ITEM);
|
||||
adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ new Object())
|
||||
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false);
|
||||
SinglePeriodAdTimeline adTimeline =
|
||||
new SinglePeriodAdTimeline(liveContentTimeline, adPlaybackState);
|
||||
setupTimelines(adTimeline);
|
||||
|
||||
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
||||
/* periodUid= */ firstPeriodUid,
|
||||
/* startPositionUs= */ 0,
|
||||
/* requestedContentPositionUs= */ C.TIME_UNSET,
|
||||
/* endPositionUs= */ C.TIME_END_OF_SOURCE,
|
||||
/* durationUs= */ C.TIME_UNSET,
|
||||
/* isPrecededByTransitionFromSameStream= */ false,
|
||||
/* isFollowedByTransitionToSameStream= */ false,
|
||||
/* isLastInPeriod= */ false,
|
||||
/* isLastInWindow= */ false,
|
||||
/* isFinal= */ false,
|
||||
/* nextAdGroupIndex= */ 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void getNextMediaPeriodInfo_multiPeriodTimelineWithNoAdsAndNoPostrollPlaceholder() {
|
||||
@ -1194,6 +1230,92 @@ public final class MediaPeriodQueueTest {
|
||||
assertThat(getQueueLength()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
updateQueuedPeriods_adInsertedIntoPeriodWithUnsetDuration_bufferedPositionEndOfSource()
|
||||
throws InterruptedException, ExoPlaybackException {
|
||||
// Initial setup enqueues the live period with only the placeholder ad in place.
|
||||
adPlaybackState =
|
||||
new AdPlaybackState(/* adsId= */ new Object())
|
||||
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false);
|
||||
SinglePeriodTimeline liveTimeline =
|
||||
new SinglePeriodTimeline(
|
||||
/* durationUs= */ C.TIME_UNSET,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ true,
|
||||
/* useLiveConfiguration= */ true,
|
||||
/* manifest= */ null,
|
||||
AD_MEDIA_ITEM);
|
||||
setupTimelines(new SinglePeriodAdTimeline(liveTimeline, adPlaybackState));
|
||||
enqueueNext();
|
||||
// The period needs to be prepared to get the actual buffered position from it.
|
||||
mediaPeriodQueue
|
||||
.getLoadingPeriod()
|
||||
.mediaPeriod
|
||||
.prepare(
|
||||
new MediaPeriod.Callback() {
|
||||
@Override
|
||||
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||
TrackGroupArray trackGroups = mediaPeriod.getTrackGroups();
|
||||
ExoTrackSelection[] selection = new ExoTrackSelection[trackGroups.length];
|
||||
SampleStream[] streams = new SampleStream[trackGroups.length];
|
||||
for (int i = 0; i < streams.length; i++) {
|
||||
streams[i] = mock(SampleStream.class);
|
||||
}
|
||||
try {
|
||||
when(trackSelector.selectTracks(any(), any(), any(), any()))
|
||||
.thenReturn(
|
||||
new TrackSelectorResult(
|
||||
new RendererConfiguration[0],
|
||||
selection,
|
||||
Tracks.EMPTY,
|
||||
/* info= */ null));
|
||||
} catch (ExoPlaybackException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
mediaPeriod.selectTracks(
|
||||
selection,
|
||||
new boolean[trackGroups.length],
|
||||
streams,
|
||||
new boolean[trackGroups.length],
|
||||
/* positionUs= */ 0L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||
},
|
||||
0L);
|
||||
// Ad inserted into timeline.
|
||||
adPlaybackState =
|
||||
adPlaybackState
|
||||
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ 30_000_123L)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
|
||||
updateAdTimeline(/* mediaSourceIndex= */ 0);
|
||||
Timeline playlistTimeline = mediaSourceList.createTimeline();
|
||||
mediaPeriodQueue
|
||||
.getLoadingPeriod()
|
||||
.handlePrepared(/* playbackSpeed= */ 1.0f, playlistTimeline, /* playWhenReady= */ false);
|
||||
// Assume renderers have not yet read beyond the ad group timeUs.
|
||||
long maxRendererReadPositionUs = Renderer.DEFAULT_DURATION_TO_PROGRESS_US + 30_000_122L;
|
||||
|
||||
@MediaPeriodQueue.UpdatePeriodQueueResult
|
||||
int updateQueuedPeriodsResult =
|
||||
mediaPeriodQueue.updateQueuedPeriods(
|
||||
playlistTimeline,
|
||||
/* rendererPositionUs= */ MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US,
|
||||
/* maxRendererReadPositionUs= */ maxRendererReadPositionUs,
|
||||
/* maxRendererPrewarmingPositionUs= */ maxRendererReadPositionUs);
|
||||
|
||||
assertThat(mediaPeriodQueue.getLoadingPeriod().mediaPeriod.getBufferedPositionUs())
|
||||
.isEqualTo(C.TIME_END_OF_SOURCE);
|
||||
assertThat(mediaPeriodQueue.getLoadingPeriod().info.durationUs).isEqualTo(30_000_123L);
|
||||
assertThat(mediaPeriodQueue.getLoadingPeriod().info.startPositionUs).isEqualTo(0);
|
||||
assertThat(mediaPeriodQueue.getLoadingPeriod().info.endPositionUs).isEqualTo(30_000_123L);
|
||||
assertThat(mediaPeriodQueue.getLoadingPeriod().getBufferedPositionUs()).isEqualTo(30_000_123L);
|
||||
assertThat(updateQueuedPeriodsResult).isEqualTo(0);
|
||||
assertThat(getQueueLength()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdPositionInSinglePeriodTimeline_resolvesToAd() {
|
||||
|
@ -15,18 +15,28 @@
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static androidx.media3.common.AdPlaybackState.AD_STATE_AVAILABLE;
|
||||
import static androidx.media3.common.AdPlaybackState.AD_STATE_UNAVAILABLE;
|
||||
import static androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
|
||||
import static androidx.media3.common.Player.DISCONTINUITY_REASON_REMOVE;
|
||||
import static androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK;
|
||||
import static androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
||||
import static androidx.media3.common.Player.DISCONTINUITY_REASON_SKIP;
|
||||
import static androidx.media3.common.Player.STATE_IDLE;
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||
import static androidx.media3.common.util.Util.castNonNull;
|
||||
import static androidx.media3.common.util.Util.msToUs;
|
||||
import static androidx.media3.common.util.Util.usToMs;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_POST;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_PRE;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.AdPlaybackState;
|
||||
import androidx.media3.common.AdViewProvider;
|
||||
@ -44,8 +54,11 @@ import androidx.media3.common.util.Consumer;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DataSpec;
|
||||
import androidx.media3.datasource.DefaultDataSource;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.PlayerMessage;
|
||||
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial;
|
||||
@ -53,6 +66,8 @@ import androidx.media3.exoplayer.source.MediaSource;
|
||||
import androidx.media3.exoplayer.source.ads.AdsLoader;
|
||||
import androidx.media3.exoplayer.source.ads.AdsMediaSource;
|
||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
||||
import androidx.media3.exoplayer.upstream.Loader;
|
||||
import androidx.media3.exoplayer.upstream.ParsingLoadable;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.IOException;
|
||||
@ -63,6 +78,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* An {@linkplain AdsLoader ads loader} that reads interstitials from the HLS playlist, adds them to
|
||||
@ -448,22 +464,42 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
|
||||
private static final String TAG = "HlsInterstitiaAdsLoader";
|
||||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final PlayerListener playerListener;
|
||||
private final Map<Object, EventListener> activeEventListeners;
|
||||
private final Map<Object, AdPlaybackState> activeAdPlaybackStates;
|
||||
private final Map<Object, Set<String>> insertedInterstitialIds;
|
||||
private final Map<Object, TreeMap<Long, AssetListData>> unresolvedAssetLists;
|
||||
private final List<Listener> listeners;
|
||||
private final Set<Object> unsupportedAdsIds;
|
||||
|
||||
@Nullable private Player player;
|
||||
@Nullable private ExoPlayer player;
|
||||
@Nullable private Loader loader;
|
||||
private boolean isReleased;
|
||||
@Nullable private PlayerMessage pendingAssetListResolutionMessage;
|
||||
|
||||
/** Creates an instance. */
|
||||
public HlsInterstitialsAdsLoader() {
|
||||
/**
|
||||
* Creates an instance with a {@link DefaultDataSource.Factory} to read HLS X-ASSET-LIST JSON
|
||||
* objects.
|
||||
*
|
||||
* @param context The context.
|
||||
*/
|
||||
public HlsInterstitialsAdsLoader(Context context) {
|
||||
this(new DefaultDataSource.Factory(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param dataSourceFactory The data source factory to read HLS X-ASSET-LIST JSON objects.
|
||||
*/
|
||||
public HlsInterstitialsAdsLoader(DataSource.Factory dataSourceFactory) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
playerListener = new PlayerListener();
|
||||
activeEventListeners = new HashMap<>();
|
||||
activeAdPlaybackStates = new HashMap<>();
|
||||
insertedInterstitialIds = new HashMap<>();
|
||||
unresolvedAssetLists = new HashMap<>();
|
||||
listeners = new ArrayList<>();
|
||||
unsupportedAdsIds = new HashSet<>();
|
||||
}
|
||||
@ -492,6 +528,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
@Override
|
||||
public void setPlayer(@Nullable Player player) {
|
||||
checkState(!isReleased);
|
||||
checkArgument(player == null || player instanceof ExoPlayer);
|
||||
if (Objects.equals(this.player, player)) {
|
||||
return;
|
||||
}
|
||||
@ -499,7 +536,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
this.player.removeListener(playerListener);
|
||||
}
|
||||
checkState(player == null || activeEventListeners.isEmpty());
|
||||
this.player = player;
|
||||
this.player = (ExoPlayer) player;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -540,6 +577,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
// Mark with NONE. Update and notify later when timeline with interstitials arrives.
|
||||
activeAdPlaybackStates.put(adsId, AdPlaybackState.NONE);
|
||||
insertedInterstitialIds.put(adsId, new HashSet<>());
|
||||
unresolvedAssetLists.put(adsId, new TreeMap<>());
|
||||
notifyListeners(listener -> listener.onStart(mediaItem, adsId, adViewProvider));
|
||||
} else {
|
||||
putAndNotifyAdPlaybackStateUpdate(adsId, new AdPlaybackState(adsId));
|
||||
@ -584,15 +622,41 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
Window window = timeline.getWindow(0, new Window());
|
||||
if (window.manifest instanceof HlsManifest) {
|
||||
HlsMediaPlaylist mediaPlaylist = ((HlsManifest) window.manifest).mediaPlaylist;
|
||||
TreeMap<Long, AssetListData> assetListDataMap = checkNotNull(unresolvedAssetLists.get(adsId));
|
||||
int unresolvedAssetListCount = assetListDataMap.size();
|
||||
adPlaybackState =
|
||||
window.isLive()
|
||||
? mapInterstitialsForLive(
|
||||
window.mediaItem,
|
||||
mediaPlaylist,
|
||||
adPlaybackState,
|
||||
window.positionInFirstPeriodUs,
|
||||
checkNotNull(insertedInterstitialIds.get(adsId)))
|
||||
: mapInterstitialsForVod(
|
||||
mediaPlaylist, adPlaybackState, checkNotNull(insertedInterstitialIds.get(adsId)));
|
||||
window.mediaItem,
|
||||
mediaPlaylist,
|
||||
adPlaybackState,
|
||||
checkNotNull(insertedInterstitialIds.get(adsId)));
|
||||
Player player = this.player;
|
||||
if (unresolvedAssetListCount != assetListDataMap.size()
|
||||
&& player != null
|
||||
&& Objects.equals(window.mediaItem, player.getCurrentMediaItem())) {
|
||||
long contentPositionUs;
|
||||
if (window.isLive()) {
|
||||
int currentPublicPeriodIndex = player.getCurrentPeriodIndex();
|
||||
Period publicPeriod =
|
||||
player.getCurrentTimeline().getPeriod(currentPublicPeriodIndex, new Period());
|
||||
// Use the default position if this is the first timeline update.
|
||||
contentPositionUs =
|
||||
publicPeriod.isPlaceholder
|
||||
? window.defaultPositionUs
|
||||
: msToUs(player.getContentPosition());
|
||||
} else {
|
||||
contentPositionUs = msToUs(player.getContentPosition());
|
||||
}
|
||||
maybeExecuteOrSetNextAssetListResolutionMessage(
|
||||
adsId, timeline, /* windowIndex= */ 0, contentPositionUs);
|
||||
}
|
||||
}
|
||||
putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
|
||||
if (!unsupportedAdsIds.contains(adsId)) {
|
||||
@ -654,6 +718,14 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
}
|
||||
insertedInterstitialIds.remove(adsId);
|
||||
unsupportedAdsIds.remove(adsId);
|
||||
unresolvedAssetLists.remove(adsId);
|
||||
cancelPendingAssetListResolutionMessage();
|
||||
if (pendingAssetListResolutionMessage != null
|
||||
&& adsMediaSource
|
||||
.getMediaItem()
|
||||
.equals(castNonNull(pendingAssetListResolutionMessage).getPayload())) {
|
||||
cancelPendingAssetListResolutionMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -663,11 +735,131 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
if (activeEventListeners.isEmpty()) {
|
||||
player = null;
|
||||
}
|
||||
cancelPendingAssetListResolutionMessage();
|
||||
if (loader != null) {
|
||||
loader.release();
|
||||
loader = null;
|
||||
}
|
||||
isReleased = true;
|
||||
}
|
||||
|
||||
// private methods
|
||||
|
||||
private void startLoadingAssetList(AssetListData assetListData) {
|
||||
cancelPendingAssetListResolutionMessage();
|
||||
getLoader()
|
||||
.startLoading(
|
||||
new ParsingLoadable<>(
|
||||
dataSourceFactory.createDataSource(),
|
||||
checkNotNull(assetListData.interstitial.assetListUri),
|
||||
C.DATA_TYPE_AD,
|
||||
new AssetListParser()),
|
||||
new LoaderCallback(assetListData),
|
||||
/* defaultMinRetryCount= */ 1);
|
||||
notifyListeners(
|
||||
(listener) ->
|
||||
listener.onAssetListLoadStarted(
|
||||
assetListData.mediaItem,
|
||||
assetListData.adsId,
|
||||
assetListData.adGroupIndex,
|
||||
assetListData.adIndexInAdGroup));
|
||||
}
|
||||
|
||||
private void maybeExecuteOrSetNextAssetListResolutionMessage(
|
||||
Object adsId, Timeline contentTimeline, int windowIndex, long windowPositionUs) {
|
||||
if (loader != null && loader.isLoading()) {
|
||||
return;
|
||||
}
|
||||
cancelPendingAssetListResolutionMessage();
|
||||
Window window = contentTimeline.getWindow(windowIndex, new Window());
|
||||
long currentPeriodPositionUs = window.positionInFirstPeriodUs + windowPositionUs;
|
||||
RunnableAtPosition nextAssetResolution = getNextAssetResolution(adsId, currentPeriodPositionUs);
|
||||
if (nextAssetResolution == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
long resolutionStartTimeUs =
|
||||
nextAssetResolution.adStartTimeUs != Long.MAX_VALUE
|
||||
? nextAssetResolution.adStartTimeUs
|
||||
: window.durationUs;
|
||||
// Load 2 times the target duration before the ad starts.
|
||||
resolutionStartTimeUs =
|
||||
max(
|
||||
currentPeriodPositionUs,
|
||||
resolutionStartTimeUs - (2 * nextAssetResolution.targetDurationUs));
|
||||
if (resolutionStartTimeUs - currentPeriodPositionUs < 200_000L) {
|
||||
// Start loading immediately.
|
||||
nextAssetResolution.run();
|
||||
} else {
|
||||
long messagePositionUs = resolutionStartTimeUs - window.positionInFirstPeriodUs;
|
||||
pendingAssetListResolutionMessage =
|
||||
checkNotNull(player)
|
||||
.createMessage((messageType, message) -> nextAssetResolution.run())
|
||||
.setPayload(window.mediaItem)
|
||||
.setLooper(checkNotNull(Looper.myLooper()))
|
||||
.setPosition(usToMs(messagePositionUs));
|
||||
pendingAssetListResolutionMessage.send();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private RunnableAtPosition getNextAssetResolution(Object adsId, long periodPositionUs) {
|
||||
TreeMap<Long, AssetListData> assetListDataMap = checkNotNull(unresolvedAssetLists.get(adsId));
|
||||
for (Long assetListTimeUs : assetListDataMap.keySet()) {
|
||||
if (assetListDataMap.size() == 1 || periodPositionUs <= assetListTimeUs) {
|
||||
AssetListData assetListData = checkNotNull(assetListDataMap.get(assetListTimeUs));
|
||||
return new RunnableAtPosition(
|
||||
/* adStartTimeUs= */ assetListTimeUs,
|
||||
assetListData.targetDurationUs,
|
||||
() -> {
|
||||
if (assetListDataMap.remove(assetListTimeUs) != null) {
|
||||
startLoadingAssetList(assetListData);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void cancelPendingAssetListResolutionMessage() {
|
||||
if (pendingAssetListResolutionMessage != null) {
|
||||
pendingAssetListResolutionMessage.cancel();
|
||||
pendingAssetListResolutionMessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
private long getUnresolvedAssetListWindowPositionForContentPositionUs(
|
||||
long contentPositionUs, Timeline timeline, int periodIndex) {
|
||||
Period period = timeline.getPeriod(periodIndex, new Period());
|
||||
long periodPositionUs = contentPositionUs - period.positionInWindowUs;
|
||||
AdPlaybackState adPlaybackState = period.adPlaybackState;
|
||||
int adGroupIndex = adPlaybackState.getAdGroupIndexForPositionUs(periodPositionUs, C.TIME_UNSET);
|
||||
if (adGroupIndex != C.INDEX_UNSET) {
|
||||
// Seek adjustment will snap to a playable ad behind the seek position.
|
||||
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
|
||||
TreeMap<Long, AssetListData> unresolvedAssets =
|
||||
unresolvedAssetLists.get(adPlaybackState.adsId);
|
||||
if (unresolvedAssets != null && unresolvedAssets.containsKey(adGroup.timeUs)) {
|
||||
Window window = timeline.getWindow(period.windowIndex, new Window());
|
||||
return adGroup.timeUs - window.positionInFirstPeriodUs;
|
||||
}
|
||||
}
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
||||
private void notifyListeners(Consumer<Listener> callable) {
|
||||
for (int i = 0; i < listeners.size(); i++) {
|
||||
callable.accept(listeners.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private Loader getLoader() {
|
||||
if (loader == null) {
|
||||
loader = new Loader("HLS-interstitials");
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
private void putAndNotifyAdPlaybackStateUpdate(Object adsId, AdPlaybackState adPlaybackState) {
|
||||
@Nullable
|
||||
AdPlaybackState oldAdPlaybackState = activeAdPlaybackStates.put(adsId, adPlaybackState);
|
||||
@ -682,10 +874,13 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyListeners(Consumer<Listener> callable) {
|
||||
for (int i = 0; i < listeners.size(); i++) {
|
||||
callable.accept(listeners.get(i));
|
||||
private void notifyAssetResolutionFailed(Object adsId, int adGroupIndex, int adIndexInAdGroup) {
|
||||
AdPlaybackState adPlaybackState = activeAdPlaybackStates.get(adsId);
|
||||
if (adPlaybackState == null) {
|
||||
return;
|
||||
}
|
||||
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup);
|
||||
putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
|
||||
}
|
||||
|
||||
private static boolean isLiveMediaItem(MediaItem mediaItem, Timeline timeline) {
|
||||
@ -709,7 +904,8 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
|| Util.inferContentType(localConfiguration.uri) == C.CONTENT_TYPE_HLS;
|
||||
}
|
||||
|
||||
private static AdPlaybackState mapInterstitialsForLive(
|
||||
private AdPlaybackState mapInterstitialsForLive(
|
||||
MediaItem mediaItem,
|
||||
HlsMediaPlaylist mediaPlaylist,
|
||||
AdPlaybackState adPlaybackState,
|
||||
long windowPositionInPeriodUs,
|
||||
@ -721,8 +917,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
interstitial.cue.contains(CUE_TRIGGER_PRE)
|
||||
? 0L
|
||||
: (interstitial.startDateUnixUs - mediaPlaylist.startTimeUs);
|
||||
if (interstitial.assetUri == null
|
||||
|| insertedInterstitialIds.contains(interstitial.id)
|
||||
if (insertedInterstitialIds.contains(interstitial.id)
|
||||
|| interstitial.cue.contains(CUE_TRIGGER_POST)
|
||||
|| positionInPlaylistWindowUs < 0) {
|
||||
continue;
|
||||
@ -759,13 +954,18 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
}
|
||||
adPlaybackState =
|
||||
insertOrUpdateInterstitialInAdGroup(
|
||||
interstitial, /* adGroupIndex= */ insertionIndex, adPlaybackState);
|
||||
mediaItem,
|
||||
interstitial,
|
||||
adPlaybackState,
|
||||
/* adGroupIndex= */ insertionIndex,
|
||||
mediaPlaylist.targetDurationUs);
|
||||
insertedInterstitialIds.add(interstitial.id);
|
||||
}
|
||||
return adPlaybackState;
|
||||
}
|
||||
|
||||
private static AdPlaybackState mapInterstitialsForVod(
|
||||
private AdPlaybackState mapInterstitialsForVod(
|
||||
MediaItem mediaItem,
|
||||
HlsMediaPlaylist mediaPlaylist,
|
||||
AdPlaybackState adPlaybackState,
|
||||
Set<String> insertedInterstitialIds) {
|
||||
@ -773,10 +973,6 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
ImmutableList<Interstitial> interstitials = mediaPlaylist.interstitials;
|
||||
for (int i = 0; i < interstitials.size(); i++) {
|
||||
Interstitial interstitial = interstitials.get(i);
|
||||
if (interstitial.assetUri == null) {
|
||||
Log.w(TAG, "Ignoring interstitials with X-ASSET-LIST. Not yet supported.");
|
||||
continue;
|
||||
}
|
||||
long timeUs;
|
||||
if (interstitial.cue.contains(CUE_TRIGGER_PRE)) {
|
||||
timeUs = 0L;
|
||||
@ -797,14 +993,23 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
adPlaybackState = adPlaybackState.withNewAdGroup(adGroupIndex, timeUs);
|
||||
}
|
||||
adPlaybackState =
|
||||
insertOrUpdateInterstitialInAdGroup(interstitial, adGroupIndex, adPlaybackState);
|
||||
insertOrUpdateInterstitialInAdGroup(
|
||||
mediaItem,
|
||||
interstitial,
|
||||
adPlaybackState,
|
||||
adGroupIndex,
|
||||
mediaPlaylist.targetDurationUs);
|
||||
insertedInterstitialIds.add(interstitial.id);
|
||||
}
|
||||
return adPlaybackState;
|
||||
}
|
||||
|
||||
private static AdPlaybackState insertOrUpdateInterstitialInAdGroup(
|
||||
Interstitial interstitial, int adGroupIndex, AdPlaybackState adPlaybackState) {
|
||||
private AdPlaybackState insertOrUpdateInterstitialInAdGroup(
|
||||
MediaItem mediaItem,
|
||||
Interstitial interstitial,
|
||||
AdPlaybackState adPlaybackState,
|
||||
int adGroupIndex,
|
||||
long playlistTargetDurationUs) {
|
||||
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
|
||||
int adIndexInAdGroup = adGroup.getIndexOfAdId(interstitial.id);
|
||||
if (adIndexInAdGroup != C.INDEX_UNSET) {
|
||||
@ -846,6 +1051,18 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
.setUri(interstitial.assetUri)
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build());
|
||||
} else {
|
||||
Object adsId = checkNotNull(adPlaybackState.adsId);
|
||||
checkNotNull(unresolvedAssetLists.get(adsId))
|
||||
.put(
|
||||
adGroup.timeUs != C.TIME_END_OF_SOURCE ? adGroup.timeUs : Long.MAX_VALUE,
|
||||
new AssetListData(
|
||||
mediaItem,
|
||||
adsId,
|
||||
interstitial,
|
||||
adGroupIndex,
|
||||
adIndexInAdGroup,
|
||||
playlistTargetDurationUs));
|
||||
}
|
||||
return adPlaybackState;
|
||||
}
|
||||
@ -902,24 +1119,56 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(
|
||||
Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason) {
|
||||
if (reason != DISCONTINUITY_REASON_AUTO_TRANSITION
|
||||
|| player == null
|
||||
|| oldPosition.mediaItem == null
|
||||
|| oldPosition.adGroupIndex == C.INDEX_UNSET) {
|
||||
return;
|
||||
}
|
||||
player.getCurrentTimeline().getPeriod(oldPosition.periodIndex, period);
|
||||
@Nullable Object adsId = period.adPlaybackState.adsId;
|
||||
if (adsId != null && activeAdPlaybackStates.containsKey(adsId)) {
|
||||
markAdAsPlayedAndNotifyListeners(
|
||||
oldPosition.mediaItem, adsId, oldPosition.adGroupIndex, oldPosition.adIndexInAdGroup);
|
||||
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
|
||||
if (timeline.isEmpty()) {
|
||||
cancelPendingAssetListResolutionMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(int playbackState) {
|
||||
public void onPositionDiscontinuity(
|
||||
Player.PositionInfo oldPosition,
|
||||
Player.PositionInfo newPosition,
|
||||
@Player.DiscontinuityReason int reason) {
|
||||
if (player == null
|
||||
|| oldPosition.mediaItem == null
|
||||
|| newPosition.mediaItem == null
|
||||
|| reason == DISCONTINUITY_REASON_REMOVE) {
|
||||
cancelPendingAssetListResolutionMessage();
|
||||
return;
|
||||
}
|
||||
Timeline currentTimeline = player.getCurrentTimeline();
|
||||
AdPlaybackState adPlaybackState =
|
||||
currentTimeline.getPeriod(newPosition.periodIndex, period).adPlaybackState;
|
||||
@Nullable Object adsId = adPlaybackState.adsId;
|
||||
if (adsId == null || !activeAdPlaybackStates.containsKey(adsId)) {
|
||||
// Currently playing a period without ads, or an ad period not managed by this ads loader.
|
||||
cancelPendingAssetListResolutionMessage();
|
||||
return;
|
||||
}
|
||||
if ((reason == DISCONTINUITY_REASON_AUTO_TRANSITION || reason == DISCONTINUITY_REASON_SKIP)
|
||||
&& oldPosition.adGroupIndex != C.INDEX_UNSET) {
|
||||
currentTimeline.getPeriod(oldPosition.periodIndex, period);
|
||||
markAdAsPlayedAndNotifyListeners(
|
||||
oldPosition.mediaItem, adsId, oldPosition.adGroupIndex, oldPosition.adIndexInAdGroup);
|
||||
} else if (reason == DISCONTINUITY_REASON_SEEK
|
||||
|| reason == DISCONTINUITY_REASON_SEEK_ADJUSTMENT) {
|
||||
long windowPositionUs = msToUs(newPosition.contentPositionMs);
|
||||
long assetListWindowPositionUs =
|
||||
getUnresolvedAssetListWindowPositionForContentPositionUs(
|
||||
windowPositionUs, currentTimeline, newPosition.periodIndex);
|
||||
maybeExecuteOrSetNextAssetListResolutionMessage(
|
||||
adsId,
|
||||
currentTimeline,
|
||||
newPosition.mediaItemIndex,
|
||||
assetListWindowPositionUs != C.TIME_UNSET
|
||||
? assetListWindowPositionUs
|
||||
: windowPositionUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||
Player player = HlsInterstitialsAdsLoader.this.player;
|
||||
if (playbackState != Player.STATE_ENDED || player == null || !player.isPlayingAd()) {
|
||||
return;
|
||||
@ -938,7 +1187,9 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
private void markAdAsPlayedAndNotifyListeners(
|
||||
MediaItem mediaItem, Object adsId, int adGroupIndex, int adIndexInAdGroup) {
|
||||
@Nullable AdPlaybackState adPlaybackState = activeAdPlaybackStates.get(adsId);
|
||||
if (adPlaybackState != null) {
|
||||
if (adPlaybackState != null
|
||||
&& adPlaybackState.getAdGroup(adGroupIndex).states[adIndexInAdGroup]
|
||||
== AD_STATE_AVAILABLE) {
|
||||
adPlaybackState = adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup);
|
||||
putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
|
||||
notifyListeners(
|
||||
@ -946,4 +1197,205 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LoaderCallback implements Loader.Callback<ParsingLoadable<AssetList>> {
|
||||
|
||||
private final AssetListData assetListData;
|
||||
|
||||
/** Creates an instance. */
|
||||
public LoaderCallback(AssetListData assetListData) {
|
||||
this.assetListData = assetListData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(
|
||||
ParsingLoadable<AssetList> loadable, long elapsedRealtimeMs, long loadDurationMs) {
|
||||
@Nullable AssetList assetList = loadable.getResult();
|
||||
AdPlaybackState adPlaybackState = activeAdPlaybackStates.get(assetListData.adsId);
|
||||
if (adPlaybackState == null || assetList == null || assetList.assets.isEmpty()) {
|
||||
if (adPlaybackState != null) {
|
||||
handleAssetResolutionFailed(new IOException("empty asset list"), /* cancelled= */ false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(assetListData.adGroupIndex);
|
||||
long oldAdDurationUs =
|
||||
adGroup.durationsUs[assetListData.adIndexInAdGroup] != C.TIME_UNSET
|
||||
? adGroup.durationsUs[assetListData.adIndexInAdGroup]
|
||||
: 0;
|
||||
int oldAdCount = adGroup.count;
|
||||
long sumOfAssetListAdDurationUs = 0L;
|
||||
if (assetList.assets.size() > 1) {
|
||||
// expanding to multiple ads
|
||||
adPlaybackState =
|
||||
adPlaybackState.withAdCount(
|
||||
assetListData.adGroupIndex, oldAdCount + assetList.assets.size() - 1);
|
||||
// Re-fetch ad group after ad count changed
|
||||
adGroup = adPlaybackState.getAdGroup(assetListData.adGroupIndex);
|
||||
}
|
||||
int adIndex = assetListData.adIndexInAdGroup;
|
||||
long[] newDurationsUs = adGroup.durationsUs.clone();
|
||||
for (int i = 0; i < assetList.assets.size(); i++) {
|
||||
Asset asset = assetList.assets.get(i);
|
||||
if (i > 0) {
|
||||
adIndex = oldAdCount + i - 1;
|
||||
}
|
||||
newDurationsUs[adIndex] = asset.durationUs;
|
||||
sumOfAssetListAdDurationUs += asset.durationUs;
|
||||
MediaItem mediaItem =
|
||||
new MediaItem.Builder()
|
||||
.setUri(asset.uri)
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build();
|
||||
adPlaybackState =
|
||||
adPlaybackState.withAvailableAdMediaItem(
|
||||
assetListData.adGroupIndex, adIndex, mediaItem);
|
||||
}
|
||||
adPlaybackState =
|
||||
adPlaybackState.withAdDurationsUs(assetListData.adGroupIndex, newDurationsUs);
|
||||
if (assetListData.interstitial.resumeOffsetUs == C.TIME_UNSET) {
|
||||
adGroup = adPlaybackState.getAdGroup(assetListData.adGroupIndex);
|
||||
long newContentResumeOffsetUs =
|
||||
adGroup.contentResumeOffsetUs - oldAdDurationUs + sumOfAssetListAdDurationUs;
|
||||
adPlaybackState =
|
||||
adPlaybackState.withContentResumeOffsetUs(
|
||||
assetListData.adGroupIndex, newContentResumeOffsetUs);
|
||||
}
|
||||
putAndNotifyAdPlaybackStateUpdate(assetListData.adsId, adPlaybackState);
|
||||
notifyListeners(
|
||||
listener ->
|
||||
listener.onAssetListLoadCompleted(
|
||||
assetListData.mediaItem,
|
||||
assetListData.adsId,
|
||||
assetListData.adGroupIndex,
|
||||
assetListData.adIndexInAdGroup,
|
||||
assetList));
|
||||
maybeContinueAssetResolution();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(
|
||||
ParsingLoadable<AssetList> loadable,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
boolean released) {
|
||||
handleAssetResolutionFailed(/* error= */ null, /* cancelled= */ true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader.LoadErrorAction onLoadError(
|
||||
ParsingLoadable<AssetList> loadable,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
IOException error,
|
||||
int errorCount) {
|
||||
handleAssetResolutionFailed(error, /* cancelled= */ false);
|
||||
return Loader.DONT_RETRY;
|
||||
}
|
||||
|
||||
private void handleAssetResolutionFailed(@Nullable IOException error, boolean cancelled) {
|
||||
notifyAssetResolutionFailed(
|
||||
assetListData.adsId, assetListData.adGroupIndex, assetListData.adIndexInAdGroup);
|
||||
notifyListeners(
|
||||
listener ->
|
||||
listener.onAssetListLoadFailed(
|
||||
assetListData.mediaItem,
|
||||
assetListData.adsId,
|
||||
assetListData.adGroupIndex,
|
||||
assetListData.adIndexInAdGroup,
|
||||
error,
|
||||
cancelled));
|
||||
maybeContinueAssetResolution();
|
||||
}
|
||||
|
||||
private void maybeContinueAssetResolution() {
|
||||
ExoPlayer player = HlsInterstitialsAdsLoader.this.player;
|
||||
if (player == null
|
||||
|| player.getPlaybackState() == STATE_IDLE
|
||||
|| !assetListData.mediaItem.equals(player.getCurrentMediaItem())) {
|
||||
return;
|
||||
}
|
||||
long contentPositionUs = msToUs(player.getContentPosition());
|
||||
Timeline currentTimeline = player.getCurrentTimeline();
|
||||
long assetListTimeUsForPositionUs =
|
||||
getUnresolvedAssetListWindowPositionForContentPositionUs(
|
||||
contentPositionUs, currentTimeline, player.getCurrentPeriodIndex());
|
||||
maybeExecuteOrSetNextAssetListResolutionMessage(
|
||||
assetListData.adsId,
|
||||
currentTimeline,
|
||||
player.getCurrentMediaItemIndex(),
|
||||
/* windowPositionUs= */ assetListTimeUsForPositionUs != C.TIME_UNSET
|
||||
? assetListTimeUsForPositionUs
|
||||
: contentPositionUs);
|
||||
}
|
||||
}
|
||||
|
||||
private static class RunnableAtPosition implements Runnable {
|
||||
public final long adStartTimeUs;
|
||||
private final long targetDurationUs;
|
||||
private final Runnable runnable;
|
||||
|
||||
/** Creates an instance. */
|
||||
public RunnableAtPosition(long adStartTimeUs, long targetDurationUs, Runnable runnable) {
|
||||
this.adStartTimeUs = adStartTimeUs;
|
||||
this.targetDurationUs = targetDurationUs;
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
private static class AssetListData {
|
||||
private final MediaItem mediaItem;
|
||||
private final Object adsId;
|
||||
private final int adGroupIndex;
|
||||
private final int adIndexInAdGroup;
|
||||
private final long targetDurationUs;
|
||||
private final Interstitial interstitial;
|
||||
|
||||
/** Create an instance. */
|
||||
public AssetListData(
|
||||
MediaItem mediaItem,
|
||||
Object adsId,
|
||||
Interstitial interstitial,
|
||||
int adGroupIndex,
|
||||
int adIndexInAdGroup,
|
||||
long targetDurationUs) {
|
||||
checkArgument(interstitial.assetListUri != null);
|
||||
this.mediaItem = mediaItem;
|
||||
this.adsId = adsId;
|
||||
this.adGroupIndex = adGroupIndex;
|
||||
this.adIndexInAdGroup = adIndexInAdGroup;
|
||||
this.targetDurationUs = targetDurationUs;
|
||||
this.interstitial = interstitial;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (!(o instanceof AssetListData)) {
|
||||
return false;
|
||||
}
|
||||
AssetListData that = (AssetListData) o;
|
||||
return adGroupIndex == that.adGroupIndex
|
||||
&& adIndexInAdGroup == that.adIndexInAdGroup
|
||||
&& targetDurationUs == that.targetDurationUs
|
||||
&& Objects.equals(mediaItem, that.mediaItem)
|
||||
&& Objects.equals(adsId, that.adsId)
|
||||
&& Objects.equals(interstitial, that.interstitial);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = mediaItem.hashCode();
|
||||
result = 31 * result + adsId.hashCode();
|
||||
result = 31 * result + interstitial.hashCode();
|
||||
result = 31 * result + adGroupIndex;
|
||||
result = 31 * result + adIndexInAdGroup;
|
||||
result = (int) (31L * result + targetDurationUs);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -69,6 +69,7 @@ import androidx.media3.effect.ScaleAndRotateTransformation;
|
||||
import androidx.media3.effect.SingleInputVideoGraph;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
|
||||
import androidx.media3.extractor.ExtractorOutput;
|
||||
import androidx.media3.muxer.MuxerException;
|
||||
import androidx.media3.test.utils.BitmapPixelTestUtil;
|
||||
import androidx.media3.test.utils.FakeExtractorOutput;
|
||||
@ -1239,14 +1240,21 @@ public final class AndroidTestUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@linkplain FakeTrackOutput video track} from the {@link FakeExtractorOutput} or
|
||||
* {@code null} if a video track is not found.
|
||||
* Returns a {@link FakeTrackOutput} of given {@link C.TrackType} from the {@link
|
||||
* FakeExtractorOutput}.
|
||||
*
|
||||
* @param extractorOutput The {@link ExtractorOutput} to get the {@link FakeTrackOutput} from.
|
||||
* @param trackType The {@link C.TrackType}.
|
||||
* @return The {@link FakeTrackOutput} or {@code null} if a track is not found.
|
||||
*/
|
||||
@Nullable
|
||||
public static FakeTrackOutput getVideoTrackOutput(FakeExtractorOutput extractorOutput) {
|
||||
public static FakeTrackOutput getTrackOutput(
|
||||
FakeExtractorOutput extractorOutput, @C.TrackType int trackType) {
|
||||
for (int i = 0; i < extractorOutput.numberOfTracks; i++) {
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(i);
|
||||
if (MimeTypes.isVideo(checkNotNull(trackOutput.lastFormat).sampleMimeType)) {
|
||||
String sampleMimeType = checkNotNull(trackOutput.lastFormat).sampleMimeType;
|
||||
if ((trackType == C.TRACK_TYPE_AUDIO && MimeTypes.isAudio(sampleMimeType))
|
||||
|| (trackType == C.TRACK_TYPE_VIDEO && MimeTypes.isVideo(sampleMimeType))) {
|
||||
return trackOutput;
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ import static androidx.media3.transformer.AndroidTestUtil.createFrameCountingEff
|
||||
import static androidx.media3.transformer.AndroidTestUtil.createOpenGlObjects;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.generateTextureFromBitmap;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.getMuxerFactoryBasedOnApi;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.getVideoTrackOutput;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.getTrackOutput;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped;
|
||||
import static androidx.media3.transformer.ExportResult.CONVERSION_PROCESS_NA;
|
||||
import static androidx.media3.transformer.ExportResult.CONVERSION_PROCESS_TRANSCODED;
|
||||
@ -1057,7 +1057,7 @@ public class TransformerEndToEndTest {
|
||||
TestUtil.extractAllSamplesFromFilePath(mp4Extractor, checkNotNull(result.filePath));
|
||||
assertThat(result.exportResult.fileSizeBytes).isGreaterThan(0);
|
||||
List<Long> videoTimestampsUs =
|
||||
checkNotNull(getVideoTrackOutput(fakeExtractorOutput)).getSampleTimesUs();
|
||||
checkNotNull(getTrackOutput(fakeExtractorOutput, C.TRACK_TYPE_VIDEO)).getSampleTimesUs();
|
||||
assertThat(videoTimestampsUs).hasSize(270);
|
||||
assertThat(videoTimestampsUs.get(0)).isEqualTo(0);
|
||||
// The second sample is originally at 1_033_333, clipping at 100_000 results in 933_333.
|
||||
@ -1086,7 +1086,7 @@ public class TransformerEndToEndTest {
|
||||
TestUtil.extractAllSamplesFromFilePath(mp4Extractor, checkNotNull(result.filePath));
|
||||
assertThat(result.exportResult.fileSizeBytes).isGreaterThan(0);
|
||||
List<Long> videoTimestampsUs =
|
||||
checkNotNull(getVideoTrackOutput(fakeExtractorOutput)).getSampleTimesUs();
|
||||
checkNotNull(getTrackOutput(fakeExtractorOutput, C.TRACK_TYPE_VIDEO)).getSampleTimesUs();
|
||||
assertThat(videoTimestampsUs).hasSize(270);
|
||||
assertThat(videoTimestampsUs.get(0)).isEqualTo(0);
|
||||
// The second sample is originally at 1_033_333, clipping at 100_000 results in 933_333.
|
||||
@ -1887,6 +1887,7 @@ public class TransformerEndToEndTest {
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(100_000)
|
||||
.addItem(editedMediaItem)
|
||||
.setForceAudioTrack(true)
|
||||
.build(),
|
||||
new EditedMediaItemSequence.Builder(editedMediaItem).build())
|
||||
.build();
|
||||
|
@ -17,11 +17,12 @@ package androidx.media3.transformer;
|
||||
|
||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.getVideoTrackOutput;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.getTrackOutput;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.extractor.mp4.Mp4Extractor;
|
||||
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
|
||||
@ -98,7 +99,7 @@ public class TransformerGapsTest {
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(
|
||||
new Mp4Extractor(new DefaultSubtitleParserFactory()), result.filePath);
|
||||
FakeTrackOutput videoTrackOutput = getVideoTrackOutput(fakeExtractorOutput);
|
||||
FakeTrackOutput videoTrackOutput = getTrackOutput(fakeExtractorOutput, C.TRACK_TYPE_VIDEO);
|
||||
// The gap is for 1024ms with 30 fps.
|
||||
int expectedBlankFrames = 31;
|
||||
assertThat(videoTrackOutput.getSampleCount())
|
||||
@ -127,32 +128,13 @@ public class TransformerGapsTest {
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(
|
||||
new Mp4Extractor(new DefaultSubtitleParserFactory()), result.filePath);
|
||||
FakeTrackOutput videoTrackOutput = getVideoTrackOutput(fakeExtractorOutput);
|
||||
FakeTrackOutput videoTrackOutput = getTrackOutput(fakeExtractorOutput, C.TRACK_TYPE_VIDEO);
|
||||
// The gap is for 1024ms with 30 fps.
|
||||
int expectedBlankFrames = 31;
|
||||
assertThat(videoTrackOutput.getSampleCount())
|
||||
.isEqualTo(2 * MP4_ASSET.videoFrameCount + expectedBlankFrames);
|
||||
}
|
||||
|
||||
// TODO: b/391111085 - Change test when gaps at the start of the sequence are supported.
|
||||
@Test
|
||||
public void export_withTwoVideoOnlyMediaItemsAndGapAtStart_throws() {
|
||||
Transformer transformer = new Transformer.Builder(context).build();
|
||||
Composition composition =
|
||||
new Composition.Builder(
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(/* durationUs= */ 1_000_000)
|
||||
.addItem(VIDEO_ONLY_MEDIA_ITEM)
|
||||
.addItem(VIDEO_ONLY_MEDIA_ITEM)
|
||||
.build())
|
||||
.build();
|
||||
TransformerAndroidTestRunner transformerAndroidTestRunner =
|
||||
new TransformerAndroidTestRunner.Builder(context, transformer).build();
|
||||
|
||||
assertThrows(
|
||||
ExportException.class, () -> transformerAndroidTestRunner.run(testId, composition));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void export_withTwoVideoOnlyMediaItemsAndGapInMiddle_insertsBlankFramesForGap()
|
||||
throws Exception {
|
||||
@ -179,7 +161,7 @@ public class TransformerGapsTest {
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(
|
||||
new Mp4Extractor(new DefaultSubtitleParserFactory()), result.filePath);
|
||||
FakeTrackOutput videoTrackOutput = getVideoTrackOutput(fakeExtractorOutput);
|
||||
FakeTrackOutput videoTrackOutput = getTrackOutput(fakeExtractorOutput, C.TRACK_TYPE_VIDEO);
|
||||
// The gap is for 1 sec with 30 fps.
|
||||
int expectedBlankFrames = 30;
|
||||
assertThat(videoTrackOutput.getSampleCount())
|
||||
@ -212,30 +194,22 @@ public class TransformerGapsTest {
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(
|
||||
new Mp4Extractor(new DefaultSubtitleParserFactory()), result.filePath);
|
||||
FakeTrackOutput videoTrackOutput = getVideoTrackOutput(fakeExtractorOutput);
|
||||
FakeTrackOutput videoTrackOutput = getTrackOutput(fakeExtractorOutput, C.TRACK_TYPE_VIDEO);
|
||||
// The gap is for 1 sec with 30 fps.
|
||||
int expectedBlankFrames = 30;
|
||||
assertThat(videoTrackOutput.getSampleCount())
|
||||
.isEqualTo(2 * MP4_ASSET.videoFrameCount + expectedBlankFrames);
|
||||
}
|
||||
|
||||
// TODO: b/391111085 - Change test when gaps at the start of the sequence are supported.
|
||||
@Test
|
||||
public void export_withTwoMediaItemsAndGapAtStart_throws() {
|
||||
Transformer transformer = new Transformer.Builder(context).build();
|
||||
Composition composition =
|
||||
new Composition.Builder(
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(/* durationUs= */ 1_000_000)
|
||||
.addItem(AUDIO_VIDEO_MEDIA_ITEM)
|
||||
.addItem(AUDIO_VIDEO_MEDIA_ITEM)
|
||||
.build())
|
||||
.build();
|
||||
TransformerAndroidTestRunner transformerAndroidTestRunner =
|
||||
new TransformerAndroidTestRunner.Builder(context, transformer).build();
|
||||
public void buildSequence_withTwoMediaItemsAndGapAtStart_throws() {
|
||||
EditedMediaItemSequence.Builder sequenceBuilder =
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(/* durationUs= */ 1_000_000)
|
||||
.addItem(AUDIO_VIDEO_MEDIA_ITEM)
|
||||
.addItem(AUDIO_VIDEO_MEDIA_ITEM);
|
||||
|
||||
assertThrows(
|
||||
ExportException.class, () -> transformerAndroidTestRunner.run(testId, composition));
|
||||
assertThrows(IllegalArgumentException.class, sequenceBuilder::build);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -263,7 +237,7 @@ public class TransformerGapsTest {
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(
|
||||
new Mp4Extractor(new DefaultSubtitleParserFactory()), result.filePath);
|
||||
FakeTrackOutput videoTrackOutput = getVideoTrackOutput(fakeExtractorOutput);
|
||||
FakeTrackOutput videoTrackOutput = getTrackOutput(fakeExtractorOutput, C.TRACK_TYPE_VIDEO);
|
||||
// The gap is for 1 sec with 30 fps.
|
||||
int expectedBlankFrames = 30;
|
||||
assertThat(videoTrackOutput.getSampleCount())
|
||||
@ -295,7 +269,7 @@ public class TransformerGapsTest {
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(
|
||||
new Mp4Extractor(new DefaultSubtitleParserFactory()), result.filePath);
|
||||
FakeTrackOutput videoTrackOutput = getVideoTrackOutput(fakeExtractorOutput);
|
||||
FakeTrackOutput videoTrackOutput = getTrackOutput(fakeExtractorOutput, C.TRACK_TYPE_VIDEO);
|
||||
// The gap is for 1 sec with 30 fps.
|
||||
int expectedBlankFrames = 30;
|
||||
assertThat(videoTrackOutput.getSampleCount())
|
||||
@ -328,7 +302,7 @@ public class TransformerGapsTest {
|
||||
FakeExtractorOutput fakeExtractorOutput =
|
||||
TestUtil.extractAllSamplesFromFilePath(
|
||||
new Mp4Extractor(new DefaultSubtitleParserFactory()), result.filePath);
|
||||
FakeTrackOutput videoTrackOutput = getVideoTrackOutput(fakeExtractorOutput);
|
||||
FakeTrackOutput videoTrackOutput = getTrackOutput(fakeExtractorOutput, C.TRACK_TYPE_VIDEO);
|
||||
// The gap is for 1024ms with 30 fps.
|
||||
int expectedBlankFramesForAudioOnlyItem = 31;
|
||||
// The gap is for 1 sec with 30 fps.
|
||||
|
@ -98,6 +98,9 @@ public final class EditedMediaItemSequence {
|
||||
*
|
||||
* <p>A gap is a period of time with no media.
|
||||
*
|
||||
* <p>If the gap is at the start of the sequence then {@linkplain #setForceAudioTrack(boolean)
|
||||
* force audio track} flag must be set to force silent audio.
|
||||
*
|
||||
* <p>Gaps at the start of the sequence are not supported if the sequence has video.
|
||||
*
|
||||
* @param durationUs The duration of the gap, in milliseconds.
|
||||
@ -230,6 +233,9 @@ public final class EditedMediaItemSequence {
|
||||
this.editedMediaItems = builder.items.build();
|
||||
checkArgument(
|
||||
!editedMediaItems.isEmpty(), "The sequence must contain at least one EditedMediaItem.");
|
||||
checkArgument(
|
||||
!editedMediaItems.get(0).isGap() || builder.forceAudioTrack,
|
||||
"If the first item in the sequence is a Gap, then forceAudioTrack flag must be set");
|
||||
this.isLooping = builder.isLooping;
|
||||
this.forceAudioTrack = builder.forceAudioTrack;
|
||||
}
|
||||
|
@ -272,8 +272,10 @@ public final class ExoPlayerAssetLoader implements AssetLoader {
|
||||
public @Transformer.ProgressState int getProgress(ProgressHolder progressHolder) {
|
||||
if (progressState == PROGRESS_STATE_AVAILABLE) {
|
||||
long durationMs = player.getDuration();
|
||||
long positionMs = player.getCurrentPosition();
|
||||
progressHolder.progress = min((int) (positionMs * 100 / durationMs), 99);
|
||||
// The player position can become greater than the duration. This happens if the player is
|
||||
// using a StandaloneMediaClock because the renderers have ended.
|
||||
long positionMs = min(player.getCurrentPosition(), durationMs);
|
||||
progressHolder.progress = (int) (positionMs * 100 / durationMs);
|
||||
}
|
||||
return progressState;
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
Looper looper) {
|
||||
editedMediaItems = sequence.editedMediaItems;
|
||||
isLooping = sequence.isLooping;
|
||||
this.forceAudioTrack = sequence.forceAudioTrack || sequence.editedMediaItems.get(0).isGap();
|
||||
this.forceAudioTrack = sequence.forceAudioTrack;
|
||||
this.assetLoaderFactory = new GapInterceptingAssetLoaderFactory(assetLoaderFactory);
|
||||
this.compositionSettings = compositionSettings;
|
||||
sequenceAssetLoaderListener = listener;
|
||||
|
@ -481,7 +481,10 @@ public class CompositionExportTest {
|
||||
new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW)).build();
|
||||
Composition composition =
|
||||
new Composition.Builder(
|
||||
new EditedMediaItemSequence.Builder().addGap(1_000_000).build(),
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(1_000_000)
|
||||
.setForceAudioTrack(true)
|
||||
.build(),
|
||||
new EditedMediaItemSequence.Builder(audioItem1000ms).build())
|
||||
.build();
|
||||
|
||||
@ -512,6 +515,7 @@ public class CompositionExportTest {
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(100_000)
|
||||
.addItem(audioEditedMediaItem)
|
||||
.setForceAudioTrack(true)
|
||||
.build(),
|
||||
new EditedMediaItemSequence.Builder(otherAudioEditedMediaItem).build())
|
||||
.build();
|
||||
@ -591,6 +595,7 @@ public class CompositionExportTest {
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(200_000)
|
||||
.addItem(audioEditedMediaItem)
|
||||
.setForceAudioTrack(true)
|
||||
.build())
|
||||
.setTransmuxVideo(true)
|
||||
.build();
|
||||
@ -716,7 +721,10 @@ public class CompositionExportTest {
|
||||
Composition composition =
|
||||
new Composition.Builder(
|
||||
new EditedMediaItemSequence.Builder(audioItem1000ms).build(),
|
||||
new EditedMediaItemSequence.Builder().addGap(1_000_000).build())
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(1_000_000)
|
||||
.setForceAudioTrack(true)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
transformer.start(composition, outputDir.newFile().getPath());
|
||||
@ -733,8 +741,14 @@ public class CompositionExportTest {
|
||||
new TestTransformerBuilder(context).setMuxerFactory(muxerFactory).build();
|
||||
Composition composition =
|
||||
new Composition.Builder(
|
||||
new EditedMediaItemSequence.Builder().addGap(500_000).build(),
|
||||
new EditedMediaItemSequence.Builder().addGap(500_000).build())
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(500_000)
|
||||
.setForceAudioTrack(true)
|
||||
.build(),
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(500_000)
|
||||
.setForceAudioTrack(true)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
transformer.start(composition, outputDir.newFile().getPath());
|
||||
|
@ -148,7 +148,7 @@ public final class MediaItemExportTest {
|
||||
new TestTransformerBuilder(context).setMuxerFactory(muxerFactory).build();
|
||||
|
||||
EditedMediaItemSequence gapSequence =
|
||||
new EditedMediaItemSequence.Builder().addGap(500_000).build();
|
||||
new EditedMediaItemSequence.Builder().addGap(500_000).setForceAudioTrack(true).build();
|
||||
|
||||
transformer.start(new Composition.Builder(gapSequence).build(), outputDir.newFile().getPath());
|
||||
ExportResult result = TransformerTestRunner.runLooper(transformer);
|
||||
|
@ -551,7 +551,11 @@ public final class SequenceExportTest {
|
||||
new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW_VIDEO))
|
||||
.build();
|
||||
EditedMediaItemSequence sequence =
|
||||
new EditedMediaItemSequence.Builder().addGap(500_000).addItem(audioVideoItem).build();
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(500_000)
|
||||
.addItem(audioVideoItem)
|
||||
.setForceAudioTrack(true)
|
||||
.build();
|
||||
Composition composition = new Composition.Builder(sequence).build();
|
||||
|
||||
transformer.start(composition, outputDir.newFile().getPath());
|
||||
@ -567,7 +571,11 @@ public final class SequenceExportTest {
|
||||
Transformer transformer =
|
||||
new TestTransformerBuilder(context).setMuxerFactory(muxerFactory).build();
|
||||
EditedMediaItemSequence sequence =
|
||||
new EditedMediaItemSequence.Builder().addGap(300_000).addGap(200_000).build();
|
||||
new EditedMediaItemSequence.Builder()
|
||||
.addGap(300_000)
|
||||
.addGap(200_000)
|
||||
.setForceAudioTrack(true)
|
||||
.build();
|
||||
Composition composition = new Composition.Builder(sequence).build();
|
||||
|
||||
transformer.start(composition, outputDir.newFile().getPath());
|
||||
@ -614,6 +622,7 @@ public final class SequenceExportTest {
|
||||
.addGap(200_000)
|
||||
.addGap(500_000)
|
||||
.addItem(audioItem)
|
||||
.setForceAudioTrack(true)
|
||||
.build();
|
||||
Composition composition = new Composition.Builder(sequence).build();
|
||||
|
||||
@ -709,6 +718,7 @@ public final class SequenceExportTest {
|
||||
.addItem(firstAudioItem)
|
||||
.addGap(200_000)
|
||||
.addItem(secondAudioItem)
|
||||
.setForceAudioTrack(true)
|
||||
.build();
|
||||
Composition composition = new Composition.Builder(sequence).build();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user