mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Compare commits
6 Commits
4bfa154acd
...
a78d0c3994
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a78d0c3994 | ||
![]() |
cb0ea7fc95 | ||
![]() |
6cae8ab8a0 | ||
![]() |
c5b6489d5d | ||
![]() |
1b3658e357 | ||
![]() |
f9617e1f8d |
@ -63,9 +63,13 @@
|
||||
instead of `MediaCodec.BufferInfo`.
|
||||
* IMA extension:
|
||||
* Session:
|
||||
* Lower aggregation timeout for platform `MediaSession` callbacks from 500
|
||||
to 100 milliseconds and add an experimental setter to allow apps to
|
||||
configure this value.
|
||||
* UI:
|
||||
* Enable `PlayerSurface` to work with `ExoPlayer.setVideoEffects` and
|
||||
`CompositionPlayer`.
|
||||
* Fix bug where `PlayerSurface` can't be recomposed with a new `Player`.
|
||||
* Downloads:
|
||||
* Add partial download support for progressive streams. Apps can prepare a
|
||||
progressive stream with `DownloadHelper`, and request a
|
||||
|
@ -1623,7 +1623,7 @@ public interface Player {
|
||||
/** {@link #getDeviceInfo()} changed. */
|
||||
int EVENT_DEVICE_INFO_CHANGED = 29;
|
||||
|
||||
/** {@link #getDeviceVolume()} changed. */
|
||||
/** {@link #getDeviceVolume()} or {@link #isDeviceMuted()} changed. */
|
||||
int EVENT_DEVICE_VOLUME_CHANGED = 30;
|
||||
|
||||
/**
|
||||
|
@ -17,19 +17,13 @@ package androidx.media3.exoplayer.drm;
|
||||
|
||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.DecoderCounters;
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
@ -103,73 +97,4 @@ public final class DrmPlaybackTest {
|
||||
getInstrumentation().waitForIdleSync();
|
||||
assertThat(playbackException.get()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearkeyPlayback_withLateThresholdToDropDecoderInput_dropsInputBuffers()
|
||||
throws Exception {
|
||||
// The API 21 emulator doesn't have a secure decoder. Due to b/18678462 MediaCodecUtil pretends
|
||||
// that there is a secure decoder so we must only run this test on API 21 - i.e. we cannot
|
||||
// assumeTrue() on getDecoderInfos.
|
||||
assumeTrue(Util.SDK_INT > 21);
|
||||
Context context = getInstrumentation().getContext();
|
||||
MockWebServer mockWebServer = new MockWebServer();
|
||||
mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(CLEARKEY_RESPONSE));
|
||||
mockWebServer.start();
|
||||
|
||||
MediaItem mediaItem =
|
||||
new MediaItem.Builder()
|
||||
.setUri("asset:///media/drm/sample_fragmented_clearkey.mp4")
|
||||
.setDrmConfiguration(
|
||||
new MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID)
|
||||
.setLicenseUri(mockWebServer.url("license").toString())
|
||||
.build())
|
||||
.build();
|
||||
AtomicReference<ExoPlayer> player = new AtomicReference<>();
|
||||
ConditionVariable playbackComplete = new ConditionVariable();
|
||||
AtomicReference<PlaybackException> playbackException = new AtomicReference<>();
|
||||
AtomicReference<DecoderCounters> decoderCountersAtomicReference = new AtomicReference<>();
|
||||
getInstrumentation()
|
||||
.runOnMainSync(
|
||||
() -> {
|
||||
player.set(
|
||||
new ExoPlayer.Builder(
|
||||
context,
|
||||
new DefaultRenderersFactory(context)
|
||||
.experimentalSetLateThresholdToDropDecoderInputUs(-100_000_000L),
|
||||
new DefaultMediaSourceFactory(context)
|
||||
.experimentalSetCodecsToParseWithinGopSampleDependencies(
|
||||
C.VIDEO_CODEC_FLAG_H264))
|
||||
.build());
|
||||
player
|
||||
.get()
|
||||
.addListener(
|
||||
new Player.Listener() {
|
||||
@Override
|
||||
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED) {
|
||||
decoderCountersAtomicReference.set(
|
||||
player.get().getVideoDecoderCounters());
|
||||
playbackComplete.open();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(PlaybackException error) {
|
||||
playbackException.set(error);
|
||||
playbackComplete.open();
|
||||
}
|
||||
});
|
||||
player.get().setMediaItem(mediaItem);
|
||||
player.get().prepare();
|
||||
player.get().play();
|
||||
});
|
||||
|
||||
playbackComplete.block();
|
||||
getInstrumentation().runOnMainSync(() -> player.get().release());
|
||||
getInstrumentation().waitForIdleSync();
|
||||
assertThat(playbackException.get()).isNull();
|
||||
// Which input buffers are dropped first depends on the number of MediaCodec buffer slots.
|
||||
// This means the asserts cannot be isEqualTo.
|
||||
assertThat(decoderCountersAtomicReference.get().droppedInputBufferCount).isAtLeast(1);
|
||||
}
|
||||
}
|
||||
|
@ -1552,6 +1552,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
|
||||
// Make sure to decode and render the last frame.
|
||||
return false;
|
||||
}
|
||||
if (buffer.isEncrypted()) {
|
||||
// Commonly used decryption algorithms require updating the initialization vector for each
|
||||
// block processed. Skipping input buffers before the decoder is not allowed.
|
||||
return false;
|
||||
}
|
||||
boolean shouldSkipDecoderInputBuffer = isBufferBeforeStartTime(buffer);
|
||||
if (!shouldSkipDecoderInputBuffer && !shouldDropDecoderInputBuffers) {
|
||||
return false;
|
||||
|
@ -32,7 +32,11 @@ 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 androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.SNAP_TYPE_IN;
|
||||
import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.SNAP_TYPE_OUT;
|
||||
import static java.lang.Math.abs;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
@ -633,7 +637,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
window.positionInFirstPeriodUs,
|
||||
checkNotNull(insertedInterstitialIds.get(adsId)))
|
||||
: mapInterstitialsForVod(
|
||||
window.mediaItem,
|
||||
window,
|
||||
mediaPlaylist,
|
||||
adPlaybackState,
|
||||
checkNotNull(insertedInterstitialIds.get(adsId)));
|
||||
@ -913,13 +917,15 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
ArrayList<Interstitial> interstitials = new ArrayList<>(mediaPlaylist.interstitials);
|
||||
for (int i = 0; i < interstitials.size(); i++) {
|
||||
Interstitial interstitial = interstitials.get(i);
|
||||
long positionInPlaylistWindowUs =
|
||||
interstitial.cue.contains(CUE_TRIGGER_PRE)
|
||||
? 0L
|
||||
: (interstitial.startDateUnixUs - mediaPlaylist.startTimeUs);
|
||||
if (insertedInterstitialIds.contains(interstitial.id)
|
||||
|| interstitial.cue.contains(CUE_TRIGGER_POST)
|
||||
|| positionInPlaylistWindowUs < 0) {
|
||||
|| interstitial.cue.contains(CUE_TRIGGER_POST)) {
|
||||
continue;
|
||||
}
|
||||
long positionInPlaylistWindowUs =
|
||||
resolveInterstitialStartTimeUs(interstitial, mediaPlaylist) - mediaPlaylist.startTimeUs;
|
||||
if (positionInPlaylistWindowUs < 0 || mediaPlaylist.durationUs < positionInPlaylistWindowUs) {
|
||||
// Ignore when behind the window including C.TIME_UNSET and C.TIME_END_OF_SOURCE.
|
||||
// When not yet in the window we wait until the window advances.
|
||||
continue;
|
||||
}
|
||||
long timeUs = windowPositionInPeriodUs + positionInPlaylistWindowUs;
|
||||
@ -935,18 +941,18 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
isNewAdGroup = false;
|
||||
break;
|
||||
} else if (adGroup.timeUs < timeUs) {
|
||||
// Insert at index after group before interstitial.
|
||||
// Insert at index after group behind interstitial.
|
||||
insertionIndex = adGroupIndex + 1;
|
||||
break;
|
||||
}
|
||||
// Interstitial is before the ad group. Possible insertion index.
|
||||
// Interstitial is behind the ad group. Possible insertion index.
|
||||
insertionIndex = adGroupIndex;
|
||||
}
|
||||
if (isNewAdGroup) {
|
||||
if (insertionIndex < getLowestValidAdGroupInsertionIndex(adPlaybackState)) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Skipping insertion of interstitial attempted to be inserted before an already"
|
||||
"Skipping insertion of interstitial attempted to be inserted behind an already"
|
||||
+ " initialized ad group.");
|
||||
continue;
|
||||
}
|
||||
@ -965,36 +971,49 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
}
|
||||
|
||||
private AdPlaybackState mapInterstitialsForVod(
|
||||
MediaItem mediaItem,
|
||||
Window window,
|
||||
HlsMediaPlaylist mediaPlaylist,
|
||||
AdPlaybackState adPlaybackState,
|
||||
Set<String> insertedInterstitialIds) {
|
||||
checkArgument(adPlaybackState.adGroupCount == 0);
|
||||
checkArgument(adPlaybackState.adGroupCount == adPlaybackState.removedAdGroupCount);
|
||||
ImmutableList<Interstitial> interstitials = mediaPlaylist.interstitials;
|
||||
long clippedWindowStartTimeUs = mediaPlaylist.startTimeUs + window.positionInFirstPeriodUs;
|
||||
long clippedWindowEndTimeUs = clippedWindowStartTimeUs + window.durationUs;
|
||||
for (int i = 0; i < interstitials.size(); i++) {
|
||||
Interstitial interstitial = interstitials.get(i);
|
||||
long timeUs;
|
||||
if (interstitial.cue.contains(CUE_TRIGGER_PRE)) {
|
||||
timeUs = 0L;
|
||||
} else if (interstitial.cue.contains(CUE_TRIGGER_POST)) {
|
||||
timeUs = C.TIME_END_OF_SOURCE;
|
||||
} else {
|
||||
timeUs = interstitial.startDateUnixUs - mediaPlaylist.startTimeUs;
|
||||
long interstitialStartTimeUs = resolveInterstitialStartTimeUs(interstitial, mediaPlaylist);
|
||||
if (interstitialStartTimeUs < clippedWindowStartTimeUs
|
||||
&& interstitial.cue.contains(CUE_TRIGGER_PRE)) {
|
||||
// Declared pre roll: move to the start of the clipped window.
|
||||
interstitialStartTimeUs = clippedWindowStartTimeUs;
|
||||
} else if (interstitialStartTimeUs > clippedWindowEndTimeUs
|
||||
&& interstitial.cue.contains(CUE_TRIGGER_POST)) {
|
||||
// Declared post roll: move to the end of the clipped window.
|
||||
interstitialStartTimeUs = clippedWindowEndTimeUs;
|
||||
} else if (interstitialStartTimeUs < clippedWindowStartTimeUs
|
||||
|| clippedWindowEndTimeUs < interstitialStartTimeUs) {
|
||||
// Ignore interstitials before or after the window that are not explicit pre or post rolls.
|
||||
continue;
|
||||
}
|
||||
long timeUs =
|
||||
clippedWindowEndTimeUs == interstitialStartTimeUs
|
||||
? C.TIME_END_OF_SOURCE
|
||||
: interstitialStartTimeUs - mediaPlaylist.startTimeUs;
|
||||
int adGroupIndex =
|
||||
adPlaybackState.getAdGroupIndexForPositionUs(timeUs, mediaPlaylist.durationUs);
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
// There is no ad group before or at the interstitials position.
|
||||
adGroupIndex = 0;
|
||||
adPlaybackState = adPlaybackState.withNewAdGroup(/* adGroupIndex= */ 0, timeUs);
|
||||
adGroupIndex = adPlaybackState.removedAdGroupCount;
|
||||
adPlaybackState =
|
||||
adPlaybackState.withNewAdGroup(adPlaybackState.removedAdGroupCount, timeUs);
|
||||
} else if (adPlaybackState.getAdGroup(adGroupIndex).timeUs != timeUs) {
|
||||
// There is an ad group before the interstitials. Insert after that index.
|
||||
// There is an ad group before the interstitial. Insert after that index.
|
||||
adGroupIndex++;
|
||||
adPlaybackState = adPlaybackState.withNewAdGroup(adGroupIndex, timeUs);
|
||||
}
|
||||
adPlaybackState =
|
||||
insertOrUpdateInterstitialInAdGroup(
|
||||
mediaItem,
|
||||
window.mediaItem,
|
||||
interstitial,
|
||||
adPlaybackState,
|
||||
adGroupIndex,
|
||||
@ -1021,7 +1040,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
adIndexInAdGroup = max(adGroup.count, 0);
|
||||
// Append duration of new interstitial into existing ad durations.
|
||||
long interstitialDurationUs =
|
||||
getInterstitialDurationUs(interstitial, /* defaultDurationUs= */ C.TIME_UNSET);
|
||||
resolveInterstitialDurationUs(interstitial, /* defaultDurationUs= */ C.TIME_UNSET);
|
||||
long[] adDurations;
|
||||
if (adIndexInAdGroup == 0) {
|
||||
adDurations = new long[1];
|
||||
@ -1081,7 +1100,8 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
return adPlaybackState.removedAdGroupCount;
|
||||
}
|
||||
|
||||
private static long getInterstitialDurationUs(Interstitial interstitial, long defaultDurationUs) {
|
||||
private static long resolveInterstitialDurationUs(
|
||||
Interstitial interstitial, long defaultDurationUs) {
|
||||
if (interstitial.playoutLimitUs != C.TIME_UNSET) {
|
||||
return interstitial.playoutLimitUs;
|
||||
} else if (interstitial.durationUs != C.TIME_UNSET) {
|
||||
@ -1094,6 +1114,45 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|
||||
return defaultDurationUs;
|
||||
}
|
||||
|
||||
private static long resolveInterstitialStartTimeUs(
|
||||
Interstitial interstitial, HlsMediaPlaylist mediaPlaylist) {
|
||||
if (interstitial.cue.contains(CUE_TRIGGER_PRE)) {
|
||||
return mediaPlaylist.startTimeUs;
|
||||
} else if (interstitial.cue.contains(CUE_TRIGGER_POST)) {
|
||||
return mediaPlaylist.startTimeUs + mediaPlaylist.durationUs;
|
||||
} else if (interstitial.snapTypes.contains(SNAP_TYPE_OUT)) {
|
||||
return getClosestSegmentBoundaryUs(interstitial.startDateUnixUs, mediaPlaylist);
|
||||
} else if (interstitial.snapTypes.contains(SNAP_TYPE_IN)) {
|
||||
long resumeOffsetUs =
|
||||
interstitial.resumeOffsetUs != C.TIME_UNSET
|
||||
? interstitial.resumeOffsetUs
|
||||
: resolveInterstitialDurationUs(interstitial, /* defaultDurationUs= */ 0L);
|
||||
return getClosestSegmentBoundaryUs(
|
||||
interstitial.startDateUnixUs + resumeOffsetUs, mediaPlaylist)
|
||||
- resumeOffsetUs;
|
||||
} else {
|
||||
return interstitial.startDateUnixUs;
|
||||
}
|
||||
}
|
||||
|
||||
private static long getClosestSegmentBoundaryUs(long unixTimeUs, HlsMediaPlaylist mediaPlaylist) {
|
||||
long positionInPlaylistUs = unixTimeUs - mediaPlaylist.startTimeUs;
|
||||
if (positionInPlaylistUs <= 0 || mediaPlaylist.segments.isEmpty()) {
|
||||
return mediaPlaylist.startTimeUs;
|
||||
} else if (positionInPlaylistUs >= mediaPlaylist.durationUs) {
|
||||
return mediaPlaylist.startTimeUs + mediaPlaylist.durationUs;
|
||||
}
|
||||
long segmentIndex =
|
||||
min(
|
||||
positionInPlaylistUs / mediaPlaylist.targetDurationUs,
|
||||
mediaPlaylist.segments.size() - 1);
|
||||
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get((int) segmentIndex);
|
||||
return positionInPlaylistUs - segment.relativeStartTimeUs
|
||||
< abs(positionInPlaylistUs - (segment.relativeStartTimeUs + segment.durationUs))
|
||||
? mediaPlaylist.startTimeUs + segment.relativeStartTimeUs
|
||||
: mediaPlaylist.startTimeUs + segment.relativeStartTimeUs + segment.durationUs;
|
||||
}
|
||||
|
||||
private class PlayerListener implements Player.Listener {
|
||||
|
||||
private final Period period = new Period();
|
||||
|
@ -265,6 +265,10 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main2.0.ts\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main3.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
@ -332,6 +336,108 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleContentTimelineChanged_clippedWindow_translatedToClippedWindow()
|
||||
throws IOException {
|
||||
AdPlaybackState actual =
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\nmain1.0.ts\n"
|
||||
+ "#EXTINF:6,\nmain2.0.ts\n"
|
||||
+ "#EXTINF:6,\nmain3.0.ts\n"
|
||||
+ "#EXTINF:6,\nmain4.0.ts\n" // ends at 24_000_000 -> 21:56:04.000Z
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\"," // pre roll
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\","
|
||||
+ "CUE=\"PRE\"," // Explicit pre roll. Aligned to clip start.
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad1-0\"," // ignored
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:42.999Z\"," // non-pre roll behind clipped window
|
||||
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2-0\"," // ignored
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:39.999Z\"," // snaps to the playlist window start
|
||||
+ "X-SNAP=\"OUT\""
|
||||
+ "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad3-0\"," // mid roll at 15_000_000
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:55.000Z\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-3-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad4-0\"," // post roll 0
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:56:00.321Z\"," // exact match at end of clip
|
||||
+ "X-ASSET-URI=\"http://example.com/media-4-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad4-1\"," // ignored
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:56:00.322Z\"," // after end of clip
|
||||
+ "X-ASSET-URI=\"http://example.com/media-4-1.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad4-2\"," // post roll 1
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2050-01-02T21:55:08.000Z\","
|
||||
+ "CUE=\"POST\"," // explicit post roll
|
||||
+ "X-ASSET-URI=\"http://example.com/media-4-2.m3u8\"\n",
|
||||
adsLoader,
|
||||
/* windowIndex= */ 1,
|
||||
6_000_123L, // clipped to 6s after start of period
|
||||
20_321_000L); // clipped to 4s before end of period
|
||||
assertThat(actual)
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", 6_000_123L, 15_000_000L, C.TIME_END_OF_SOURCE)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 1, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 2, 2)
|
||||
.withAdId(0, 0, "ad0-0")
|
||||
.withAdId(1, 0, "ad3-0")
|
||||
.withAdId(2, 0, "ad4-0")
|
||||
.withAdId(2, 1, "ad4-2")
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 1,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-3-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 2,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-4-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 2,
|
||||
/* adIndexInAdGroup= */ 1,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-4-2.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleContentTimelineChanged_3preRolls_mergedIntoSinglePreRollAdGroup()
|
||||
throws IOException {
|
||||
@ -416,18 +522,21 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "ID=\"ad0-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\","
|
||||
+ "END-DATE=\"2020-01-02T21:55:46.000Z\"," // adds to resume offset
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-1\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\","
|
||||
+ "DURATION=1.1," // adds to resume offset
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-2\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:44.000Z\","
|
||||
+ "PLANNED-DURATION=1.2," // adds to resume offset
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-2.m3u8\""
|
||||
+ "\n";
|
||||
|
||||
@ -440,9 +549,9 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 4_000_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 2_000_000L, 1_100_000L, 1_200_000L)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 3)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 0L)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 4_300_000L)
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1")
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, "ad0-2")
|
||||
@ -472,51 +581,49 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
@Test
|
||||
public void handleContentTimelineChanged_3postRolls_mergedIntoSinglePostRollAdGroup()
|
||||
throws IOException {
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:30.000Z\","
|
||||
+ "END-DATE=\"2020-01-02T21:55:31.000Z\","
|
||||
+ "CUE=\"POST\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-1\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "CUE=\"POST\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:40.000Z\","
|
||||
+ "DURATION=1.1,"
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-2\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:51.000Z\","
|
||||
+ "CUE=\"POST\","
|
||||
+ "PLANNED-DURATION=1.2,"
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-2.m3u8\""
|
||||
+ "\n";
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString,
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:30.000Z\","
|
||||
+ "CUE=\"POST\"," // cued as post roll
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-1\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:46.000Z\"," // exact match
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-2\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:46.001Z\"," // late but snaps to post roll.
|
||||
+ "X-SNAP=\"OUT\""
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-2.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-3\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:46.001Z\"," // late and hence ignored.
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-3.m3u8\""
|
||||
+ "\n",
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L, 1_100_000L, 1_200_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 3)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 3_300_000L)
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1")
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, "ad0-2")
|
||||
@ -909,25 +1016,608 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleContentTimelineChanged_noDurationSet_durationTimeUnset() throws IOException {
|
||||
String playlistString =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:41.123Z\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n";
|
||||
|
||||
public void handleContentTimelineChanged_snapOut_snapToClosestSegmentBoundaryOfStartPosition()
|
||||
throws IOException {
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString,
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6.111,\nmain1.ts\n"
|
||||
+ "#EXTINF:6.111,\nmain2.ts\n"
|
||||
+ "#EXTINF:6.111,\nmain3.ts\n" // end of window: 18_333_000 -> 21:55:58.333
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\"," // post roll
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:58.000Z\"," // snap to 21:55:58.333
|
||||
+ "X-SNAP=\"OUT\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad1-0\"," // mid roll
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:46.000Z\"," // snap to 21:55:46.111
|
||||
+ "X-SNAP=\"OUT\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad3-0\"," // pre roll
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:40.123Z\"," // snap to 21:55:40.000
|
||||
+ "X-SNAP=\"OUT\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-3-0.m3u8\""
|
||||
+ "\n",
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState(
|
||||
"adsId", /* adGroupTimesUs...= */ 0L, 6_111_000L, C.TIME_END_OF_SOURCE)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 1, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 2, 1)
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad3-0")
|
||||
.withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0")
|
||||
.withAdId(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, "ad0-0")
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-3-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 1,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-1-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 2,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleContentTimelineChanged_snapOutLive_snapToClosestSegmentBoundaryOfStartPosition()
|
||||
throws IOException {
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates(
|
||||
adsLoader,
|
||||
/* startAdsLoader= */ true,
|
||||
/* windowOffsetInFirstPeriodUs= */ 2_000_123L, // window offset!
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\"," // mid roll
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:46.000\"," // snap to 6.111 + 2_000_123
|
||||
+ "X-SNAP=\"OUT\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad1-0\"," // mid roll
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:52.999Z\"," // snap to 12.222 + 2_000_123
|
||||
+ "X-SNAP=\"OUT\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2-0\"," // mid roll at end of window
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2050-01-02T21:56:04.000Z\"," // snap to 18.333 + 2_000_123
|
||||
+ "X-SNAP=\"OUT\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" // 2s offset in period
|
||||
+ "#EXTINF:6.111,\nmain1.0.ts\n"
|
||||
+ "#EXTINF:6.111,\nmain2.0.ts\n"
|
||||
+ "#EXTINF:6.111,\nmain3.0.ts\n")) // window end time at 18.333 -> 21:55:58.333
|
||||
.containsExactly(
|
||||
new AdPlaybackState(
|
||||
"adsId", /* adGroupTimesUs...= */ 8_111_123L, 14_222_123L, 20_333_123L)
|
||||
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 1, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 2, 1)
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
|
||||
.withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0")
|
||||
.withAdId(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, "ad2-0")
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 1,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-1-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 2,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-2-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfResumptionPosition()
|
||||
throws IOException {
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6.111,\nmain1.ts\n"
|
||||
+ "#EXTINF:6.111,\nmain2.ts\n" // segment start at 6.111
|
||||
+ "#EXTINF:6.111,\nmain3.ts\n" // end of window at 18.333
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:39.900\"," // snap to 12.222 - 12.222 -> 0L
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "DURATION=12.222,"
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad1-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:51.222\"," // aligned 12.222 - 3.222 -> 9_000L
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "DURATION=3.222,"
|
||||
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:54.678\"," // snap to end of window - 4.333
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "DURATION=4.333,"
|
||||
+ "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\""
|
||||
+ "\n",
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 9_000_000L, 14_000_000L)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 1, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 2, 1)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 12_222_000L)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 1, 3_222_000L)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 2, 4_333_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 12_222_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 1, 3_222_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 2, 4_333_000L)
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
|
||||
.withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0")
|
||||
.withAdId(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, "ad2-0")
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 1,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-1-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 2,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-2-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
handleContentTimelineChanged_snapInLive_snapToClosestSegmentBoundaryOfResumptionPosition()
|
||||
throws IOException {
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates(
|
||||
adsLoader,
|
||||
/* startAdsLoader= */ true,
|
||||
/* windowOffsetInFirstPeriodUs= */ 123L,
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:39.900\"," // snap to (12.222 - 12.222) -> 0L
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "X-RESUME-OFFSET=12.222,"
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad1-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:51.222\"," // snap to (12.222 - 3.22) -> 9_000L
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "X-RESUME-OFFSET=3.222,"
|
||||
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:54.678\"," // snap to (end of window - 4.333)
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "DURATION=4.333,"
|
||||
+ "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6.111,\nmain1.ts\n"
|
||||
+ "#EXTINF:6.111,\nmain2.ts\n" // segment start at 6.111
|
||||
+ "#EXTINF:6.111,\nmain3.ts\n")) // segment start at 12.222
|
||||
.containsExactly(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 123L, 9_000_123L, 14_000_123L)
|
||||
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 1, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 2, 1)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 12_222_000L)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 1, 3_222_000L)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 2, 4_333_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 2, 4_333_000L)
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
|
||||
.withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0")
|
||||
.withAdId(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, "ad2-0")
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 1,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-1-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 2,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-2-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
handleContentTimelineChanged_snapInFarBeforeOrAfterWindow_snapToStartOfWindowAndPostRoll()
|
||||
throws IOException {
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6.111,\nmain1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\"," // pre roll
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"1990-01-02T00:00:00.000\"," // snap to start of window.
|
||||
+ "DURATION=1.0,"
|
||||
+ "X-RESUME-OFFSET=0.0," // with no offset SNAP_IN => SNAP_OUT
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad1-0\"," // ignored
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"1990-01-02T00:00:00.000\"," // snap end to start of window
|
||||
+ "DURATION=1.0," // translate start of ad back to 1s behind window
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2-0\"," // post roll
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2050-01-02T21:55:00.900\"," // snap to end of window
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\""
|
||||
+ "\n",
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, C.TIME_END_OF_SOURCE)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 1, 1)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L)
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
|
||||
.withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad2-0")
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 1,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-2-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
handleContentTimelineChanged_snapInLiveFarBeforeOrAfterWindow_snapToStartAndEndOfWindow()
|
||||
throws IOException {
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates(
|
||||
adsLoader,
|
||||
/* startAdsLoader= */ true,
|
||||
/* windowOffsetInFirstPeriodUs= */ 0,
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\"," // pre roll
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"1990-01-02T00:00:00.000\"," // snap to start of window
|
||||
+ "DURATION=1.0,"
|
||||
+ "X-RESUME-OFFSET=0.0," // with no offset SNAP_IN => SNAP_OUT
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad1-0\"," // ignore
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"1990-01-02T00:00:00.000\"," // snap to start of window
|
||||
+ "DURATION=1.0," // translate start of ad back to 1s behind window
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad2-0\"," // mid roll at end of window (but not C.TIME_END_OF_SOURCE)
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2050-01-02T21:55:00.900\"," // snap end of window: 12_222_000
|
||||
+ "X-SNAP=\"IN\"," // no duration or offset: SNAP_IN => SNAP_OUT
|
||||
+ "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6.111,\nmain1.0.ts\n"
|
||||
+ "#EXTINF:6.111,\nmain1.0.ts\n" // end of window: 12:222 - 21:55:52.222
|
||||
+ "\n"))
|
||||
.containsExactly(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 12_222_000L)
|
||||
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 1, 1)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L)
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
|
||||
.withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad2-0")
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 1,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-2-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
handleContentTimelineChanged_snapInOut_snapToSameSegmentBoundaryMergedIntoSameAdGroup()
|
||||
throws IOException {
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6.111,\nmain1.0.ts\n"
|
||||
+ "#EXTINF:6.111,\nmain2.0.ts\n" // segment start at 6.111 -> 21:55:46.111
|
||||
+ "#EXTINF:6.111,\nmain3.0.ts\n" // segment start at 12.222 -> 21:55:52.222
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\"," // mid roll
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:45.000\"," // snap tp 6_111_000
|
||||
+ "X-SNAP=\"OUT\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-1\"," // mid roll
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:47.000\"," // snap to 6_111_000
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "DURATION=6.111," // ends at 21:55:53.111 -> 21:55:52.222 - 6.111
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\""
|
||||
+ "\n",
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 6_111_000L)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 2)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 6_111_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, 6_111_000L)
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1")
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 1,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-1.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
handleContentTimelineChanged_snapInOutLive_snapToSameSegmentBoundaryMergedInSameAdGroup()
|
||||
throws IOException {
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates(
|
||||
adsLoader,
|
||||
/* startAdsLoader= */ true,
|
||||
/* windowOffsetInFirstPeriodUs= */ 0L,
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:45.000\"," // -> 6_111_000L
|
||||
+ "X-SNAP=\"OUT\","
|
||||
+ "DURATION=2.222,"
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-1\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:47.000\"," // -> 6_111_000L
|
||||
+ "X-SNAP=\"IN\","
|
||||
+ "DURATION=6.111,"
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6.111,\nmain1.0.ts\n"
|
||||
+ "#EXTINF:6.111,\nmain2.0.ts\n" // segment start at 6.111 -> 21:55:46.111
|
||||
+ "#EXTINF:6.111,\nmain3.0.ts\n" // segment start at 12.222 -> 21:55:52.222
|
||||
+ "\n"))
|
||||
.containsExactly(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 6_111_000L)
|
||||
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 2)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 8_333_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 2_222_000L, 6_111_000L)
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1")
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 1,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-1.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleContentTimelineChanged_snapOut_snapToExactTargetDurationBoundaryInWindow()
|
||||
throws IOException {
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6.111,\nmain0.ts\n"
|
||||
+ "#EXTINF:6.111,\nmain1.ts\n"
|
||||
+ "#EXTINF:6.111,\nmain2.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\","
|
||||
+ "X-SNAP=\"OUT\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:46.000Z\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad1-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:58.000Z\","
|
||||
+ "X-SNAP=\"OUT\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\""
|
||||
+ "\n",
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 6_111_000L, C.TIME_END_OF_SOURCE)
|
||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||
.withAdCount(/* adGroupIndex= */ 1, 1)
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
|
||||
.withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0")
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 0,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-0-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build())
|
||||
.withAvailableAdMediaItem(
|
||||
/* adGroupIndex= */ 1,
|
||||
/* adIndexInAdGroup= */ 0,
|
||||
new MediaItem.Builder()
|
||||
.setUri("http://example.com/media-1-0.m3u8")
|
||||
.setMimeType(MimeTypes.APPLICATION_M3U8)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleContentTimelineChanged_noDurationSet_durationTimeUnset() throws IOException {
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-TARGETDURATION:6\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n"
|
||||
+ "#EXTINF:6,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
+ "ID=\"ad0-0\","
|
||||
+ "CLASS=\"com.apple.hls.interstitial\","
|
||||
+ "START-DATE=\"2020-01-02T21:55:41.123Z\","
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n",
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
@ -1467,6 +2157,8 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.ts\n"
|
||||
+ "#EXTINF:81,\n"
|
||||
+ "main0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
@ -1597,6 +2289,8 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXTINF:16.001,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
@ -1663,6 +2357,8 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXTINF:81,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
@ -1752,8 +2448,10 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "X-ASSET-LIST=\"http://example.com/assetlist-1-0.json\""
|
||||
+ "\n"
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.0.ts\n";
|
||||
+ "#EXTINF:9,\nmain0.0.ts\n"
|
||||
+ "#EXTINF:9,\nmain1.0.ts\n"
|
||||
+ "#EXTINF:9,\nmain2.0.ts\n"
|
||||
+ "#EXTINF:3,\nmain3.0.ts\n";
|
||||
when(mockPlayer.getContentPosition()).thenReturn(0L);
|
||||
contentWindowDefinition =
|
||||
contentWindowDefinition
|
||||
@ -1767,7 +2465,7 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowIndex= */ 2,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
|
||||
@ -1891,6 +2589,10 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
@ -1935,6 +2637,8 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXTINF:81,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
@ -2194,6 +2898,10 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main1.0.ts\n"
|
||||
+ "#EXTINF:72,\n"
|
||||
+ "main2.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
@ -2306,6 +3014,10 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXTINF:9,\n"
|
||||
+ "main0.0.ts\n"
|
||||
+ "#EXT-X-ENDLIST"
|
||||
+ "\n"
|
||||
+ "#EXT-X-DATERANGE:"
|
||||
@ -3061,11 +3773,16 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
initialWindows[windowIndex] =
|
||||
contentWindowDefinition
|
||||
.buildUpon()
|
||||
.setPlaceholder(true)
|
||||
.setDynamic(true)
|
||||
.setDurationUs(C.TIME_UNSET)
|
||||
.setDurationUs(durationUs)
|
||||
.setWindowPositionInFirstPeriodUs(windowPositionInPeriodUs)
|
||||
.build();
|
||||
when(mockPlayer.getCurrentTimeline()).thenReturn(new FakeTimeline(initialWindows));
|
||||
when(mockPlayer.getCurrentMediaItem()).thenReturn(contentWindowDefinition.mediaItem);
|
||||
when(mockPlayer.getCurrentMediaItemIndex()).thenReturn(windowIndex);
|
||||
when(mockPlayer.getCurrentPeriodIndex()).thenReturn(windowIndex);
|
||||
// Set the player.
|
||||
adsLoader.setPlayer(mockPlayer);
|
||||
// Start the ad.
|
||||
@ -3096,8 +3813,6 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
.setAdPlaybackStates(ImmutableList.of(adPlaybackState.getValue()))
|
||||
.build();
|
||||
when(mockPlayer.getCurrentTimeline()).thenReturn(new FakeTimeline(windowsAfterTimelineChange));
|
||||
when(mockPlayer.getCurrentMediaItemIndex()).thenReturn(windowIndex);
|
||||
when(mockPlayer.getCurrentPeriodIndex()).thenReturn(windowIndex);
|
||||
return adPlaybackState.getValue();
|
||||
}
|
||||
|
||||
|
@ -894,6 +894,8 @@ import java.util.concurrent.TimeoutException;
|
||||
return metadata.writer;
|
||||
case MediaMetadataCompat.METADATA_KEY_COMPOSER:
|
||||
return metadata.composer;
|
||||
case MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE:
|
||||
return metadata.subtitle;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -61,6 +61,7 @@ public final class MediaBrowser extends MediaController {
|
||||
private Looper applicationLooper;
|
||||
private @MonotonicNonNull BitmapLoader bitmapLoader;
|
||||
private int maxCommandsForMediaItems;
|
||||
private long platformSessionCallbackAggregationTimeoutMs;
|
||||
|
||||
/**
|
||||
* Creates a builder for {@link MediaBrowser}.
|
||||
@ -78,6 +79,8 @@ public final class MediaBrowser extends MediaController {
|
||||
connectionHints = Bundle.EMPTY;
|
||||
listener = new Listener() {};
|
||||
applicationLooper = Util.getCurrentOrMainLooper();
|
||||
platformSessionCallbackAggregationTimeoutMs =
|
||||
DEFAULT_PLATFORM_CALLBACK_AGGREGATION_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,6 +159,24 @@ public final class MediaBrowser extends MediaController {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timeout after which updates from the platform session callbacks are applied to the
|
||||
* browser, in milliseconds.
|
||||
*
|
||||
* <p>The default is 100ms.
|
||||
*
|
||||
* @param platformSessionCallbackAggregationTimeoutMs The timeout, in milliseconds.
|
||||
* @return The builder to allow chaining.
|
||||
*/
|
||||
@UnstableApi
|
||||
@CanIgnoreReturnValue
|
||||
public Builder experimentalSetPlatformSessionCallbackAggregationTimeoutMs(
|
||||
long platformSessionCallbackAggregationTimeoutMs) {
|
||||
this.platformSessionCallbackAggregationTimeoutMs =
|
||||
platformSessionCallbackAggregationTimeoutMs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link MediaBrowser} asynchronously.
|
||||
*
|
||||
@ -196,7 +217,8 @@ public final class MediaBrowser extends MediaController {
|
||||
applicationLooper,
|
||||
holder,
|
||||
bitmapLoader,
|
||||
maxCommandsForMediaItems);
|
||||
maxCommandsForMediaItems,
|
||||
platformSessionCallbackAggregationTimeoutMs);
|
||||
postOrRun(new Handler(applicationLooper), () -> holder.setController(browser));
|
||||
return holder;
|
||||
}
|
||||
@ -266,7 +288,8 @@ public final class MediaBrowser extends MediaController {
|
||||
Looper applicationLooper,
|
||||
ConnectionCallback connectionCallback,
|
||||
@Nullable BitmapLoader bitmapLoader,
|
||||
int maxCommandsForMediaItems) {
|
||||
int maxCommandsForMediaItems,
|
||||
long platformSessionCallbackAggregationTimeoutMs) {
|
||||
super(
|
||||
context,
|
||||
token,
|
||||
@ -275,7 +298,8 @@ public final class MediaBrowser extends MediaController {
|
||||
applicationLooper,
|
||||
connectionCallback,
|
||||
bitmapLoader,
|
||||
maxCommandsForMediaItems);
|
||||
maxCommandsForMediaItems,
|
||||
platformSessionCallbackAggregationTimeoutMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -286,12 +310,19 @@ public final class MediaBrowser extends MediaController {
|
||||
SessionToken token,
|
||||
Bundle connectionHints,
|
||||
Looper applicationLooper,
|
||||
@Nullable BitmapLoader bitmapLoader) {
|
||||
@Nullable BitmapLoader bitmapLoader,
|
||||
long platformSessionCallbackAggregationTimeoutMs) {
|
||||
MediaBrowserImpl impl;
|
||||
if (token.isLegacySession()) {
|
||||
impl =
|
||||
new MediaBrowserImplLegacy(
|
||||
context, this, token, connectionHints, applicationLooper, checkNotNull(bitmapLoader));
|
||||
context,
|
||||
this,
|
||||
token,
|
||||
connectionHints,
|
||||
applicationLooper,
|
||||
checkNotNull(bitmapLoader),
|
||||
platformSessionCallbackAggregationTimeoutMs);
|
||||
} else {
|
||||
impl = new MediaBrowserImplBase(context, this, token, connectionHints, applicationLooper);
|
||||
}
|
||||
|
@ -64,8 +64,16 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
|
||||
SessionToken token,
|
||||
Bundle connectionHints,
|
||||
Looper applicationLooper,
|
||||
BitmapLoader bitmapLoader) {
|
||||
super(context, instance, token, connectionHints, applicationLooper, bitmapLoader);
|
||||
BitmapLoader bitmapLoader,
|
||||
long platformSessionCallbackAggregationTimeoutMs) {
|
||||
super(
|
||||
context,
|
||||
instance,
|
||||
token,
|
||||
connectionHints,
|
||||
applicationLooper,
|
||||
bitmapLoader,
|
||||
platformSessionCallbackAggregationTimeoutMs);
|
||||
this.instance = instance;
|
||||
commandButtonsForMediaItems = ImmutableMap.of();
|
||||
}
|
||||
|
@ -201,6 +201,8 @@ public class MediaController implements Player {
|
||||
"MediaController method is called from a wrong thread."
|
||||
+ " See javadoc of MediaController for details.";
|
||||
|
||||
@UnstableApi protected static final long DEFAULT_PLATFORM_CALLBACK_AGGREGATION_TIMEOUT_MS = 100L;
|
||||
|
||||
/** A builder for {@link MediaController}. */
|
||||
public static final class Builder {
|
||||
|
||||
@ -211,6 +213,7 @@ public class MediaController implements Player {
|
||||
private Looper applicationLooper;
|
||||
private @MonotonicNonNull BitmapLoader bitmapLoader;
|
||||
private int maxCommandsForMediaItems;
|
||||
private long platformSessionCallbackAggregationTimeoutMs;
|
||||
|
||||
/**
|
||||
* Creates a builder for {@link MediaController}.
|
||||
@ -242,6 +245,8 @@ public class MediaController implements Player {
|
||||
connectionHints = Bundle.EMPTY;
|
||||
listener = new Listener() {};
|
||||
applicationLooper = Util.getCurrentOrMainLooper();
|
||||
platformSessionCallbackAggregationTimeoutMs =
|
||||
DEFAULT_PLATFORM_CALLBACK_AGGREGATION_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -320,6 +325,24 @@ public class MediaController implements Player {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timeout after which updates from the platform session callbacks are applied to the
|
||||
* browser, in milliseconds.
|
||||
*
|
||||
* <p>The default is 100ms.
|
||||
*
|
||||
* @param platformSessionCallbackAggregationTimeoutMs The timeout, in milliseconds.
|
||||
* @return tThe builder to allow chaining.
|
||||
*/
|
||||
@UnstableApi
|
||||
@CanIgnoreReturnValue
|
||||
public Builder experimentalSetPlatformSessionCallbackAggregationTimeoutMs(
|
||||
long platformSessionCallbackAggregationTimeoutMs) {
|
||||
this.platformSessionCallbackAggregationTimeoutMs =
|
||||
platformSessionCallbackAggregationTimeoutMs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link MediaController} asynchronously.
|
||||
*
|
||||
@ -361,7 +384,8 @@ public class MediaController implements Player {
|
||||
applicationLooper,
|
||||
holder,
|
||||
bitmapLoader,
|
||||
maxCommandsForMediaItems);
|
||||
maxCommandsForMediaItems,
|
||||
platformSessionCallbackAggregationTimeoutMs);
|
||||
postOrRun(new Handler(applicationLooper), () -> holder.setController(controller));
|
||||
return holder;
|
||||
}
|
||||
@ -553,7 +577,8 @@ public class MediaController implements Player {
|
||||
Looper applicationLooper,
|
||||
ConnectionCallback connectionCallback,
|
||||
@Nullable BitmapLoader bitmapLoader,
|
||||
int maxCommandsForMediaItems) {
|
||||
int maxCommandsForMediaItems,
|
||||
long platformSessionCallbackAggregationTimeoutMs) {
|
||||
checkNotNull(context, "context must not be null");
|
||||
checkNotNull(token, "token must not be null");
|
||||
Log.i(
|
||||
@ -576,7 +601,14 @@ public class MediaController implements Player {
|
||||
this.connectionCallback = connectionCallback;
|
||||
this.maxCommandsForMediaItems = maxCommandsForMediaItems;
|
||||
|
||||
impl = createImpl(context, token, connectionHints, applicationLooper, bitmapLoader);
|
||||
impl =
|
||||
createImpl(
|
||||
context,
|
||||
token,
|
||||
connectionHints,
|
||||
applicationLooper,
|
||||
bitmapLoader,
|
||||
platformSessionCallbackAggregationTimeoutMs);
|
||||
impl.connect();
|
||||
}
|
||||
|
||||
@ -587,10 +619,17 @@ public class MediaController implements Player {
|
||||
SessionToken token,
|
||||
Bundle connectionHints,
|
||||
Looper applicationLooper,
|
||||
@Nullable BitmapLoader bitmapLoader) {
|
||||
@Nullable BitmapLoader bitmapLoader,
|
||||
long platformSessionCallbackAggregationTimeoutMs) {
|
||||
if (token.isLegacySession()) {
|
||||
return new MediaControllerImplLegacy(
|
||||
context, this, token, connectionHints, applicationLooper, checkNotNull(bitmapLoader));
|
||||
context,
|
||||
this,
|
||||
token,
|
||||
connectionHints,
|
||||
applicationLooper,
|
||||
checkNotNull(bitmapLoader),
|
||||
platformSessionCallbackAggregationTimeoutMs);
|
||||
} else {
|
||||
return new MediaControllerImplBase(context, this, token, connectionHints, applicationLooper);
|
||||
}
|
||||
|
@ -93,8 +93,6 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
|
||||
|
||||
private static final String TAG = "MCImplLegacy";
|
||||
|
||||
private static final long AGGREGATES_CALLBACKS_WITHIN_TIMEOUT_MS = 500L;
|
||||
|
||||
/* package */ final Context context;
|
||||
private final MediaController instance;
|
||||
|
||||
@ -104,6 +102,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
|
||||
private final BitmapLoader bitmapLoader;
|
||||
private final ImmutableList<CommandButton> commandButtonsForMediaItems;
|
||||
private final Bundle connectionHints;
|
||||
private final long platformSessionCallbackAggregationTimeoutMs;
|
||||
|
||||
@Nullable private MediaControllerCompat controllerCompat;
|
||||
@Nullable private MediaBrowserCompat browserCompat;
|
||||
@ -122,7 +121,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
|
||||
SessionToken token,
|
||||
Bundle connectionHints,
|
||||
Looper applicationLooper,
|
||||
BitmapLoader bitmapLoader) {
|
||||
BitmapLoader bitmapLoader,
|
||||
long platformSessionCallbackAggregationTimeoutMs) {
|
||||
// Initialize default values.
|
||||
legacyPlayerInfo = new LegacyPlayerInfo();
|
||||
pendingLegacyPlayerInfo = new LegacyPlayerInfo();
|
||||
@ -140,6 +140,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
|
||||
this.token = token;
|
||||
this.connectionHints = connectionHints;
|
||||
this.bitmapLoader = bitmapLoader;
|
||||
this.platformSessionCallbackAggregationTimeoutMs = platformSessionCallbackAggregationTimeoutMs;
|
||||
currentPositionMs = C.TIME_UNSET;
|
||||
lastSetPlayWhenReadyCalledTimeMs = C.TIME_UNSET;
|
||||
// Always empty. Only supported for a MediaBrowser connected to a MediaBrowserServiceCompat.
|
||||
@ -1992,7 +1993,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
|
||||
return;
|
||||
}
|
||||
pendingChangesHandler.sendEmptyMessageDelayed(
|
||||
MSG_HANDLE_PENDING_UPDATES, AGGREGATES_CALLBACKS_WITHIN_TIMEOUT_MS);
|
||||
MSG_HANDLE_PENDING_UPDATES, platformSessionCallbackAggregationTimeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -315,7 +315,8 @@ public final class MediaMetadataCompat implements Parcelable {
|
||||
METADATA_KEY_ALBUM_ARTIST,
|
||||
METADATA_KEY_WRITER,
|
||||
METADATA_KEY_AUTHOR,
|
||||
METADATA_KEY_COMPOSER
|
||||
METADATA_KEY_COMPOSER,
|
||||
METADATA_KEY_DISPLAY_SUBTITLE
|
||||
};
|
||||
|
||||
final Bundle mBundle;
|
||||
|
@ -266,6 +266,38 @@ public final class LegacyConversionsTest {
|
||||
assertThat(convertedMediaItemWithDisplayTitleAndTitle.mediaMetadata.albumTitle).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
convertToMediaDescriptionCompat_withoutDisplayTitleWithSubtitle_subtitleUsedAsSubtitle() {
|
||||
MediaMetadata metadata =
|
||||
new MediaMetadata.Builder().setTitle("a_title").setSubtitle("a_subtitle").build();
|
||||
MediaItem mediaItem =
|
||||
new MediaItem.Builder().setMediaId("testId").setMediaMetadata(metadata).build();
|
||||
|
||||
MediaDescriptionCompat descriptionCompat =
|
||||
LegacyConversions.convertToMediaDescriptionCompat(mediaItem, /* artworkBitmap= */ null);
|
||||
|
||||
assertThat(descriptionCompat.getTitle().toString()).isEqualTo("a_title");
|
||||
assertThat(descriptionCompat.getSubtitle().toString()).isEqualTo("a_subtitle");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertToMediaDescriptionCompat_withDisplayTitleAndSubtitle_subtitleUsedAsSubtitle() {
|
||||
MediaMetadata metadata =
|
||||
new MediaMetadata.Builder()
|
||||
.setDisplayTitle("a_display_title")
|
||||
.setSubtitle("a_subtitle")
|
||||
.build();
|
||||
MediaItem mediaItem =
|
||||
new MediaItem.Builder().setMediaId("testId").setMediaMetadata(metadata).build();
|
||||
|
||||
MediaDescriptionCompat descriptionCompat =
|
||||
LegacyConversions.convertToMediaDescriptionCompat(mediaItem, /* artworkBitmap= */ null);
|
||||
|
||||
assertThat(descriptionCompat.getTitle().toString()).isEqualTo("a_display_title");
|
||||
assertThat(descriptionCompat.getSubtitle().toString()).isEqualTo("a_subtitle");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertToQueueItemId() {
|
||||
assertThat(LegacyConversions.convertToQueueItemId(C.INDEX_UNSET))
|
||||
|
@ -16,12 +16,17 @@
|
||||
|
||||
package androidx.media3.ui.compose
|
||||
|
||||
import android.content.Context
|
||||
import android.view.SurfaceView
|
||||
import android.view.TextureView
|
||||
import android.view.View
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.media3.common.Player
|
||||
@ -47,37 +52,53 @@ fun PlayerSurface(
|
||||
modifier: Modifier = Modifier,
|
||||
surfaceType: @SurfaceType Int = SURFACE_TYPE_SURFACE_VIEW,
|
||||
) {
|
||||
// Player might change between compositions,
|
||||
// we need long-lived surface-related lambdas to always use the latest value
|
||||
val currentPlayer by rememberUpdatedState(player)
|
||||
|
||||
when (surfaceType) {
|
||||
SURFACE_TYPE_SURFACE_VIEW ->
|
||||
AndroidView(
|
||||
factory = {
|
||||
SurfaceView(it).apply {
|
||||
if (currentPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE))
|
||||
currentPlayer.setVideoSurfaceView(this)
|
||||
}
|
||||
},
|
||||
onReset = {},
|
||||
modifier = modifier,
|
||||
PlayerSurfaceInternal(
|
||||
player,
|
||||
modifier,
|
||||
createView = { SurfaceView(it) },
|
||||
setViewOnPlayer = { player, view -> player.setVideoSurfaceView(view) },
|
||||
clearViewFromPlayer = { player, view -> player.clearVideoSurfaceView(view) },
|
||||
)
|
||||
SURFACE_TYPE_TEXTURE_VIEW ->
|
||||
AndroidView(
|
||||
factory = {
|
||||
TextureView(it).apply {
|
||||
if (currentPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE))
|
||||
currentPlayer.setVideoTextureView(this)
|
||||
}
|
||||
},
|
||||
onReset = {},
|
||||
modifier = modifier,
|
||||
PlayerSurfaceInternal(
|
||||
player,
|
||||
modifier,
|
||||
createView = { TextureView(it) },
|
||||
setViewOnPlayer = { player, view -> player.setVideoTextureView(view) },
|
||||
clearViewFromPlayer = { player, view -> player.clearVideoTextureView(view) },
|
||||
)
|
||||
else -> throw IllegalArgumentException("Unrecognized surface type: $surfaceType")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : View> PlayerSurfaceInternal(
|
||||
player: Player,
|
||||
modifier: Modifier,
|
||||
createView: (Context) -> T,
|
||||
setViewOnPlayer: (Player, T) -> Unit,
|
||||
clearViewFromPlayer: (Player, T) -> Unit,
|
||||
) {
|
||||
var view by remember { mutableStateOf<T?>(null) }
|
||||
var registeredPlayer by remember { mutableStateOf<Player?>(null) }
|
||||
AndroidView(factory = { createView(it).apply { view = this } }, onReset = {}, modifier = modifier)
|
||||
view?.let { view ->
|
||||
LaunchedEffect(view, player) {
|
||||
registeredPlayer?.let { previousPlayer ->
|
||||
if (previousPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE))
|
||||
clearViewFromPlayer(previousPlayer, view)
|
||||
registeredPlayer = null
|
||||
}
|
||||
if (player.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) {
|
||||
setViewOnPlayer(player, view)
|
||||
registeredPlayer = player
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of surface used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW] or
|
||||
* [SURFACE_TYPE_TEXTURE_VIEW].
|
||||
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.MutableIntState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.media3.common.ForwardingPlayer
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.ui.compose.utils.TestPlayer
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
@ -28,6 +29,9 @@ import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.Mockito.inOrder
|
||||
import org.mockito.Mockito.spy
|
||||
|
||||
/** Unit test for [PlayerSurface]. */
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@ -87,4 +91,30 @@ class PlayerSurfaceTest {
|
||||
|
||||
assertThat(player.videoOutput).isInstanceOf(SurfaceView::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playerSurface_withNewPlayer_unsetsSurfaceOnOldPlayerFirst() {
|
||||
val player0 = TestPlayer()
|
||||
val player1 = TestPlayer()
|
||||
val spyPlayer0 = spy(ForwardingPlayer(player0))
|
||||
val spyPlayer1 = spy(ForwardingPlayer(player1))
|
||||
|
||||
lateinit var playerIndex: MutableIntState
|
||||
composeTestRule.setContent {
|
||||
playerIndex = remember { mutableIntStateOf(0) }
|
||||
PlayerSurface(
|
||||
player = if (playerIndex.intValue == 0) spyPlayer0 else spyPlayer1,
|
||||
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
|
||||
)
|
||||
}
|
||||
composeTestRule.waitForIdle()
|
||||
playerIndex.intValue = 1
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
assertThat(player0.videoOutput).isNull()
|
||||
assertThat(player1.videoOutput).isNotNull()
|
||||
val inOrder = inOrder(spyPlayer0, spyPlayer1)
|
||||
inOrder.verify(spyPlayer0).clearVideoSurfaceView(any())
|
||||
inOrder.verify(spyPlayer1).setVideoSurfaceView(any())
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user