Compare commits

..

No commits in common. "d0833c4e7cd27a5c9192f61a6c7d8dfb68f7b0b4" and "52db3a240de6353416055a307aa88e91bfd9556b" have entirely different histories.

13 changed files with 98 additions and 840 deletions

View File

@ -69,7 +69,6 @@
* Cronet extension: * Cronet extension:
* RTMP extension: * RTMP extension:
* HLS extension: * HLS extension:
* Support X-ASSET-LIST and live streams with `HlsInterstitialsAdsLoader`.
* DASH extension: * DASH extension:
* Smooth Streaming extension: * Smooth Streaming extension:
* RTSP extension: * RTSP extension:

View File

@ -596,21 +596,6 @@ public final class AdPlaybackState {
return C.INDEX_UNSET; return C.INDEX_UNSET;
} }
/** Returns a safe copy with all array fields copied into the new instance as new arrays. */
public AdGroup copy() {
return new AdGroup(
timeUs,
count,
originalCount,
Arrays.copyOf(states, states.length),
Arrays.copyOf(mediaItems, mediaItems.length),
Arrays.copyOf(durationsUs, durationsUs.length),
contentResumeOffsetUs,
isServerSideInserted,
Arrays.copyOf(ids, ids.length),
isPlaceholder);
}
@CheckResult @CheckResult
private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) { private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) {
int oldStateCount = states.length; int oldStateCount = states.length;
@ -959,20 +944,6 @@ public final class AdPlaybackState {
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
} }
/**
* Returns an new instance that is a safe deep copy of this instance in case an immutable object
* is used for {@link #adsId}.
*/
@CheckResult
public AdPlaybackState copy() {
AdGroup[] adGroups = new AdGroup[this.adGroups.length];
for (int i = 0; i < adGroups.length; i++) {
adGroups[i] = this.adGroups[i].copy();
}
return new AdPlaybackState(
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/** /**
* @deprecated Use {@link #withAvailableAdMediaItem} instead. * @deprecated Use {@link #withAvailableAdMediaItem} instead.
*/ */

View File

@ -26,7 +26,6 @@ import static org.junit.Assert.fail;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.lang.reflect.Field;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -1202,65 +1201,4 @@ public class AdPlaybackStateTest {
assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 3).durationsUs).hasLength(0); assertThat(adPlaybackState.getAdGroup(/* adGroupIndex= */ 3).durationsUs).hasLength(0);
} }
@SuppressWarnings("deprecation") // testing deprecated field `uris`
@Test
public void copy() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId", 10_000L)
.withLivePostrollPlaceholderAppended(false)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/0-0"))
.withNewAdGroup(/* adGroupIndex= */ 1, 11_000)
.withAdCount(/* adGroupIndex= */ 1, 2)
.withAvailableAdMediaItem(
/* adGroupIndex= */ 1,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/1-0"))
.withAvailableAdMediaItem(
/* adGroupIndex= */ 1,
/* adIndexInAdGroup= */ 1,
MediaItem.fromUri("http://example.com/1-1"))
.withNewAdGroup(/* adGroupIndex= */ 2, 12_000);
AdPlaybackState copy = adPlaybackState.copy();
assertThat(copy).isEqualTo(adPlaybackState);
assertThat(copy).isNotSameInstanceAs(adPlaybackState);
for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) {
AdPlaybackState.AdGroup adGroupCopy = copy.getAdGroup(adGroupIndex);
AdPlaybackState.AdGroup originalAdGroup = adPlaybackState.getAdGroup(adGroupIndex);
assertThat(adGroupCopy).isNotSameInstanceAs(originalAdGroup);
assertThat(adGroupCopy.durationsUs).isNotSameInstanceAs(originalAdGroup.durationsUs);
assertThat(adGroupCopy.ids).isNotSameInstanceAs(originalAdGroup.ids);
assertThat(adGroupCopy.mediaItems).isNotSameInstanceAs(originalAdGroup.mediaItems);
assertThat(adGroupCopy.states).isNotSameInstanceAs(originalAdGroup.states);
assertThat(adGroupCopy.uris).isNotSameInstanceAs(originalAdGroup.uris);
}
}
/**
* If this test fails a new field of type array has been added to {@link AdPlaybackState.AdGroup}.
* Make sure to update {@link AdPlaybackState.AdGroup#copy} and add a line in the test {@link
* #copy()} to verify that the new array field has been copied as a new array instance. Then
* increment the expected count in this test case.
*/
@Test
public void adGroup_numberOfFieldsOfTypeArray_hasNotChanged() {
// 5 fields of type array durationsUs, ids, mediaItems, states, uris.
int expectedNumberOfFieldsOfTypeArray = 5;
Class<?> clazz = AdPlaybackState.AdGroup.class;
Field[] fields = clazz.getFields();
int arrayFieldCount = 0;
for (Field field : fields) {
if (field.getType().isArray()) {
arrayFieldCount++;
}
}
assertThat(arrayFieldCount).isEqualTo(expectedNumberOfFieldsOfTypeArray);
}
} }

View File

@ -63,7 +63,6 @@ import androidx.media3.extractor.AacUtil;
import androidx.media3.extractor.Ac3Util; import androidx.media3.extractor.Ac3Util;
import androidx.media3.extractor.Ac4Util; import androidx.media3.extractor.Ac4Util;
import androidx.media3.extractor.DtsUtil; import androidx.media3.extractor.DtsUtil;
import androidx.media3.extractor.ExtractorUtil;
import androidx.media3.extractor.MpegAudioUtil; import androidx.media3.extractor.MpegAudioUtil;
import androidx.media3.extractor.OpusUtil; import androidx.media3.extractor.OpusUtil;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@ -1480,7 +1479,8 @@ public final class DefaultAudioSink implements AudioSink {
long byteRate = long byteRate =
configuration.outputMode == OUTPUT_MODE_PCM configuration.outputMode == OUTPUT_MODE_PCM
? (long) configuration.outputSampleRate * configuration.outputPcmFrameSize ? (long) configuration.outputSampleRate * configuration.outputPcmFrameSize
: getNonPcmMaximumEncodedRateBytesPerSecond(configuration.outputEncoding); : DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond(
configuration.outputEncoding);
return Util.scaleLargeValue( return Util.scaleLargeValue(
configuration.bufferSize, C.MICROS_PER_SECOND, byteRate, RoundingMode.DOWN); configuration.bufferSize, C.MICROS_PER_SECOND, byteRate, RoundingMode.DOWN);
} }
@ -2387,12 +2387,6 @@ public final class DefaultAudioSink implements AudioSink {
} }
} }
private static int getNonPcmMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) {
int rate = ExtractorUtil.getMaximumEncodedRateBytesPerSecond(encoding);
checkState(rate != C.RATE_UNSET_INT);
return rate;
}
@RequiresApi(23) @RequiresApi(23)
private static final class Api23 { private static final class Api23 {
private Api23() {} private Api23() {}
@ -2410,7 +2404,8 @@ public final class DefaultAudioSink implements AudioSink {
: Util.scaleLargeValue( : Util.scaleLargeValue(
audioTrack.getBufferSizeInFrames(), audioTrack.getBufferSizeInFrames(),
C.MICROS_PER_SECOND, C.MICROS_PER_SECOND,
getNonPcmMaximumEncodedRateBytesPerSecond(configuration.outputEncoding), DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond(
configuration.outputEncoding),
RoundingMode.DOWN); RoundingMode.DOWN);
} }
} }

