mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Compare commits
3 Commits
52db3a240d
...
d0833c4e7c
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d0833c4e7c | ||
![]() |
7f6ddef502 | ||
![]() |
c4c3e5e0c8 |
@ -69,6 +69,7 @@
|
|||||||
* 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:
|
||||||
|
@ -596,6 +596,21 @@ 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;
|
||||||
@ -944,6 +959,20 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
@ -26,6 +26,7 @@ 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;
|
||||||
@ -1201,4 +1202,65 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,7 @@ 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;
|
||||||
@ -1479,8 +1480,7 @@ 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
|
||||||
: DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond(
|
: getNonPcmMaximumEncodedRateBytesPerSecond(configuration.outputEncoding);
|
||||||
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,6 +2387,12 @@ 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() {}
|
||||||
@ -2404,8 +2410,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
: Util.scaleLargeValue(
|
: Util.scaleLargeValue(
|
||||||
audioTrack.getBufferSizeInFrames(),
|
audioTrack.getBufferSizeInFrames(),
|
||||||
C.MICROS_PER_SECOND,
|
C.MICROS_PER_SECOND,
|
||||||
DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond(
|
getNonPcmMaximumEncodedRateBytesPerSecond(configuration.outputEncoding),
|
||||||
configuration.outputEncoding),
|
|
||||||
RoundingMode.DOWN);
|
RoundingMode.DOWN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
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;
|
||||||
@ -28,12 +29,7 @@ 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.AacUtil;
|
import androidx.media3.extractor.ExtractorUtil;
|
||||||
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;
|
||||||
|
|
||||||
@ -266,13 +262,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)
|
||||||
: getMaximumEncodedRateBytesPerSecond(encoding);
|
: getNonPcmMaximumEncodedRateBytesPerSecond(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 = getMaximumEncodedRateBytesPerSecond(encoding);
|
int maxByteRate = getNonPcmMaximumEncodedRateBytesPerSecond(encoding);
|
||||||
return checkedCast((long) offloadBufferDurationUs * maxByteRate / C.MICROS_PER_SECOND);
|
return checkedCast((long) offloadBufferDurationUs * maxByteRate / C.MICROS_PER_SECOND);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,49 +276,9 @@ public class DefaultAudioTrackBufferSizeProvider
|
|||||||
return checkedCast((long) durationUs * samplingRate * frameSize / C.MICROS_PER_SECOND);
|
return checkedCast((long) durationUs * samplingRate * frameSize / C.MICROS_PER_SECOND);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) {
|
private static int getNonPcmMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) {
|
||||||
switch (encoding) {
|
int rate = ExtractorUtil.getMaximumEncodedRateBytesPerSecond(encoding);
|
||||||
case C.ENCODING_MP3:
|
checkState(rate != C.RATE_UNSET_INT);
|
||||||
return MpegAudioUtil.MAX_RATE_BYTES_PER_SECOND;
|
return rate;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -830,7 +830,6 @@ 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) {
|
||||||
@ -863,15 +862,11 @@ 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);
|
||||||
assertPreparedWithProgressiveSource();
|
assertPreparedWithMedia();
|
||||||
populateDownloadRequestBuilderWithDownloadRange(builder, startPositionMs, durationMs);
|
populateDownloadRequestBuilderWithDownloadRange(builder, startPositionMs, durationMs);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
@ -906,13 +901,22 @@ 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 =
|
||||||
@ -957,6 +961,25 @@ 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",
|
||||||
|
@ -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.exoplayer.audio.DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond;
|
import static androidx.media3.extractor.ExtractorUtil.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;
|
||||||
|
@ -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.exoplayer.audio.DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond;
|
import static androidx.media3.extractor.ExtractorUtil.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;
|
||||||
|
@ -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.exoplayer.audio.DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond;
|
import static androidx.media3.extractor.ExtractorUtil.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;
|
||||||
|
@ -18,7 +18,6 @@ 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;
|
||||||
@ -72,10 +71,15 @@ 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(/* periodCount= */ 2, /* id= */ new Object()));
|
new TimelineWindowDefinition.Builder()
|
||||||
|
.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;
|
||||||
@ -86,6 +90,7 @@ 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
|
||||||
@ -124,7 +129,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};
|
||||||
|
|
||||||
@ -329,8 +334,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.
|
||||||
TrackSelectionParameters parameters =
|
DefaultTrackSelector.Parameters parameters =
|
||||||
new TrackSelectionParameters.Builder(getApplicationContext())
|
new DefaultTrackSelector.Parameters.Builder()
|
||||||
.setPreferredAudioLanguage("de")
|
.setPreferredAudioLanguage("de")
|
||||||
.setPreferredTextLanguage("en")
|
.setPreferredTextLanguage("en")
|
||||||
.build();
|
.build();
|
||||||
@ -433,8 +438,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.
|
||||||
TrackSelectionParameters parameters =
|
DefaultTrackSelector.Parameters parameters =
|
||||||
new TrackSelectionParameters.Builder(getApplicationContext())
|
new DefaultTrackSelector.Parameters.Builder()
|
||||||
.setPreferredAudioLanguage("de")
|
.setPreferredAudioLanguage("de")
|
||||||
.setPreferredTextLanguage("en")
|
.setPreferredTextLanguage("en")
|
||||||
.build();
|
.build();
|
||||||
@ -464,8 +469,8 @@ public class DownloadHelperTest {
|
|||||||
throws Exception {
|
throws Exception {
|
||||||
prepareDownloadHelper(downloadHelper);
|
prepareDownloadHelper(downloadHelper);
|
||||||
|
|
||||||
TrackSelectionParameters parameters =
|
DefaultTrackSelector.Parameters parameters =
|
||||||
new TrackSelectionParameters.Builder(getApplicationContext())
|
new DefaultTrackSelector.Parameters.Builder()
|
||||||
.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)
|
||||||
@ -485,7 +490,7 @@ public class DownloadHelperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
getDownloadRequest_createsDownloadRequestWithConcreteTimeRange_requestContainsConcreteByteRange()
|
getDownloadRequestForProgressive_withConcreteTimeRange_requestContainsConcreteByteRange()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
DownloadHelper downloadHelper =
|
DownloadHelper downloadHelper =
|
||||||
new DownloadHelper.Factory()
|
new DownloadHelper.Factory()
|
||||||
@ -504,7 +509,7 @@ public class DownloadHelperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
getDownloadRequest_createsDownloadRequestWithUnsetStartPosition_requestContainsConcreteByteRange()
|
getDownloadRequestForProgressive_withUnsetStartPosition_requestContainsConcreteByteRange()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
DownloadHelper downloadHelper =
|
DownloadHelper downloadHelper =
|
||||||
new DownloadHelper.Factory()
|
new DownloadHelper.Factory()
|
||||||
@ -522,8 +527,9 @@ public class DownloadHelperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getDownloadRequest_createsDownloadRequestWithUnsetLength_requestContainsUnsetLength()
|
public void
|
||||||
throws Exception {
|
getDownloadRequestForProgressive_withUnsetDuration_requestContainsUnsetByteRangeLength()
|
||||||
|
throws Exception {
|
||||||
DownloadHelper downloadHelper =
|
DownloadHelper downloadHelper =
|
||||||
new DownloadHelper.Factory()
|
new DownloadHelper.Factory()
|
||||||
.setDataSourceFactory(new DefaultDataSource.Factory(getApplicationContext()))
|
.setDataSourceFactory(new DefaultDataSource.Factory(getApplicationContext()))
|
||||||
@ -541,7 +547,7 @@ public class DownloadHelperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
getDownloadRequest_createsDownloadRequestForTooShortStreamWithTimeRange_requestContainsUnsetLength()
|
getDownloadRequestForShortProgressive_withConcreteTimeRange_requestContainsUnsetByteRangeLength()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
DownloadHelper downloadHelper =
|
DownloadHelper downloadHelper =
|
||||||
new DownloadHelper.Factory()
|
new DownloadHelper.Factory()
|
||||||
@ -559,9 +565,8 @@ public class DownloadHelperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void getDownloadRequestForProgressive_withoutRange_requestContainsNullByteRange()
|
||||||
getDownloadRequest_createsDownloadRequestWithoutTimeRange_requestContainsNullByteRange()
|
throws Exception {
|
||||||
throws Exception {
|
|
||||||
DownloadHelper downloadHelper =
|
DownloadHelper downloadHelper =
|
||||||
new DownloadHelper.Factory()
|
new DownloadHelper.Factory()
|
||||||
.setDataSourceFactory(new DefaultDataSource.Factory(getApplicationContext()))
|
.setDataSourceFactory(new DefaultDataSource.Factory(getApplicationContext()))
|
||||||
@ -575,17 +580,155 @@ public class DownloadHelperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
getDownloadRequest_createDownloadRequestWithTimeRangeForNonProgressiveStream_throwsIllegalStateException()
|
getDownloadRequestForNonProgressive_withConcreteTimeRange_requestContainsCorrectTimeRange()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
// We use this.downloadHelper as it was created with a TestMediaSource, thus the DownloadHelper
|
DownloadHelper downloadHelper =
|
||||||
// will treat it as non-progressive.
|
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);
|
prepareDownloadHelper(downloadHelper);
|
||||||
|
|
||||||
assertThrows(
|
DownloadRequest downloadRequest =
|
||||||
IllegalStateException.class,
|
downloadHelper.getDownloadRequest(
|
||||||
() ->
|
/* data= */ null, /* startPositionMs= */ 0, /* durationMs= */ 10000);
|
||||||
downloadHelper.getDownloadRequest(
|
|
||||||
/* data= */ null, /* startPositionMs= */ 0, /* durationMs= */ 10000));
|
assertThat(downloadRequest.timeRange).isNotNull();
|
||||||
|
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
|
||||||
|
@ -40,6 +40,7 @@ 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;
|
||||||
@ -94,6 +95,7 @@ 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 {
|
||||||
|
|
||||||
@ -205,6 +207,67 @@ 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.
|
||||||
*/
|
*/
|
||||||
@ -474,6 +537,7 @@ 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;
|
||||||
|
|
||||||
@ -504,6 +568,7 @@ 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<>();
|
||||||
}
|
}
|
||||||
@ -553,6 +618,99 @@ 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,
|
||||||
@ -578,14 +736,19 @@ 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 {
|
||||||
putAndNotifyAdPlaybackStateUpdate(adsId, new AdPlaybackState(adsId));
|
|
||||||
Log.w(TAG, "Unsupported media item. Playing without ads for adsId=" + adsId);
|
Log.w(TAG, "Unsupported media item. Playing without ads for adsId=" + adsId);
|
||||||
|
putAndNotifyAdPlaybackStateUpdate(adsId, new AdPlaybackState(adsId));
|
||||||
unsupportedAdsIds.add(adsId);
|
unsupportedAdsIds.add(adsId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -714,6 +877,12 @@ 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(
|
||||||
@ -740,6 +909,7 @@ 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();
|
||||||
|
@ -53,6 +53,7 @@ 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;
|
||||||
@ -60,6 +61,7 @@ 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;
|
||||||
@ -256,6 +258,270 @@ 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 {
|
||||||
@ -3409,6 +3675,16 @@ 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));
|
||||||
@ -3700,6 +3976,29 @@ 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,
|
||||||
|
@ -17,6 +17,7 @@ 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;
|
||||||
@ -122,5 +123,58 @@ 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() {}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user