View File

@ -15,7 +15,6 @@
*/ */
package androidx.media3.exoplayer.audio; package androidx.media3.exoplayer.audio;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.constrainValue; import static androidx.media3.common.util.Util.constrainValue;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_OFFLOAD; import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_OFFLOAD;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH; import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH;
@ -29,7 +28,12 @@ import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.audio.DefaultAudioSink.OutputMode; import androidx.media3.exoplayer.audio.DefaultAudioSink.OutputMode;
import androidx.media3.extractor.ExtractorUtil; import androidx.media3.extractor.AacUtil;
import androidx.media3.extractor.Ac3Util;
import androidx.media3.extractor.Ac4Util;
import androidx.media3.extractor.DtsUtil;
import androidx.media3.extractor.MpegAudioUtil;
import androidx.media3.extractor.OpusUtil;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.math.RoundingMode; import java.math.RoundingMode;
@ -262,13 +266,13 @@ public class DefaultAudioTrackBufferSizeProvider
int byteRate = int byteRate =
bitrate != Format.NO_VALUE bitrate != Format.NO_VALUE
? divide(bitrate, 8, RoundingMode.CEILING) ? divide(bitrate, 8, RoundingMode.CEILING)
: getNonPcmMaximumEncodedRateBytesPerSecond(encoding); : getMaximumEncodedRateBytesPerSecond(encoding);
return checkedCast((long) bufferSizeUs * byteRate / C.MICROS_PER_SECOND); return checkedCast((long) bufferSizeUs * byteRate / C.MICROS_PER_SECOND);
} }
/** Returns the buffer size for offload playback. */ /** Returns the buffer size for offload playback. */
protected int getOffloadBufferSizeInBytes(@C.Encoding int encoding) { protected int getOffloadBufferSizeInBytes(@C.Encoding int encoding) {
int maxByteRate = getNonPcmMaximumEncodedRateBytesPerSecond(encoding); int maxByteRate = getMaximumEncodedRateBytesPerSecond(encoding);
return checkedCast((long) offloadBufferDurationUs * maxByteRate / C.MICROS_PER_SECOND); return checkedCast((long) offloadBufferDurationUs * maxByteRate / C.MICROS_PER_SECOND);
} }
@ -276,9 +280,49 @@ public class DefaultAudioTrackBufferSizeProvider
return checkedCast((long) durationUs * samplingRate * frameSize / C.MICROS_PER_SECOND); return checkedCast((long) durationUs * samplingRate * frameSize / C.MICROS_PER_SECOND);
} }
private static int getNonPcmMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) { protected static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) {
int rate = ExtractorUtil.getMaximumEncodedRateBytesPerSecond(encoding); switch (encoding) {
checkState(rate != C.RATE_UNSET_INT); case C.ENCODING_MP3:
return rate; return MpegAudioUtil.MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_LC:
return AacUtil.AAC_LC_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_HE_V1:
return AacUtil.AAC_HE_V1_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_HE_V2:
return AacUtil.AAC_HE_V2_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_XHE:
return AacUtil.AAC_XHE_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_ELD:
return AacUtil.AAC_ELD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AC3:
return Ac3Util.AC3_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_E_AC3:
case C.ENCODING_E_AC3_JOC:
return Ac3Util.E_AC3_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AC4:
return Ac4Util.MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DTS:
return DtsUtil.DTS_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DTS_HD:
case C.ENCODING_DTS_UHD_P2:
return DtsUtil.DTS_HD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DOLBY_TRUEHD:
return Ac3Util.TRUEHD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_OPUS:
return OpusUtil.MAX_BYTES_PER_SECOND;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
case C.ENCODING_PCM_24BIT:
case C.ENCODING_PCM_24BIT_BIG_ENDIAN:
case C.ENCODING_PCM_32BIT:
case C.ENCODING_PCM_32BIT_BIG_ENDIAN:
case C.ENCODING_PCM_8BIT:
case C.ENCODING_PCM_FLOAT:
case C.ENCODING_AAC_ER_BSAC:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
throw new IllegalArgumentException();
}
} }
} }

View File

@ -830,6 +830,7 @@ public final class DownloadHelper {
* to, or {@link C#TIME_UNSET} if the download should cover to the end of the media. If the * to, or {@link C#TIME_UNSET} if the download should cover to the end of the media. If the
* {@code endPositionMs} is larger than the duration of the media, then the download will * {@code endPositionMs} is larger than the duration of the media, then the download will
* cover to the end of the media. * cover to the end of the media.
* @throws IllegalStateException If the media item is of type DASH, HLS or SmoothStreaming.
*/ */
public DownloadRequest getDownloadRequest( public DownloadRequest getDownloadRequest(
@Nullable byte[] data, long startPositionMs, long durationMs) { @Nullable byte[] data, long startPositionMs, long durationMs) {
@ -862,11 +863,15 @@ public final class DownloadHelper {
* {@link C#TIME_UNSET} if the download should cover to the end of the media. If the end * {@link C#TIME_UNSET} if the download should cover to the end of the media. If the end
* position resolved from {@code startPositionMs} and {@code durationMs} is beyond the * position resolved from {@code startPositionMs} and {@code durationMs} is beyond the
* duration of the media, then the download will just cover to the end of the media. * duration of the media, then the download will just cover to the end of the media.
* @throws IllegalStateException If the media item is of type DASH, HLS or SmoothStreaming.
*/ */
public DownloadRequest getDownloadRequest( public DownloadRequest getDownloadRequest(
String id, @Nullable byte[] data, long startPositionMs, long durationMs) { String id, @Nullable byte[] data, long startPositionMs, long durationMs) {
checkState(
mode == MODE_PREPARE_PROGRESSIVE_SOURCE,
"Partial download is only supported for progressive streams");
DownloadRequest.Builder builder = getDownloadRequestBuilder(id, data); DownloadRequest.Builder builder = getDownloadRequestBuilder(id, data);
assertPreparedWithMedia(); assertPreparedWithProgressiveSource();
populateDownloadRequestBuilderWithDownloadRange(builder, startPositionMs, durationMs); populateDownloadRequestBuilderWithDownloadRange(builder, startPositionMs, durationMs);
return builder.build(); return builder.build();
} }
@ -901,22 +906,13 @@ public final class DownloadHelper {
private void populateDownloadRequestBuilderWithDownloadRange( private void populateDownloadRequestBuilderWithDownloadRange(
DownloadRequest.Builder requestBuilder, long startPositionMs, long durationMs) { DownloadRequest.Builder requestBuilder, long startPositionMs, long durationMs) {
switch (mode) {
case MODE_PREPARE_PROGRESSIVE_SOURCE:
populateDownloadRequestBuilderWithByteRange(requestBuilder, startPositionMs, durationMs);
break;
case MODE_PREPARE_NON_PROGRESSIVE_SOURCE_AND_SELECT_TRACKS:
populateDownloadRequestBuilderWithTimeRange(requestBuilder, startPositionMs, durationMs);
break;
default:
break;
}
}
private void populateDownloadRequestBuilderWithByteRange(
DownloadRequest.Builder requestBuilder, long startPositionMs, long durationMs) {
assertPreparedWithProgressiveSource(); assertPreparedWithProgressiveSource();
Timeline timeline = mediaPreparer.timeline; Timeline timeline = mediaPreparer.timeline;
if (mediaPreparer.mediaPeriods.length > 1) {
Log.w(TAG, "Partial download is only supported for single period.");
return;
}
Timeline.Window window = new Timeline.Window(); Timeline.Window window = new Timeline.Window();
Timeline.Period period = new Timeline.Period(); Timeline.Period period = new Timeline.Period();
long periodStartPositionUs = long periodStartPositionUs =
@ -961,25 +957,6 @@ public final class DownloadHelper {
} }
} }
private void populateDownloadRequestBuilderWithTimeRange(
DownloadRequest.Builder requestBuilder, long startPositionMs, long durationMs) {
assertPreparedWithNonProgressiveSourceAndTracksSelected();
Timeline timeline = mediaPreparer.timeline;
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
long startPositionUs =
startPositionMs == C.TIME_UNSET
? window.getDefaultPositionUs()
: Util.msToUs(startPositionMs);
long windowDurationUs = window.getDurationUs();
long durationUs = durationMs == C.TIME_UNSET ? windowDurationUs : Util.msToUs(durationMs);
if (windowDurationUs != C.TIME_UNSET) {
startPositionUs = min(startPositionUs, windowDurationUs);
durationUs = min(durationUs, windowDurationUs - startPositionUs);
}
requestBuilder.setTimeRange(startPositionUs, durationUs);
}
@RequiresNonNull({ @RequiresNonNull({
"trackGroupArrays", "trackGroupArrays",
"trackSelectionsByPeriodAndRenderer", "trackSelectionsByPeriodAndRenderer",

View File

@ -17,7 +17,7 @@ package androidx.media3.exoplayer.audio;
import static androidx.media3.common.C.MICROS_PER_SECOND; import static androidx.media3.common.C.MICROS_PER_SECOND;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH; import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH;
import static androidx.media3.extractor.ExtractorUtil.getMaximumEncodedRateBytesPerSecond; import static androidx.media3.exoplayer.audio.DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.C; import androidx.media3.common.C;

View File

@ -17,7 +17,7 @@ package androidx.media3.exoplayer.audio;
import static androidx.media3.common.C.MICROS_PER_SECOND; import static androidx.media3.common.C.MICROS_PER_SECOND;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH; import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH;
import static androidx.media3.extractor.ExtractorUtil.getMaximumEncodedRateBytesPerSecond; import static androidx.media3.exoplayer.audio.DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.C; import androidx.media3.common.C;

View File

@ -17,7 +17,7 @@ package androidx.media3.exoplayer.audio;
import static androidx.media3.common.C.MICROS_PER_SECOND; import static androidx.media3.common.C.MICROS_PER_SECOND;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH; import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH;
import static androidx.media3.extractor.ExtractorUtil.getMaximumEncodedRateBytesPerSecond; import static androidx.media3.exoplayer.audio.DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.C; import androidx.media3.common.C;

View File

@ -18,6 +18,7 @@ package androidx.media3.exoplayer.offline;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertThrows;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
import android.content.Context; import android.content.Context;
@ -71,15 +72,10 @@ import org.junit.runner.RunWith;
public class DownloadHelperTest { public class DownloadHelperTest {
private static final Object TEST_MANIFEST = new Object(); private static final Object TEST_MANIFEST = new Object();
private static final long TEST_WINDOW_DEFAULT_POSITION_US = C.MICROS_PER_SECOND;
private static final Timeline TEST_TIMELINE = private static final Timeline TEST_TIMELINE =
new FakeTimeline( new FakeTimeline(
new Object[] {TEST_MANIFEST}, new Object[] {TEST_MANIFEST},
new TimelineWindowDefinition.Builder() new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ new Object()));
.setPeriodCount(2)
.setDefaultPositionUs(TEST_WINDOW_DEFAULT_POSITION_US)
.build());
private static TrackGroup trackGroupVideoLow; private static TrackGroup trackGroupVideoLow;
private static TrackGroup trackGroupVideoLowAndHigh; private static TrackGroup trackGroupVideoLowAndHigh;
@ -90,7 +86,6 @@ public class DownloadHelperTest {
private static TrackGroupArray[] trackGroupArrays; private static TrackGroupArray[] trackGroupArrays;
private static MediaItem testMediaItem; private static MediaItem testMediaItem;
private RenderersFactory renderersFactory;
private DownloadHelper downloadHelper; private DownloadHelper downloadHelper;
@BeforeClass @BeforeClass
@ -129,7 +124,7 @@ public class DownloadHelperTest {
FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO); FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
FakeRenderer textRenderer = new FakeRenderer(C.TRACK_TYPE_TEXT); FakeRenderer textRenderer = new FakeRenderer(C.TRACK_TYPE_TEXT);
renderersFactory = RenderersFactory renderersFactory =
(handler, videoListener, audioListener, metadata, text) -> (handler, videoListener, audioListener, metadata, text) ->
new Renderer[] {textRenderer, audioRenderer, videoRenderer}; new Renderer[] {textRenderer, audioRenderer, videoRenderer};
@ -334,8 +329,8 @@ public class DownloadHelperTest {
prepareDownloadHelper(downloadHelper); prepareDownloadHelper(downloadHelper);
// Select parameters to require some merging of track groups because the new parameters add // Select parameters to require some merging of track groups because the new parameters add
// all video tracks to initial video single track selection. // all video tracks to initial video single track selection.
DefaultTrackSelector.Parameters parameters = TrackSelectionParameters parameters =
new DefaultTrackSelector.Parameters.Builder() new TrackSelectionParameters.Builder(getApplicationContext())
.setPreferredAudioLanguage("de") .setPreferredAudioLanguage("de")
.setPreferredTextLanguage("en") .setPreferredTextLanguage("en")
.build(); .build();
@ -438,8 +433,8 @@ public class DownloadHelperTest {
prepareDownloadHelper(downloadHelper); prepareDownloadHelper(downloadHelper);
// Ensure we have track groups with multiple indices, renderers with multiple track groups and // Ensure we have track groups with multiple indices, renderers with multiple track groups and
// also renderers without any track groups. // also renderers without any track groups.
DefaultTrackSelector.Parameters parameters = TrackSelectionParameters parameters =
new DefaultTrackSelector.Parameters.Builder() new TrackSelectionParameters.Builder(getApplicationContext())
.setPreferredAudioLanguage("de") .setPreferredAudioLanguage("de")
.setPreferredTextLanguage("en") .setPreferredTextLanguage("en")
.build(); .build();
@ -469,8 +464,8 @@ public class DownloadHelperTest {
throws Exception { throws Exception {
prepareDownloadHelper(downloadHelper); prepareDownloadHelper(downloadHelper);
DefaultTrackSelector.Parameters parameters = TrackSelectionParameters parameters =
new DefaultTrackSelector.Parameters.Builder() new TrackSelectionParameters.Builder(getApplicationContext())
.addOverride(new TrackSelectionOverride(trackGroupAudioUs, /* trackIndex= */ 0)) .addOverride(new TrackSelectionOverride(trackGroupAudioUs, /* trackIndex= */ 0))
.addOverride(new TrackSelectionOverride(trackGroupAudioZh, /* trackIndex= */ 0)) .addOverride(new TrackSelectionOverride(trackGroupAudioZh, /* trackIndex= */ 0))
.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, /* disabled= */ true) .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, /* disabled= */ true)
@ -490,7 +485,7 @@ public class DownloadHelperTest {
@Test @Test
public void public void
getDownloadRequestForProgressive_withConcreteTimeRange_requestContainsConcreteByteRange() getDownloadRequest_createsDownloadRequestWithConcreteTimeRange_requestContainsConcreteByteRange()
throws Exception { throws Exception {
DownloadHelper downloadHelper = DownloadHelper downloadHelper =
new DownloadHelper.Factory() new DownloadHelper.Factory()
@ -509,7 +504,7 @@ public class DownloadHelperTest {
@Test @Test
public void public void
getDownloadRequestForProgressive_withUnsetStartPosition_requestContainsConcreteByteRange() getDownloadRequest_createsDownloadRequestWithUnsetStartPosition_requestContainsConcreteByteRange()
throws Exception { throws Exception {
DownloadHelper downloadHelper = DownloadHelper downloadHelper =
new DownloadHelper.Factory() new DownloadHelper.Factory()
@ -527,9 +522,8 @@ public class DownloadHelperTest {
} }
@Test @Test
public void public void getDownloadRequest_createsDownloadRequestWithUnsetLength_requestContainsUnsetLength()
getDownloadRequestForProgressive_withUnsetDuration_requestContainsUnsetByteRangeLength() throws Exception {
throws Exception {
DownloadHelper downloadHelper = DownloadHelper downloadHelper =
new DownloadHelper.Factory() new DownloadHelper.Factory()
.setDataSourceFactory(new DefaultDataSource.Factory(getApplicationContext())) .setDataSourceFactory(new DefaultDataSource.Factory(getApplicationContext()))
@ -547,7 +541,7 @@ public class DownloadHelperTest {
@Test @Test
public void public void
getDownloadRequestForShortProgressive_withConcreteTimeRange_requestContainsUnsetByteRangeLength() getDownloadRequest_createsDownloadRequestForTooShortStreamWithTimeRange_requestContainsUnsetLength()
throws Exception { throws Exception {
DownloadHelper downloadHelper = DownloadHelper downloadHelper =
new DownloadHelper.Factory() new DownloadHelper.Factory()
@ -565,8 +559,9 @@ public class DownloadHelperTest {
} }
@Test @Test
public void getDownloadRequestForProgressive_withoutRange_requestContainsNullByteRange() public void
throws Exception { getDownloadRequest_createsDownloadRequestWithoutTimeRange_requestContainsNullByteRange()
throws Exception {
DownloadHelper downloadHelper = DownloadHelper downloadHelper =
new DownloadHelper.Factory() new DownloadHelper.Factory()
.setDataSourceFactory(new DefaultDataSource.Factory(getApplicationContext())) .setDataSourceFactory(new DefaultDataSource.Factory(getApplicationContext()))
@ -580,155 +575,17 @@ public class DownloadHelperTest {
@Test @Test
public void public void
getDownloadRequestForNonProgressive_withConcreteTimeRange_requestContainsCorrectTimeRange() getDownloadRequest_createDownloadRequestWithTimeRangeForNonProgressiveStream_throwsIllegalStateException()
throws Exception { throws Exception {
DownloadHelper downloadHelper = // We use this.downloadHelper as it was created with a TestMediaSource, thus the DownloadHelper
new DownloadHelper( // will treat it as non-progressive.
new MediaItem.Builder()
.setUri("http://test.uri")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build(),
new TestMediaSource(),
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
new DefaultRendererCapabilitiesList.Factory(renderersFactory)
.createRendererCapabilitiesList());
prepareDownloadHelper(downloadHelper); prepareDownloadHelper(downloadHelper);
DownloadRequest downloadRequest = assertThrows(
downloadHelper.getDownloadRequest( IllegalStateException.class,
/* data= */ null, /* startPositionMs= */ 0, /* durationMs= */ 10000); () ->
downloadHelper.getDownloadRequest(
assertThat(downloadRequest.timeRange).isNotNull(); /* data= */ null, /* startPositionMs= */ 0, /* durationMs= */ 10000));
assertThat(downloadRequest.timeRange.startPositionUs).isEqualTo(0);
assertThat(downloadRequest.timeRange.durationUs).isEqualTo(10000000);
}
@Test
public void
getDownloadRequestForNonProgressive_withUnsetStartPosition_requestContainsCorrectTimeRange()
throws Exception {
DownloadHelper downloadHelper =
new DownloadHelper(
new MediaItem.Builder()
.setUri("http://test.uri")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build(),
new TestMediaSource(),
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
new DefaultRendererCapabilitiesList.Factory(renderersFactory)
.createRendererCapabilitiesList());
prepareDownloadHelper(downloadHelper);
DownloadRequest downloadRequest =
downloadHelper.getDownloadRequest(
/* data= */ null, /* startPositionMs= */ C.TIME_UNSET, /* durationMs= */ 5000);
assertThat(downloadRequest.timeRange).isNotNull();
// The startPositionUs is set to window.defaultPositionUs.
Timeline.Window window = TEST_TIMELINE.getWindow(0, new Timeline.Window());
assertThat(downloadRequest.timeRange.startPositionUs).isEqualTo(window.defaultPositionUs);
assertThat(downloadRequest.timeRange.durationUs).isEqualTo(5000000);
}
@Test
public void
getDownloadRequestForNonProgressive_withStartPositionExceedingWindowDuration_requestContainsCorrectTimeRange()
throws Exception {
DownloadHelper downloadHelper =
new DownloadHelper(
new MediaItem.Builder()
.setUri("http://test.uri")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build(),
new TestMediaSource(),
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
new DefaultRendererCapabilitiesList.Factory(renderersFactory)
.createRendererCapabilitiesList());
prepareDownloadHelper(downloadHelper);
Timeline.Window window = TEST_TIMELINE.getWindow(0, new Timeline.Window());
DownloadRequest downloadRequest =
downloadHelper.getDownloadRequest(
/* data= */ null,
/* startPositionMs= */ window.durationUs + 100,
/* durationMs= */ C.TIME_UNSET);
assertThat(downloadRequest.timeRange).isNotNull();
// The startPositionUs is set to window.durationUs.
assertThat(downloadRequest.timeRange.startPositionUs).isEqualTo(window.durationUs);
assertThat(downloadRequest.timeRange.durationUs).isEqualTo(0);
}
@Test
public void
getDownloadRequestForNonProgressive_withUnsetDuration_requestContainsCorrectTimeRange()
throws Exception {
DownloadHelper downloadHelper =
new DownloadHelper(
new MediaItem.Builder()
.setUri("http://test.uri")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build(),
new TestMediaSource(),
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
new DefaultRendererCapabilitiesList.Factory(renderersFactory)
.createRendererCapabilitiesList());
prepareDownloadHelper(downloadHelper);
DownloadRequest downloadRequest =
downloadHelper.getDownloadRequest(
/* data= */ null, /* startPositionMs= */ 10, /* durationMs= */ C.TIME_UNSET);
assertThat(downloadRequest.timeRange).isNotNull();
assertThat(downloadRequest.timeRange.startPositionUs).isEqualTo(10_000);
Timeline.Window window = TEST_TIMELINE.getWindow(0, new Timeline.Window());
assertThat(downloadRequest.timeRange.durationUs).isEqualTo(window.durationUs - 10_000);
}
@Test
public void
getDownloadRequestForNonProgressive_withDurationExceedingWindowDuration_requestContainsCorrectTimeRange()
throws Exception {
DownloadHelper downloadHelper =
new DownloadHelper(
new MediaItem.Builder()
.setUri("http://test.uri")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build(),
new TestMediaSource(),
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
new DefaultRendererCapabilitiesList.Factory(renderersFactory)
.createRendererCapabilitiesList());
prepareDownloadHelper(downloadHelper);
Timeline.Window window = TEST_TIMELINE.getWindow(0, new Timeline.Window());
DownloadRequest downloadRequest =
downloadHelper.getDownloadRequest(
/* data= */ null, /* startPositionMs= */ 0, /* durationMs= */ window.durationUs + 100);
assertThat(downloadRequest.timeRange).isNotNull();
assertThat(downloadRequest.timeRange.startPositionUs).isEqualTo(0);
assertThat(downloadRequest.timeRange.durationUs).isEqualTo(window.durationUs);
}
@Test
public void getDownloadRequestForNonProgressive_withoutRange_requestContainsNullTimeRange()
throws Exception {
DownloadHelper downloadHelper =
new DownloadHelper(
new MediaItem.Builder()
.setUri("http://test.uri")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build(),
new TestMediaSource(),
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
new DefaultRendererCapabilitiesList.Factory(renderersFactory)
.createRendererCapabilitiesList());
prepareDownloadHelper(downloadHelper);
DownloadRequest downloadRequest = downloadHelper.getDownloadRequest(/* data= */ null);
assertThat(downloadRequest.timeRange).isNull();
} }
// https://github.com/androidx/media/issues/1224 // https://github.com/androidx/media/issues/1224

View File

@ -40,7 +40,6 @@ import static java.lang.Math.min;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.AdPlaybackState; import androidx.media3.common.AdPlaybackState;
@ -95,7 +94,6 @@ import java.util.TreeMap;
* ads media sources}. These ad media source can be added to the same playlist as far as each of the * ads media sources}. These ad media source can be added to the same playlist as far as each of the
* sources have a different ads IDs. * sources have a different ads IDs.
*/ */
@SuppressWarnings("PatternMatchingInstanceof")
@UnstableApi @UnstableApi
public final class HlsInterstitialsAdsLoader implements AdsLoader { public final class HlsInterstitialsAdsLoader implements AdsLoader {
@ -207,67 +205,6 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
} }
} }
/**
* The state of the given ads ID to resume playback at the given {@link AdPlaybackState}.
*
* <p>This state object can be bundled and unbundled while preserving an {@link
* AdPlaybackState#adsId ads ID} of type {@link String}.
*/
public static class AdsResumptionState {
private final AdPlaybackState adPlaybackState;
/** The ads ID */
public final String adsId;
/**
* Creates a new instance.
*
* @param adsId The ads ID of the playback state.
* @param adPlaybackState The {@link AdPlaybackState} with the given {@code adsId}.
* @throws IllegalArgumentException Thrown if the passed in adsId is not equal to {@link
* AdPlaybackState#adsId}.
*/
public AdsResumptionState(String adsId, AdPlaybackState adPlaybackState) {
checkArgument(adsId.equals(adPlaybackState.adsId));
this.adsId = adsId;
this.adPlaybackState = adPlaybackState;
}
@Override
public boolean equals(@Nullable Object o) {
if (!(o instanceof AdsResumptionState)) {
return false;
}
AdsResumptionState adsResumptionState = (AdsResumptionState) o;
return Objects.equals(adsId, adsResumptionState.adsId)
&& Objects.equals(adPlaybackState, adsResumptionState.adPlaybackState);
}
@Override
public int hashCode() {
return Objects.hash(adsId, adPlaybackState);
}
private static final String FIELD_ADS_ID = Util.intToStringMaxRadix(0);
private static final String FIELD_AD_PLAYBACK_STATE = Util.intToStringMaxRadix(1);
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putString(FIELD_ADS_ID, adsId);
bundle.putBundle(FIELD_AD_PLAYBACK_STATE, adPlaybackState.toBundle());
return bundle;
}
public static AdsResumptionState fromBundle(Bundle bundle) {
String adsId = checkNotNull(bundle.getString(FIELD_ADS_ID));
AdPlaybackState adPlaybackState =
AdPlaybackState.fromBundle(checkNotNull(bundle.getBundle(FIELD_AD_PLAYBACK_STATE)))
.withAdsId(adsId);
return new AdsResumptionState(adsId, adPlaybackState);
}
}
/** /**
* A {@link MediaSource.Factory} to create a media source to play HLS streams with interstitials. * A {@link MediaSource.Factory} to create a media source to play HLS streams with interstitials.
*/ */
@ -537,7 +474,6 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
private final Map<Object, AdPlaybackState> activeAdPlaybackStates; private final Map<Object, AdPlaybackState> activeAdPlaybackStates;
private final Map<Object, Set<String>> insertedInterstitialIds; private final Map<Object, Set<String>> insertedInterstitialIds;
private final Map<Object, TreeMap<Long, AssetListData>> unresolvedAssetLists; private final Map<Object, TreeMap<Long, AssetListData>> unresolvedAssetLists;
private final Map<Object, AdPlaybackState> resumptionStates;
private final List<Listener> listeners; private final List<Listener> listeners;
private final Set<Object> unsupportedAdsIds; private final Set<Object> unsupportedAdsIds;
@ -568,7 +504,6 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
activeAdPlaybackStates = new HashMap<>(); activeAdPlaybackStates = new HashMap<>();
insertedInterstitialIds = new HashMap<>(); insertedInterstitialIds = new HashMap<>();
unresolvedAssetLists = new HashMap<>(); unresolvedAssetLists = new HashMap<>();
resumptionStates = new HashMap<>();
listeners = new ArrayList<>(); listeners = new ArrayList<>();
unsupportedAdsIds = new HashSet<>(); unsupportedAdsIds = new HashSet<>();
} }
@ -618,99 +553,6 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
/**
* Returns the resumption states of the currently active {@link AdsMediaSource ads media sources}.
*
* <p>Call this method to get the resumption states before releasing the player and {@linkplain
* #addAdResumptionState(AdsResumptionState) resume at the same state later}.
*
* <p>Live streams and streams with an {@linkplain AdsMediaSource#getAdsId() ads ID} that are not
* of type string are ignored and are not included in the returned list of ad resumption state.
*
* <p>See {@link HlsInterstitialsAdsLoader.Listener#onStop(MediaItem, Object, AdPlaybackState)}
* and {@link #addAdResumptionState(Object, AdPlaybackState)} also.
*/
public ImmutableList<AdsResumptionState> getAdsResumptionStates() {
ImmutableList.Builder<AdsResumptionState> resumptionStates = new ImmutableList.Builder<>();
for (AdPlaybackState adPlaybackState : activeAdPlaybackStates.values()) {
boolean isLiveStream = adPlaybackState.endsWithLivePostrollPlaceHolder();
if (!isLiveStream && adPlaybackState.adsId instanceof String) {
resumptionStates.add(
new AdsResumptionState((String) adPlaybackState.adsId, adPlaybackState.copy()));
} else {
Log.i(
TAG,
isLiveStream
? "getAdsResumptionStates(): ignoring active ad playback state of live stream."
+ " adsId="
+ adPlaybackState.adsId
: "getAdsResumptionStates(): ignoring active ad playback state when creating"
+ " resumption states. `adsId` is not of type String: "
+ castNonNull(adPlaybackState.adsId).getClass());
}
}
return resumptionStates.build();
}
/**
* Adds the given {@link AdsResumptionState} to resume playback of the {@link AdsMediaSource} with
* {@linkplain AdsMediaSource#getAdsId() ads ID} at the provided ad playback state.
*
* <p>If added while the given ads ID is active, the resumption state is ignored. The resumption
* state for a given ads ID must be added before {@link #start(AdsMediaSource, DataSpec, Object,
* AdViewProvider, EventListener)} or after {@link #stop(AdsMediaSource, EventListener)} is called
* for that ads ID.
*
* @param adsResumptionState The state to resume with.
* @throws IllegalArgumentException Thrown if the ad playback state {@linkplain
* AdPlaybackState#endsWithLivePostrollPlaceHolder() ends with a live placeholder}.
*/
public void addAdResumptionState(AdsResumptionState adsResumptionState) {
addAdResumptionState(adsResumptionState.adsId, adsResumptionState.adPlaybackState);
}
/**
* Adds the given {@link AdPlaybackState} to resume playback of the {@link AdsMediaSource} with
* {@linkplain AdsMediaSource#getAdsId() ads ID} at the provided ad playback state.
*
* <p>If added while the given ads ID is active, the resumption state is ignored. The resumption
* state for a given ads ID must be added before {@link #start(AdsMediaSource, DataSpec, Object,
* AdViewProvider, EventListener)} or after {@link #stop(AdsMediaSource, EventListener)} is called
* for that ads ID.
*
* @param adsId The ads ID identifying the {@link AdsMediaSource} to resume with the given state.
* @param adPlaybackState The state to resume with.
* @throws IllegalArgumentException Thrown if the ad playback state {@linkplain
* AdPlaybackState#endsWithLivePostrollPlaceHolder() ends with a live placeholder}.
*/
public void addAdResumptionState(Object adsId, AdPlaybackState adPlaybackState) {
checkArgument(!adPlaybackState.endsWithLivePostrollPlaceHolder());
if (!activeAdPlaybackStates.containsKey(adsId)) {
resumptionStates.put(adsId, adPlaybackState.copy().withAdsId(adsId));
} else {
Log.w(
TAG,
"Attempting to add an ad resumption state for an adsId that is currently active. adsId="
+ adsId);
}
}
/**
* Removes the {@link AdsResumptionState} for the given ads ID, or null if there is no active ad
* playback state for the given ads ID.
*
* @param adsId The ads ID for which to remove the resumption state.
* @return The removed resumption state or null.
*/
public boolean removeAdResumptionState(Object adsId) {
return resumptionStates.remove(adsId) != null;
}
/** Clears all ad resumptions states. */
public void clearAllAdResumptionStates() {
resumptionStates.clear();
}
@Override @Override
public void start( public void start(
AdsMediaSource adsMediaSource, AdsMediaSource adsMediaSource,
@ -736,19 +578,14 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
activeEventListeners.put(adsId, eventListener); activeEventListeners.put(adsId, eventListener);
MediaItem mediaItem = adsMediaSource.getMediaItem(); MediaItem mediaItem = adsMediaSource.getMediaItem();
if (isHlsMediaItem(mediaItem)) { if (isHlsMediaItem(mediaItem)) {
// Mark with NONE. Update and notify later when timeline with interstitials arrives.
activeAdPlaybackStates.put(adsId, AdPlaybackState.NONE);
insertedInterstitialIds.put(adsId, new HashSet<>()); insertedInterstitialIds.put(adsId, new HashSet<>());
unresolvedAssetLists.put(adsId, new TreeMap<>()); unresolvedAssetLists.put(adsId, new TreeMap<>());
if (adsId instanceof String && resumptionStates.containsKey(adsId)) {
// Use resumption playback state. Interstitials arriving with the timeline are ignored.
putAndNotifyAdPlaybackStateUpdate(adsId, checkNotNull(resumptionStates.remove(adsId)));
} else {
// Mark with NONE and wait for the timeline to get interstitials from the HLS playlist.
activeAdPlaybackStates.put(adsId, AdPlaybackState.NONE);
}
notifyListeners(listener -> listener.onStart(mediaItem, adsId, adViewProvider)); notifyListeners(listener -> listener.onStart(mediaItem, adsId, adViewProvider));
} else { } else {
Log.w(TAG, "Unsupported media item. Playing without ads for adsId=" + adsId);
putAndNotifyAdPlaybackStateUpdate(adsId, new AdPlaybackState(adsId)); putAndNotifyAdPlaybackStateUpdate(adsId, new AdPlaybackState(adsId));
Log.w(TAG, "Unsupported media item. Playing without ads for adsId=" + adsId);
unsupportedAdsIds.add(adsId); unsupportedAdsIds.add(adsId);
} }
} }
@ -877,12 +714,6 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
} }
} }
if (!isReleased && !unsupportedAdsIds.contains(adsId)) { if (!isReleased && !unsupportedAdsIds.contains(adsId)) {
if (adPlaybackState != null
&& adsId instanceof String
&& resumptionStates.containsKey(adsId)) {
// Update the resumption state in case the user has added one.
resumptionStates.put(adsId, adPlaybackState);
}
notifyListeners( notifyListeners(
listener -> listener ->
listener.onStop( listener.onStop(
@ -909,7 +740,6 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
if (activeEventListeners.isEmpty()) { if (activeEventListeners.isEmpty()) {
player = null; player = null;
} }
clearAllAdResumptionStates();
cancelPendingAssetListResolutionMessage(); cancelPendingAssetListResolutionMessage();
if (loader != null) { if (loader != null) {
loader.release(); loader.release();

View File

@ -53,7 +53,6 @@ import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.PlayerMessage; import androidx.media3.exoplayer.PlayerMessage;
import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.AdsResumptionState;
import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.Asset; import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.Asset;
import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.AssetList; import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.AssetList;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
@ -61,7 +60,6 @@ import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.ads.AdsLoader; import androidx.media3.exoplayer.source.ads.AdsLoader;
import androidx.media3.exoplayer.source.ads.AdsMediaSource; import androidx.media3.exoplayer.source.ads.AdsMediaSource;
import androidx.media3.test.utils.FakeMediaSource;
import androidx.media3.test.utils.FakeTimeline; import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition; import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@ -258,270 +256,6 @@ public class HlsInterstitialsAdsLoaderTest {
verifyNoMoreInteractions(mockAdsLoaderListener); verifyNoMoreInteractions(mockAdsLoaderListener);
} }
@Test
public void start_resumptionStateAvailable_resumptionStateUsedAndEventListenerCalled() {
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0, 10L, C.TIME_END_OF_SOURCE)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withAdCount(/* adGroupIndex= */ 1, 2)
.withAdCount(/* adGroupIndex= */ 2, 3)
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com"));
adsLoader.addAdResumptionState(new AdsResumptionState("adsId", adPlaybackState));
adsLoader.setPlayer(mockPlayer);
adsLoader.start(adsMediaSource, adTagDataSpec, "adsId", mockAdViewProvider, mockEventListener);
adsLoader.stop(adsMediaSource, mockEventListener);
ArgumentCaptor<AdPlaybackState> adPlaybackStateArgumentCaptor =
ArgumentCaptor.forClass(AdPlaybackState.class);
verify(mockEventListener).onAdPlaybackState(adPlaybackStateArgumentCaptor.capture());
verify(mockAdsLoaderListener)
.onStop(any(), eq("adsId"), adPlaybackStateArgumentCaptor.capture());
assertThat(adPlaybackStateArgumentCaptor.getAllValues())
.containsExactly(adPlaybackState, adPlaybackState);
verify(mockAdsLoaderListener).onStart(eq(contentMediaItem), eq("adsId"), isNotNull());
assertThat(adsLoader.removeAdResumptionState("adsId")).isFalse();
}
@Test
public void addAdResumptionState_whileAdsIdIsActive_ignored() throws IOException {
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"
+ "#EXTINF:6,\n"
+ "main2.0.ts\n"
+ "#EXTINF:6,\n"
+ "main3.0.ts\n"
+ "#EXT-X-ENDLIST"
+ "\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad0-0\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:55:44.000Z\","
+ "CUE=\"PRE\","
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
+ "\n",
adsLoader,
/* windowIndex= */ 0,
/* windowPositionInPeriodUs= */ 0,
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
adsLoader.addAdResumptionState(new AdsResumptionState("adsId", new AdPlaybackState("adsId")));
assertThat(adsLoader.getAdsResumptionStates())
.containsExactly(
new AdsResumptionState(
"adsId",
new AdPlaybackState("adsId", 0L)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType("application/x-mpegURL")
.build())));
assertThat(adsLoader.removeAdResumptionState("adsId")).isFalse();
}
@Test
public void addAdResumptionState_withLivePostRollHolder_throwsIllegalArgumentException() {
AdsResumptionState adsResumptionState =
new AdsResumptionState(
"adsId",
new AdPlaybackState("adsId")
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false));
assertThrows(
IllegalArgumentException.class, () -> adsLoader.addAdResumptionState(adsResumptionState));
}
@Test
public void getAdsResumptionStates_withLivePostRollPlaceholder_ignored() throws IOException {
List<AdPlaybackState> adPlaybackStates =
callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates(
adsLoader,
/* startAdsLoader= */ true,
/* windowOffsetInFirstPeriodUs= */ 0L,
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:6\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
+ "#EXTINF:6,\nmain0.0.ts\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"
+ "\n");
// active ad playback state with live post roll is ignored.
assertThat(adsLoader.getAdsResumptionStates()).isEmpty();
// Stop to verify that there was an active ad playback state when calling getAdResumptionStates.
adsLoader.stop(adsMediaSource, mockEventListener);
ArgumentCaptor<AdPlaybackState> adPlaybackState =
ArgumentCaptor.forClass(AdPlaybackState.class);
verify(mockAdsLoaderListener).onStop(any(), eq("adsId"), adPlaybackState.capture());
assertThat(adPlaybackState.getAllValues()).isEqualTo(adPlaybackStates);
}
@Test
public void getAdsResumptionStates_returnsResumptionStateOfActiveAdsIds() throws IOException {
String secondPlaylistString =
"#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"
+ "#EXTINF:6,\n"
+ "main2.0.ts\n"
+ "#EXTINF:6,\n"
+ "main3.0.ts\n"
+ "#EXT-X-ENDLIST"
+ "\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad1-1\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:55:44.000Z\","
+ "CUE=\"POST\","
+ "X-ASSET-URI=\"http://example.com/media-1-1.m3u8\""
+ "\n";
HlsMediaPlaylist secondMediaPlaylist =
(HlsMediaPlaylist)
new HlsPlaylistParser()
.parse(
Uri.EMPTY, new ByteArrayInputStream(Util.getUtf8Bytes(secondPlaylistString)));
HlsManifest secondHlsManifest =
new HlsManifest(/* multivariantPlaylist= */ null, secondMediaPlaylist);
TimelineWindowDefinition secondInitialTimelineWindowDefinition =
new TimelineWindowDefinition.Builder()
.setPlaceholder(true)
.setDynamic(true)
.setDurationUs(C.TIME_UNSET)
.setWindowPositionInFirstPeriodUs(0)
.setMediaItem(MediaItem.fromUri("http://example.com/2.m3u8"))
.build();
AdsMediaSource secondAdsMediaSource =
new AdsMediaSource(
new FakeMediaSource(new FakeTimeline(secondInitialTimelineWindowDefinition)),
new DataSpec(secondInitialTimelineWindowDefinition.mediaItem.localConfiguration.uri),
"adsId2",
new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()),
adsLoader,
mockAdViewProvider);
AdsResumptionState firstAdsResumptionState =
new AdsResumptionState(
"adsId",
new AdPlaybackState("adsId", 0L)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType("application/x-mpegURL")
.build()));
AdsResumptionState secondAdsResumptionState =
new AdsResumptionState(
"adsId2",
new AdPlaybackState("adsId2", C.TIME_END_OF_SOURCE)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad1-1")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
new MediaItem.Builder()
.setUri("http://example.com/media-1-1.m3u8")
.setMimeType("application/x-mpegURL")
.build()));
// Start the first adsId with a pre roll.
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"
+ "#EXTINF:6,\n"
+ "main2.0.ts\n"
+ "#EXTINF:6,\n"
+ "main3.0.ts\n"
+ "#EXT-X-ENDLIST"
+ "\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad0-0\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:55:44.000Z\","
+ "CUE=\"PRE\","
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
+ "\n",
adsLoader,
/* windowIndex= */ 0,
/* windowPositionInPeriodUs= */ 0,
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
assertThat(adsLoader.getAdsResumptionStates()).containsExactly(firstAdsResumptionState);
// Start a second adsId with a post roll.
adsLoader.start(
secondAdsMediaSource,
new DataSpec(Uri.EMPTY),
"adsId2",
mockAdViewProvider,
mockEventListener);
adsLoader.handleContentTimelineChanged(
secondAdsMediaSource,
new FakeTimeline(
new Object[] {secondHlsManifest},
secondInitialTimelineWindowDefinition
.buildUpon()
.setDurationUs(secondMediaPlaylist.durationUs)
.setDynamic(false)
.setPlaceholder(false)
.build()));
assertThat(adsLoader.getAdsResumptionStates())
.containsExactly(firstAdsResumptionState, secondAdsResumptionState);
// Stop the first ads media source.
adsLoader.stop(adsMediaSource, mockEventListener);
assertThat(adsLoader.getAdsResumptionStates()).containsExactly(secondAdsResumptionState);
// Stop the second ads media source.
adsLoader.stop(secondAdsMediaSource, mockEventListener);
assertThat(adsLoader.getAdsResumptionStates()).isEmpty();
}
@Test
public void removeAdResumptionState_removesAvailableResumptionState() {
AdsResumptionState adsResumptionState =
new AdsResumptionState("adsId", new AdPlaybackState("adsId"));
adsLoader.addAdResumptionState(adsResumptionState);
assertThat(adsLoader.removeAdResumptionState("adsId")).isTrue();
assertThat(adsLoader.removeAdResumptionState("adsId")).isFalse();
}
@Test
public void clearAllAdResumptionStates_removesAvailableResumptionState() {
adsLoader.addAdResumptionState(new AdsResumptionState("adsId", new AdPlaybackState("adsId")));
adsLoader.addAdResumptionState("adsId2", new AdPlaybackState("adsId2"));
adsLoader.clearAllAdResumptionStates();
assertThat(adsLoader.removeAdResumptionState("adsId")).isFalse();
}
@Test @Test
public void handleContentTimelineChanged_preMidAndPostRolls_translatedToAdPlaybackState() public void handleContentTimelineChanged_preMidAndPostRolls_translatedToAdPlaybackState()
throws IOException { throws IOException {
@ -3675,16 +3409,6 @@ public class HlsInterstitialsAdsLoaderTest {
verifyNoMoreInteractions(mockPlayer); verifyNoMoreInteractions(mockPlayer);
} }
@Test
public void release_clearsResumptionStates() {
adsLoader.addAdResumptionState(
"adsId", new AdPlaybackState(/* adsId= */ "adsId", 0L, C.TIME_END_OF_SOURCE));
adsLoader.release();
assertThat(adsLoader.removeAdResumptionState("adsId")).isFalse();
}
@Test @Test
public void release_afterStartButBeforeStopped_playerListenerRemovedAfterAllSourcesStopped() { public void release_afterStartButBeforeStopped_playerListenerRemovedAfterAllSourcesStopped() {
when(mockPlayer.getCurrentTimeline()).thenReturn(new FakeTimeline(contentWindowDefinition)); when(mockPlayer.getCurrentTimeline()).thenReturn(new FakeTimeline(contentWindowDefinition));
@ -3976,29 +3700,6 @@ public class HlsInterstitialsAdsLoaderTest {
verifyNoMoreInteractions(mockEventListener); verifyNoMoreInteractions(mockEventListener);
} }
@Test
public void state_bundleUnbundleRoundTrip_createsEqualInstance() {
AdPlaybackState adPlaybackState =
new AdPlaybackState(
/* adsId= */ "1234", /* adGroupTimesUs...= */ 0L, 10L, C.TIME_END_OF_SOURCE)
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ true)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
AdsResumptionState adsResumptionState = new AdsResumptionState("1234", adPlaybackState);
AdsResumptionState resultingAdsResumptionState =
AdsResumptionState.fromBundle(adsResumptionState.toBundle());
assertThat(resultingAdsResumptionState).isEqualTo(adsResumptionState);
}
@Test
public void state_constructorWithAdsIdsThatDoNotMatch_throwsIllegalArgumentException() {
AdPlaybackState adPlaybackState = new AdPlaybackState("1234");
assertThrows(
IllegalArgumentException.class, () -> new AdsResumptionState("5678", adPlaybackState));
}
private List<AdPlaybackState> callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates( private List<AdPlaybackState> callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates(
HlsInterstitialsAdsLoader adsLoader, HlsInterstitialsAdsLoader adsLoader,
boolean startAdsLoader, boolean startAdsLoader,

View File

@ -17,7 +17,6 @@ package androidx.media3.extractor;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.ParserException; import androidx.media3.common.ParserException;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import java.io.EOFException; import java.io.EOFException;
@ -123,58 +122,5 @@ public final class ExtractorUtil {
} }
} }
/**
* Returns the maximum encoded rate for samples of the given encoding.
*
* @param encoding A {@link C.Encoding}.
* @return The maximum encoded rate for this encoding in bytes per second, or {@link
* C#RATE_UNSET_INT} if unknown.
*/
public static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) {
switch (encoding) {
case C.ENCODING_MP3:
return MpegAudioUtil.MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_LC:
return AacUtil.AAC_LC_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_HE_V1:
return AacUtil.AAC_HE_V1_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_HE_V2:
return AacUtil.AAC_HE_V2_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_XHE:
return AacUtil.AAC_XHE_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_ELD:
return AacUtil.AAC_ELD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AC3:
return Ac3Util.AC3_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_E_AC3:
case C.ENCODING_E_AC3_JOC:
return Ac3Util.E_AC3_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AC4:
return Ac4Util.MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DTS:
return DtsUtil.DTS_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DTS_HD:
case C.ENCODING_DTS_UHD_P2:
return DtsUtil.DTS_HD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DOLBY_TRUEHD:
return Ac3Util.TRUEHD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_OPUS:
return OpusUtil.MAX_BYTES_PER_SECOND;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
case C.ENCODING_PCM_24BIT:
case C.ENCODING_PCM_24BIT_BIG_ENDIAN:
case C.ENCODING_PCM_32BIT:
case C.ENCODING_PCM_32BIT_BIG_ENDIAN:
case C.ENCODING_PCM_8BIT:
case C.ENCODING_PCM_FLOAT:
case C.ENCODING_AAC_ER_BSAC:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
return C.RATE_UNSET_INT;
}
}
private ExtractorUtil() {} private ExtractorUtil() {}
} }