From 537f8b26652913d8afe882b814724b3bc6c7d8d8 Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Fri, 11 Nov 2022 11:23:41 +0000 Subject: [PATCH 001/141] Changed decoder list sort to order by functional support of format Added new method to check if codec just functionally supports a format. Changed getDecoderInfosSortedByFormatSupport to use new function to order by functional support. This allows decoders that only support functionally and are more preferred by the MediaCodecSelector to keep their preferred position in the sorted list. Unit tests included -Two MediaCodecVideoRenderer tests that verify hw vs sw does not have an effect on sort of the decoder list, it is only based on functional support. Issue: google/ExoPlayer#10604 PiperOrigin-RevId: 487779284 (cherry picked from commit fab66d972ef84599cdaa2b498b91f21d104fbf26) --- RELEASENOTES.md | 12 +- .../exoplayer/RendererCapabilities.java | 6 +- .../exoplayer/mediacodec/MediaCodecInfo.java | 24 +++- .../mediacodec/MediaCodecRenderer.java | 8 ++ .../exoplayer/mediacodec/MediaCodecUtil.java | 13 +- .../video/MediaCodecVideoRendererTest.java | 121 ++++++++++++++++++ 6 files changed, 163 insertions(+), 21 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1eb87be7155..33f9a6146d3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,4 +1,14 @@ -Release notes +# Release notes + +### Unreleased changes + +* Core library: + * Tweak the renderer's decoder ordering logic to uphold the + `MediaCodecSelector`'s preferences, even if a decoder reports it may not + be able to play the media performantly. For example with default + selector, hardware decoder with only functional support will be + preferred over software decoder that fully supports the format + ([#10604](https://github.com/google/ExoPlayer/issues/10604)). ### 1.0.0-beta03 (2022-11-22) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilities.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilities.java index 604b607842e..dbc2fa059e0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilities.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilities.java @@ -144,13 +144,13 @@ public interface RendererCapabilities { /** A mask to apply to {@link Capabilities} to obtain {@link DecoderSupport} only. */ int MODE_SUPPORT_MASK = 0b11 << 7; /** - * The renderer will use a decoder for fallback mimetype if possible as format's MIME type is - * unsupported + * The format's MIME type is unsupported and the renderer may use a decoder for a fallback MIME + * type. */ int DECODER_SUPPORT_FALLBACK_MIMETYPE = 0b10 << 7; /** The renderer is able to use the primary decoder for the format's MIME type. */ int DECODER_SUPPORT_PRIMARY = 0b1 << 7; - /** The renderer will use a fallback decoder. */ + /** The format exceeds the primary decoder's capabilities but is supported by fallback decoder */ int DECODER_SUPPORT_FALLBACK = 0; /** diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java index 72733a90e9a..e49a9a5a3cc 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java @@ -245,7 +245,8 @@ public int getMaxSupportedInstances() { } /** - * Returns whether the decoder may support decoding the given {@code format}. + * Returns whether the decoder may support decoding the given {@code format} both functionally and + * performantly. * * @param format The input media format. * @return Whether the decoder may support decoding the given {@code format}. @@ -256,7 +257,7 @@ public boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQue return false; } - if (!isCodecProfileAndLevelSupported(format)) { + if (!isCodecProfileAndLevelSupported(format, /* checkPerformanceCapabilities= */ true)) { return false; } @@ -283,15 +284,24 @@ public boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQue } } + /** + * Returns whether the decoder may functionally support decoding the given {@code format}. + * + * @param format The input media format. + * @return Whether the decoder may functionally support decoding the given {@code format}. + */ + public boolean isFormatFunctionallySupported(Format format) { + return isSampleMimeTypeSupported(format) + && isCodecProfileAndLevelSupported(format, /* checkPerformanceCapabilities= */ false); + } + private boolean isSampleMimeTypeSupported(Format format) { return mimeType.equals(format.sampleMimeType) || mimeType.equals(MediaCodecUtil.getAlternativeCodecMimeType(format)); } - private boolean isCodecProfileAndLevelSupported(Format format) { - if (format.codecs == null) { - return true; - } + private boolean isCodecProfileAndLevelSupported( + Format format, boolean checkPerformanceCapabilities) { Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel == null) { // If we don't know any better, we assume that the profile and level are supported. @@ -327,7 +337,7 @@ private boolean isCodecProfileAndLevelSupported(Format format) { for (CodecProfileLevel profileLevel : profileLevels) { if (profileLevel.profile == profile - && profileLevel.level >= level + && (profileLevel.level >= level || !checkPerformanceCapabilities) && !needsProfileExcludedWorkaround(mimeType, profile)) { return true; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java index 79c3b9ca7a4..4a4c43e6d4a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java @@ -1113,6 +1113,14 @@ private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exce } codecInitializedTimestamp = SystemClock.elapsedRealtime(); + if (!codecInfo.isFormatSupported(inputFormat)) { + Log.w( + TAG, + Util.formatInvariant( + "Format exceeds selected codec's capabilities [%s, %s]", + Format.toLogString(inputFormat), codecName)); + } + this.codecInfo = codecInfo; this.codecOperatingRate = codecOperatingRate; codecInputFormat = inputFormat; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java index c3200150d0d..e97e053084d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java @@ -190,22 +190,15 @@ public static synchronized List getDecoderInfos( } /** - * Returns a copy of the provided decoder list sorted such that decoders with format support are - * listed first. The returned list is modifiable for convenience. + * Returns a copy of the provided decoder list sorted such that decoders with functional format + * support are listed first. The returned list is modifiable for convenience. */ @CheckResult public static List getDecoderInfosSortedByFormatSupport( List decoderInfos, Format format) { decoderInfos = new ArrayList<>(decoderInfos); sortByScore( - decoderInfos, - decoderInfo -> { - try { - return decoderInfo.isFormatSupported(format) ? 1 : 0; - } catch (DecoderQueryException e) { - return -1; - } - }); + decoderInfos, decoderInfo -> decoderInfo.isFormatFunctionallySupported(format) ? 1 : 0); return decoderInfos; } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java index 4e3f48ea062..d42ab38fe02 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java @@ -58,6 +58,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import java.util.Collections; +import java.util.List; import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; @@ -84,6 +85,32 @@ public class MediaCodecVideoRendererTest { .setHeight(1080) .build(); + private static final MediaCodecInfo H264_PROFILE8_LEVEL4_HW_MEDIA_CODEC_INFO = + MediaCodecInfo.newInstance( + /* name= */ "h264-codec-hw", + /* mimeType= */ MimeTypes.VIDEO_H264, + /* codecMimeType= */ MimeTypes.VIDEO_H264, + /* capabilities= */ createCodecCapabilities( + CodecProfileLevel.AVCProfileHigh, CodecProfileLevel.AVCLevel4), + /* hardwareAccelerated= */ true, + /* softwareOnly= */ false, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); + + private static final MediaCodecInfo H264_PROFILE8_LEVEL5_SW_MEDIA_CODEC_INFO = + MediaCodecInfo.newInstance( + /* name= */ "h264-codec-sw", + /* mimeType= */ MimeTypes.VIDEO_H264, + /* codecMimeType= */ MimeTypes.VIDEO_H264, + /* capabilities= */ createCodecCapabilities( + CodecProfileLevel.AVCProfileHigh, CodecProfileLevel.AVCLevel5), + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); + private Looper testMainLooper; private Surface surface; private MediaCodecVideoRenderer mediaCodecVideoRenderer; @@ -711,6 +738,100 @@ public void supportsFormat_withDolbyVision_setsDecoderSupportFlagsByDisplayDolby .isEqualTo(RendererCapabilities.DECODER_SUPPORT_PRIMARY); } + @Test + public void getDecoderInfo_withNonPerformantHardwareDecoder_returnsHardwareDecoderFirst() + throws Exception { + // AVC Format, Profile: 8, Level: 8192 + Format avcFormat = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setCodecs("avc1.64002a") + .build(); + // Provide hardware and software AVC decoders + MediaCodecSelector mediaCodecSelector = + (mimeType, requiresSecureDecoder, requiresTunnelingDecoder) -> { + if (!mimeType.equals(MimeTypes.VIDEO_H264)) { + return ImmutableList.of(); + } + // Hardware decoder supports above format functionally but not performantly as + // it supports MIME type & Profile but not Level + // Software decoder supports format functionally and peformantly as it supports + // MIME type, Profile, and Level(assuming resolution/frame rate support too) + return ImmutableList.of( + H264_PROFILE8_LEVEL4_HW_MEDIA_CODEC_INFO, H264_PROFILE8_LEVEL5_SW_MEDIA_CODEC_INFO); + }; + MediaCodecVideoRenderer renderer = + new MediaCodecVideoRenderer( + ApplicationProvider.getApplicationContext(), + mediaCodecSelector, + /* allowedJoiningTimeMs= */ 0, + /* eventHandler= */ new Handler(testMainLooper), + /* eventListener= */ eventListener, + /* maxDroppedFramesToNotify= */ 1); + renderer.init(/* index= */ 0, PlayerId.UNSET); + + List mediaCodecInfoList = + renderer.getDecoderInfos(mediaCodecSelector, avcFormat, false); + @Capabilities int capabilities = renderer.supportsFormat(avcFormat); + + assertThat(mediaCodecInfoList).hasSize(2); + assertThat(mediaCodecInfoList.get(0).hardwareAccelerated).isTrue(); + assertThat(RendererCapabilities.getFormatSupport(capabilities)).isEqualTo(C.FORMAT_HANDLED); + assertThat(RendererCapabilities.getDecoderSupport(capabilities)) + .isEqualTo(RendererCapabilities.DECODER_SUPPORT_FALLBACK); + } + + @Test + public void getDecoderInfo_softwareDecoderPreferred_returnsSoftwareDecoderFirst() + throws Exception { + // AVC Format, Profile: 8, Level: 8192 + Format avcFormat = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setCodecs("avc1.64002a") + .build(); + // Provide software and hardware AVC decoders + MediaCodecSelector mediaCodecSelector = + (mimeType, requiresSecureDecoder, requiresTunnelingDecoder) -> { + if (!mimeType.equals(MimeTypes.VIDEO_H264)) { + return ImmutableList.of(); + } + // Hardware decoder supports above format functionally but not performantly as + // it supports MIME type & Profile but not Level + // Software decoder supports format functionally and peformantly as it supports + // MIME type, Profile, and Level(assuming resolution/frame rate support too) + return ImmutableList.of( + H264_PROFILE8_LEVEL5_SW_MEDIA_CODEC_INFO, H264_PROFILE8_LEVEL4_HW_MEDIA_CODEC_INFO); + }; + MediaCodecVideoRenderer renderer = + new MediaCodecVideoRenderer( + ApplicationProvider.getApplicationContext(), + mediaCodecSelector, + /* allowedJoiningTimeMs= */ 0, + /* eventHandler= */ new Handler(testMainLooper), + /* eventListener= */ eventListener, + /* maxDroppedFramesToNotify= */ 1); + renderer.init(/* index= */ 0, PlayerId.UNSET); + + List mediaCodecInfoList = + renderer.getDecoderInfos(mediaCodecSelector, avcFormat, false); + @Capabilities int capabilities = renderer.supportsFormat(avcFormat); + + assertThat(mediaCodecInfoList).hasSize(2); + assertThat(mediaCodecInfoList.get(0).hardwareAccelerated).isFalse(); + assertThat(RendererCapabilities.getFormatSupport(capabilities)).isEqualTo(C.FORMAT_HANDLED); + assertThat(RendererCapabilities.getDecoderSupport(capabilities)) + .isEqualTo(RendererCapabilities.DECODER_SUPPORT_PRIMARY); + } + + private static CodecCapabilities createCodecCapabilities(int profile, int level) { + CodecCapabilities capabilities = new CodecCapabilities(); + capabilities.profileLevels = new CodecProfileLevel[] {new CodecProfileLevel()}; + capabilities.profileLevels[0].profile = profile; + capabilities.profileLevels[0].level = level; + return capabilities; + } + @Test public void getCodecMaxInputSize_videoH263() { MediaCodecInfo codecInfo = createMediaCodecInfo(MimeTypes.VIDEO_H263); From bbf73244945ec8230590f62d5a4589ae1031abde Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Wed, 16 Nov 2022 10:32:29 +0000 Subject: [PATCH 002/141] Update targetSdkVersion of demo session app to appTargetSdkVersion PiperOrigin-RevId: 488884403 (cherry picked from commit cfe36af8478e78dd6e334298bcee425c61a9ba2a) --- demos/session/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/session/build.gradle b/demos/session/build.gradle index b3b61dac554..376c69534d3 100644 --- a/demos/session/build.gradle +++ b/demos/session/build.gradle @@ -31,7 +31,7 @@ android { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion multiDexEnabled true } From 73d40e1cfc339ef3d8a7466d34c762dcc65627af Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 16 Nov 2022 15:52:36 +0000 Subject: [PATCH 003/141] Add bundling exclusions with unit tests The exclusion will be used in a follow-up CL when sending PlayerInfo updates. #minor-release PiperOrigin-RevId: 488939258 (cherry picked from commit bae509009bd62554876ecb7485708e50af4eaa2a) --- .../androidx/media3/session/PlayerInfo.java | 73 ++++++++++++++++++- .../media3/session/PlayerInfoTest.java | 53 ++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java index c1b67c62077..8fe6eece283 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java @@ -46,6 +46,8 @@ import androidx.media3.common.VideoSize; import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.UnstableApi; +import com.google.common.base.Objects; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -58,6 +60,75 @@ */ /* package */ class PlayerInfo implements Bundleable { + /** + * Holds information about what properties of the {@link PlayerInfo} have been excluded when sent + * to the controller. + */ + public static class BundlingExclusions implements Bundleable { + + /** Whether the {@linkplain PlayerInfo#timeline timeline} is excluded. */ + public final boolean isTimelineExcluded; + /** Whether the {@linkplain PlayerInfo#currentTracks current tracks} are excluded. */ + public final boolean areCurrentTracksExcluded; + + /** Creates a new instance. */ + public BundlingExclusions(boolean isTimelineExcluded, boolean areCurrentTracksExcluded) { + this.isTimelineExcluded = isTimelineExcluded; + this.areCurrentTracksExcluded = areCurrentTracksExcluded; + } + + // Bundleable implementation. + + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({FIELD_IS_TIMELINE_EXCLUDED, FIELD_ARE_CURRENT_TRACKS_EXCLUDED}) + private @interface FieldNumber {} + + private static final int FIELD_IS_TIMELINE_EXCLUDED = 0; + private static final int FIELD_ARE_CURRENT_TRACKS_EXCLUDED = 1; + // Next field key = 2 + + @UnstableApi + @Override + public Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putBoolean(keyForField(FIELD_IS_TIMELINE_EXCLUDED), isTimelineExcluded); + bundle.putBoolean(keyForField(FIELD_ARE_CURRENT_TRACKS_EXCLUDED), areCurrentTracksExcluded); + return bundle; + } + + public static final Creator CREATOR = + bundle -> + new BundlingExclusions( + bundle.getBoolean( + keyForField(FIELD_IS_TIMELINE_EXCLUDED), /* defaultValue= */ false), + bundle.getBoolean( + keyForField(FIELD_ARE_CURRENT_TRACKS_EXCLUDED), /* defaultValue= */ false)); + + private static String keyForField(@BundlingExclusions.FieldNumber int field) { + return Integer.toString(field, Character.MAX_RADIX); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BundlingExclusions)) { + return false; + } + BundlingExclusions that = (BundlingExclusions) o; + return isTimelineExcluded == that.isTimelineExcluded + && areCurrentTracksExcluded == that.areCurrentTracksExcluded; + } + + @Override + public int hashCode() { + return Objects.hashCode(isTimelineExcluded, areCurrentTracksExcluded); + } + } + public static class Builder { @Nullable private PlaybackException playerError; @@ -983,7 +1054,7 @@ private static PlayerInfo fromBundle(Bundle bundle) { trackSelectionParameters); } - private static String keyForField(@FieldNumber int field) { + private static String keyForField(@PlayerInfo.FieldNumber int field) { return Integer.toString(field, Character.MAX_RADIX); } } diff --git a/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java b/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java new file mode 100644 index 00000000000..7e8738f9d90 --- /dev/null +++ b/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Bundle; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link PlayerInfo}. */ +@RunWith(AndroidJUnit4.class) +public class PlayerInfoTest { + + @Test + public void bundlingExclusionEquals_equalInstances() { + PlayerInfo.BundlingExclusions bundlingExclusions1 = + new PlayerInfo.BundlingExclusions( + /* isTimelineExcluded= */ true, /* areCurrentTracksExcluded= */ false); + PlayerInfo.BundlingExclusions bundlingExclusions2 = + new PlayerInfo.BundlingExclusions( + /* isTimelineExcluded= */ true, /* areCurrentTracksExcluded= */ false); + + assertThat(bundlingExclusions1).isEqualTo(bundlingExclusions2); + } + + @Test + public void bundlingExclusionFromBundle_toBundleRoundTrip_equalInstances() { + PlayerInfo.BundlingExclusions bundlingExclusions = + new PlayerInfo.BundlingExclusions( + /* isTimelineExcluded= */ true, /* areCurrentTracksExcluded= */ true); + Bundle bundle = bundlingExclusions.toBundle(); + + PlayerInfo.BundlingExclusions resultingBundlingExclusions = + PlayerInfo.BundlingExclusions.CREATOR.fromBundle(bundle); + + assertThat(resultingBundlingExclusions).isEqualTo(bundlingExclusions); + } +} From c11b5cf91c7f3f3e42aa6f3d62b51d6a812be799 Mon Sep 17 00:00:00 2001 From: Googler Date: Wed, 16 Nov 2022 18:07:00 +0000 Subject: [PATCH 004/141] Fix NPE when listener is not set PiperOrigin-RevId: 488970696 (cherry picked from commit f3ed9e359dfdff2a99bf8766ffceb59a93d1bc93) --- .../androidx/media3/exoplayer/audio/DefaultAudioSink.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index bd77a50cac2..9ca07c700a7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -1000,9 +1000,11 @@ public boolean handleBuffer( getSubmittedFrames() - trimmingAudioProcessor.getTrimmedFrameCount()); if (!startMediaTimeUsNeedsSync && Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) { - listener.onAudioSinkError( - new AudioSink.UnexpectedDiscontinuityException( - presentationTimeUs, expectedPresentationTimeUs)); + if (listener != null) { + listener.onAudioSinkError( + new AudioSink.UnexpectedDiscontinuityException( + presentationTimeUs, expectedPresentationTimeUs)); + } startMediaTimeUsNeedsSync = true; } if (startMediaTimeUsNeedsSync) { From 9ba059f73f8d6a7d70c3670fca8df4f5bc763959 Mon Sep 17 00:00:00 2001 From: Googler Date: Wed, 16 Nov 2022 18:42:27 +0000 Subject: [PATCH 005/141] Add setPlaybackLooper ExoPlayer builder method The method allows clients to specify a pre-existing thread to use for playback. This can be used to run multiple ExoPlayer instances on the same playback thread. PiperOrigin-RevId: 488980749 (cherry picked from commit e1fe3120e29a66ac2dcde6e9960756197bac6444) --- RELEASENOTES.md | 2 ++ .../androidx/media3/exoplayer/ExoPlayer.java | 21 ++++++++++++ .../media3/exoplayer/ExoPlayerImpl.java | 3 +- .../exoplayer/ExoPlayerImplInternal.java | 33 ++++++++++++------- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 33f9a6146d3..50b763ca1ae 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -34,6 +34,8 @@ This release corresponds to the * Fix bug where removing listeners during the player release can cause an `IllegalStateException` ([#10758](https://github.com/google/ExoPlayer/issues/10758)). + * Add `ExoPlayer.Builder.setPlaybackLooper` that sets a pre-existing + playback thread for a new ExoPlayer instance. * Build: * Enforce minimum `compileSdkVersion` to avoid compilation errors ([#10684](https://github.com/google/ExoPlayer/issues/10684)). diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index 84769d802f9..eae251688e7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -24,6 +24,7 @@ import android.media.AudioTrack; import android.media.MediaCodec; import android.os.Looper; +import android.os.Process; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -485,6 +486,7 @@ final class Builder { /* package */ long detachSurfaceTimeoutMs; /* package */ boolean pauseAtEndOfMediaItems; /* package */ boolean usePlatformDiagnostics; + @Nullable /* package */ Looper playbackLooper; /* package */ boolean buildCalled; /** @@ -527,6 +529,7 @@ final class Builder { *
  • {@code pauseAtEndOfMediaItems}: {@code false} *
  • {@code usePlatformDiagnostics}: {@code true} *
  • {@link Clock}: {@link Clock#DEFAULT} + *
  • {@code playbackLooper}: {@code null} (create new thread) * * * @param context A {@link Context}. @@ -1134,6 +1137,24 @@ public Builder setClock(Clock clock) { return this; } + /** + * Sets the {@link Looper} that will be used for playback. + * + *

    The backing thread should run with priority {@link Process#THREAD_PRIORITY_AUDIO} and + * should handle messages within 10ms. + * + * @param playbackLooper A {@link looper}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + @CanIgnoreReturnValue + @UnstableApi + public Builder setPlaybackLooper(Looper playbackLooper) { + checkState(!buildCalled); + this.playbackLooper = playbackLooper; + return this; + } + /** * Builds an {@link ExoPlayer} instance. * diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 1d087d23b3c..d896bc76544 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -345,7 +345,8 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer) applicationLooper, clock, playbackInfoUpdateListener, - playerId); + playerId, + builder.playbackLooper); volume = 1; repeatMode = Player.REPEAT_MODE_OFF; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index 312a9c67ae8..d92a5e8b877 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -189,7 +189,7 @@ public interface PlaybackInfoUpdateListener { private final LoadControl loadControl; private final BandwidthMeter bandwidthMeter; private final HandlerWrapper handler; - private final HandlerThread internalPlaybackThread; + @Nullable private final HandlerThread internalPlaybackThread; private final Looper playbackLooper; private final Timeline.Window window; private final Timeline.Period period; @@ -244,7 +244,8 @@ public ExoPlayerImplInternal( Looper applicationLooper, Clock clock, PlaybackInfoUpdateListener playbackInfoUpdateListener, - PlayerId playerId) { + PlayerId playerId, + Looper playbackLooper) { this.playbackInfoUpdateListener = playbackInfoUpdateListener; this.renderers = renderers; this.trackSelector = trackSelector; @@ -285,12 +286,18 @@ public ExoPlayerImplInternal( mediaSourceList = new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId); - // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can - // not normally change to this priority" is incorrect. - internalPlaybackThread = new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO); - internalPlaybackThread.start(); - playbackLooper = internalPlaybackThread.getLooper(); - handler = clock.createHandler(playbackLooper, this); + if (playbackLooper != null) { + internalPlaybackThread = null; + this.playbackLooper = playbackLooper; + } else { + // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can + // not normally change to this priority" is incorrect. + internalPlaybackThread = + new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO); + internalPlaybackThread.start(); + this.playbackLooper = internalPlaybackThread.getLooper(); + } + handler = clock.createHandler(this.playbackLooper, this); } public void experimentalSetForegroundModeTimeoutMs(long setForegroundModeTimeoutMs) { @@ -393,7 +400,7 @@ public void setShuffleOrder(ShuffleOrder shuffleOrder) { @Override public synchronized void sendMessage(PlayerMessage message) { - if (released || !internalPlaybackThread.isAlive()) { + if (released || !playbackLooper.getThread().isAlive()) { Log.w(TAG, "Ignoring messages sent after release."); message.markAsProcessed(/* isDelivered= */ false); return; @@ -408,7 +415,7 @@ public synchronized void sendMessage(PlayerMessage message) { * @return Whether the operations succeeded. If false, the operation timed out. */ public synchronized boolean setForegroundMode(boolean foregroundMode) { - if (released || !internalPlaybackThread.isAlive()) { + if (released || !playbackLooper.getThread().isAlive()) { return true; } if (foregroundMode) { @@ -430,7 +437,7 @@ public synchronized boolean setForegroundMode(boolean foregroundMode) { * @return Whether the release succeeded. If false, the release timed out. */ public synchronized boolean release() { - if (released || !internalPlaybackThread.isAlive()) { + if (released || !playbackLooper.getThread().isAlive()) { return true; } handler.sendEmptyMessage(MSG_RELEASE); @@ -1382,7 +1389,9 @@ private void releaseInternal() { /* resetError= */ false); loadControl.onReleased(); setState(Player.STATE_IDLE); - internalPlaybackThread.quit(); + if (internalPlaybackThread != null) { + internalPlaybackThread.quit(); + } synchronized (this) { released = true; notifyAll(); From f3268ac8ae79030b9cd0430e7b3d087ecccc49d3 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Thu, 17 Nov 2022 15:23:35 +0000 Subject: [PATCH 006/141] Load bitmaps for `MediaBrowserCompat`. * Transforms the `ListenableFuture>` and `ListenableFuture>>` to `ListenableFuture` and `ListenableFuture>`, and the result will be sent out when `ListenableFuture` the `MediaBrowserCompat.MediaItem` (or the list of it) is fulfilled. * Add `artworkData` to the tests in `MediaBrowserCompatWithMediaLibraryServiceTest`. PiperOrigin-RevId: 489205547 (cherry picked from commit 4ce171a3cfef7ce1f533fdc0b7366d7b18ef44d1) --- .../MediaLibraryServiceLegacyStub.java | 177 +++++++++++++++--- .../androidx/media3/session/MediaUtils.java | 41 ++-- ...wserCompatWithMediaLibraryServiceTest.java | 5 + .../media3/session/MediaUtilsTest.java | 30 --- .../session/MockMediaLibraryService.java | 48 ++++- 5 files changed, 225 insertions(+), 76 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java index eeb4f3674aa..60de48cae18 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java @@ -26,6 +26,7 @@ import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES; import android.annotation.SuppressLint; +import android.graphics.Bitmap; import android.os.BadParcelableException; import android.os.Bundle; import android.os.RemoteException; @@ -37,6 +38,7 @@ import androidx.media.MediaBrowserServiceCompat; import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; @@ -44,14 +46,19 @@ import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Implementation of {@link MediaBrowserServiceCompat} for interoperability between {@link @@ -218,7 +225,11 @@ public void onLoadChildren( ListenableFuture>> future = librarySessionImpl.onGetChildrenOnHandler( controller, parentId, page, pageSize, params); - sendLibraryResultWithMediaItemsWhenReady(result, future); + ListenableFuture<@NullableType List> + browserItemsFuture = + Util.transformFutureAsync( + future, createMediaItemsToBrowserItemsAsyncFunction()); + sendLibraryResultWithMediaItemsWhenReady(result, browserItemsFuture); return; } // Cannot distinguish onLoadChildren() why it's called either by @@ -236,7 +247,9 @@ public void onLoadChildren( /* page= */ 0, /* pageSize= */ Integer.MAX_VALUE, /* params= */ null); - sendLibraryResultWithMediaItemsWhenReady(result, future); + ListenableFuture<@NullableType List> browserItemsFuture = + Util.transformFutureAsync(future, createMediaItemsToBrowserItemsAsyncFunction()); + sendLibraryResultWithMediaItemsWhenReady(result, browserItemsFuture); }); } @@ -264,7 +277,9 @@ public void onLoadItem(String itemId, Result resul } ListenableFuture> future = librarySessionImpl.onGetItemOnHandler(controller, itemId); - sendLibraryResultWithMediaItemWhenReady(result, future); + ListenableFuture browserItemFuture = + Util.transformFutureAsync(future, createMediaItemToBrowserItemAsyncFunction()); + sendLibraryResultWithMediaItemWhenReady(result, browserItemFuture); }); } @@ -362,17 +377,12 @@ private static void sendCustomActionResultWhenReady( private static void sendLibraryResultWithMediaItemWhenReady( Result result, - ListenableFuture> future) { + ListenableFuture future) { future.addListener( () -> { try { - LibraryResult libraryResult = - checkNotNull(future.get(), "LibraryResult must not be null"); - if (libraryResult.resultCode != RESULT_SUCCESS || libraryResult.value == null) { - result.sendResult(/* result= */ null); - } else { - result.sendResult(MediaUtils.convertToBrowserItem(libraryResult.value)); - } + MediaBrowserCompat.MediaItem mediaItem = future.get(); + result.sendResult(mediaItem); } catch (CancellationException | ExecutionException | InterruptedException unused) { result.sendError(/* extras= */ null); } @@ -382,20 +392,15 @@ private static void sendLibraryResultWithMediaItemWhenReady( private static void sendLibraryResultWithMediaItemsWhenReady( Result> result, - ListenableFuture>> future) { + ListenableFuture<@NullableType List> future) { future.addListener( () -> { try { - LibraryResult> libraryResult = - checkNotNull(future.get(), "LibraryResult must not be null"); - if (libraryResult.resultCode != RESULT_SUCCESS || libraryResult.value == null) { - result.sendResult(/* result= */ null); - } else { - result.sendResult( - MediaUtils.truncateListBySize( - MediaUtils.convertToBrowserItemList(libraryResult.value), - TRANSACTION_SIZE_LIMIT_IN_BYTES)); - } + List mediaItems = future.get(); + result.sendResult( + (mediaItems == null) + ? null + : MediaUtils.truncateListBySize(mediaItems, TRANSACTION_SIZE_LIMIT_IN_BYTES)); } catch (CancellationException | ExecutionException | InterruptedException unused) { result.sendError(/* extras= */ null); } @@ -403,6 +408,130 @@ private static void sendLibraryResultWithMediaItemsWhenReady( MoreExecutors.directExecutor()); } + private AsyncFunction< + LibraryResult>, @NullableType List> + createMediaItemsToBrowserItemsAsyncFunction() { + return result -> { + checkNotNull(result, "LibraryResult must not be null"); + SettableFuture<@NullableType List> outputFuture = + SettableFuture.create(); + if (result.resultCode != RESULT_SUCCESS || result.value == null) { + outputFuture.set(null); + return outputFuture; + } + + ImmutableList mediaItems = result.value; + if (mediaItems.isEmpty()) { + outputFuture.set(new ArrayList<>()); + return outputFuture; + } + + List<@NullableType ListenableFuture> bitmapFutures = new ArrayList<>(); + outputFuture.addListener( + () -> { + if (outputFuture.isCancelled()) { + cancelAllFutures(bitmapFutures); + } + }, + MoreExecutors.directExecutor()); + + final AtomicInteger resultCount = new AtomicInteger(0); + Runnable handleBitmapFuturesTask = + () -> { + int completedBitmapFutureCount = resultCount.incrementAndGet(); + if (completedBitmapFutureCount == mediaItems.size()) { + handleBitmapFuturesAllCompletedAndSetOutputFuture( + bitmapFutures, mediaItems, outputFuture); + } + }; + + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem mediaItem = mediaItems.get(i); + MediaMetadata metadata = mediaItem.mediaMetadata; + if (metadata.artworkData == null) { + bitmapFutures.add(null); + handleBitmapFuturesTask.run(); + } else { + ListenableFuture bitmapFuture = + librarySessionImpl.getBitmapLoader().decodeBitmap(metadata.artworkData); + bitmapFutures.add(bitmapFuture); + bitmapFuture.addListener(handleBitmapFuturesTask, MoreExecutors.directExecutor()); + } + } + return outputFuture; + }; + } + + private void handleBitmapFuturesAllCompletedAndSetOutputFuture( + List<@NullableType ListenableFuture> bitmapFutures, + List mediaItems, + SettableFuture<@NullableType List> outputFuture) { + List outputMediaItems = new ArrayList<>(); + for (int i = 0; i < bitmapFutures.size(); i++) { + @Nullable ListenableFuture future = bitmapFutures.get(i); + @Nullable Bitmap bitmap = null; + if (future != null) { + try { + bitmap = Futures.getDone(future); + } catch (CancellationException | ExecutionException e) { + Log.d(TAG, "Failed to get bitmap"); + } + } + outputMediaItems.add(MediaUtils.convertToBrowserItem(mediaItems.get(i), bitmap)); + } + outputFuture.set(outputMediaItems); + } + + private static void cancelAllFutures(List<@NullableType ListenableFuture> futures) { + for (int i = 0; i < futures.size(); i++) { + if (futures.get(i) != null) { + futures.get(i).cancel(/* mayInterruptIfRunning= */ false); + } + } + } + + private AsyncFunction, MediaBrowserCompat.@NullableType MediaItem> + createMediaItemToBrowserItemAsyncFunction() { + return result -> { + checkNotNull(result, "LibraryResult must not be null"); + SettableFuture outputFuture = + SettableFuture.create(); + if (result.resultCode != RESULT_SUCCESS || result.value == null) { + outputFuture.set(null); + return outputFuture; + } + + MediaItem mediaItem = result.value; + MediaMetadata metadata = mediaItem.mediaMetadata; + if (metadata.artworkData == null) { + outputFuture.set(MediaUtils.convertToBrowserItem(mediaItem, /* artworkBitmap= */ null)); + return outputFuture; + } + + ListenableFuture bitmapFuture = + librarySessionImpl.getBitmapLoader().decodeBitmap(metadata.artworkData); + outputFuture.addListener( + () -> { + if (outputFuture.isCancelled()) { + bitmapFuture.cancel(/* mayInterruptIfRunning= */ false); + } + }, + MoreExecutors.directExecutor()); + bitmapFuture.addListener( + () -> { + @Nullable Bitmap bitmap = null; + try { + bitmap = Futures.getDone(bitmapFuture); + } catch (CancellationException | ExecutionException e) { + Log.d(TAG, "failed to get bitmap"); + } + outputFuture.set(MediaUtils.convertToBrowserItem(mediaItem, bitmap)); + }, + MoreExecutors.directExecutor()); + return outputFuture; + }; + } + private static void ignoreFuture(Future unused) { // no-op } @@ -504,7 +633,9 @@ public void onSearchResultChanged( ListenableFuture>> future = librarySessionImpl.onGetSearchResultOnHandler( request.controller, request.query, page, pageSize, libraryParams); - sendLibraryResultWithMediaItemsWhenReady(request.result, future); + ListenableFuture<@NullableType List> mediaItemsFuture = + Util.transformFutureAsync(future, createMediaItemsToBrowserItemsAsyncFunction()); + sendLibraryResultWithMediaItemsWhenReady(request.result, mediaItemsFuture); } }); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index ae17cc834f2..3f89c5dd737 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -136,9 +136,9 @@ public static PlaybackException convertToPlaybackException( errorMessage, /* cause= */ null, PlaybackException.ERROR_CODE_REMOTE_ERROR); } - /** Converts a {@link MediaItem} to a {@link MediaBrowserCompat.MediaItem}. */ - public static MediaBrowserCompat.MediaItem convertToBrowserItem(MediaItem item) { - MediaDescriptionCompat description = convertToMediaDescriptionCompat(item); + public static MediaBrowserCompat.MediaItem convertToBrowserItem( + MediaItem item, @Nullable Bitmap artworkBitmap) { + MediaDescriptionCompat description = convertToMediaDescriptionCompat(item, artworkBitmap); MediaMetadata metadata = item.mediaMetadata; int flags = 0; if (metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE) { @@ -150,15 +150,6 @@ public static MediaBrowserCompat.MediaItem convertToBrowserItem(MediaItem item) return new MediaBrowserCompat.MediaItem(description, flags); } - /** Converts a list of {@link MediaItem} to a list of {@link MediaBrowserCompat.MediaItem}. */ - public static List convertToBrowserItemList(List items) { - List result = new ArrayList<>(); - for (int i = 0; i < items.size(); i++) { - result.add(convertToBrowserItem(items.get(i))); - } - return result; - } - /** Converts a {@link MediaBrowserCompat.MediaItem} to a {@link MediaItem}. */ public static MediaItem convertToMediaItem(MediaBrowserCompat.MediaItem item) { return convertToMediaItem(item.getDescription(), item.isBrowsable(), item.isPlayable()); @@ -320,16 +311,32 @@ public static List truncateListBySize( return result; } - /** Converts a {@link MediaItem} to a {@link MediaDescriptionCompat}. */ + /** + * Converts a {@link MediaItem} to a {@link MediaDescriptionCompat}. + * + * @deprecated Use {@link #convertToMediaDescriptionCompat(MediaItem, Bitmap)} instead. + */ + @Deprecated public static MediaDescriptionCompat convertToMediaDescriptionCompat(MediaItem item) { + MediaMetadata metadata = item.mediaMetadata; + @Nullable Bitmap artworkBitmap = null; + if (metadata.artworkData != null) { + artworkBitmap = + BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData.length); + } + + return convertToMediaDescriptionCompat(item, artworkBitmap); + } + + /** Converts a {@link MediaItem} to a {@link MediaDescriptionCompat} */ + public static MediaDescriptionCompat convertToMediaDescriptionCompat( + MediaItem item, @Nullable Bitmap artworkBitmap) { MediaDescriptionCompat.Builder builder = new MediaDescriptionCompat.Builder() .setMediaId(item.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID) ? null : item.mediaId); MediaMetadata metadata = item.mediaMetadata; - if (metadata.artworkData != null) { - Bitmap artwork = - BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData.length); - builder.setIconBitmap(artwork); + if (artworkBitmap != null) { + builder.setIconBitmap(artworkBitmap); } @Nullable Bundle extras = metadata.extras; if (metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE) { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java index e9ec255abcd..4e36a19ece9 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java @@ -129,6 +129,7 @@ public void onItemLoaded(MediaItem item) { assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(itemRef.get().getMediaId()).isEqualTo(mediaId); assertThat(itemRef.get().isBrowsable()).isTrue(); + assertThat(itemRef.get().getDescription().getIconBitmap()).isNotNull(); } @Test @@ -151,6 +152,7 @@ public void onItemLoaded(MediaItem item) { assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(itemRef.get().getMediaId()).isEqualTo(mediaId); assertThat(itemRef.get().isPlayable()).isTrue(); + assertThat(itemRef.get().getDescription().getIconBitmap()).isNotNull(); } @Test @@ -181,6 +183,7 @@ public void onItemLoaded(MediaItem item) { BundleSubject.assertThat(description.getExtras()) .string(METADATA_EXTRA_KEY) .isEqualTo(METADATA_EXTRA_VALUE); + assertThat(description.getIconBitmap()).isNotNull(); } @Test @@ -245,6 +248,7 @@ public void onChildrenLoaded(String parentId, List children, Bundle o EXTRAS_KEY_COMPLETION_STATUS, /* defaultValue= */ EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + 1)) .isEqualTo(EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED); + assertThat(mediaItem.getDescription().getIconBitmap()).isNotNull(); } } @@ -311,6 +315,7 @@ public void onChildrenLoaded(String parentId, List children, Bundle o int relativeIndex = originalIndex - fromIndex; assertThat(children.get(relativeIndex).getMediaId()) .isEqualTo(GET_CHILDREN_RESULT.get(originalIndex)); + assertThat(children.get(relativeIndex).getDescription().getIconBitmap()).isNotNull(); } latch.countDown(); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java index 1d53a847941..d7a8fca105d 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java @@ -32,7 +32,6 @@ import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; -import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.media.AudioAttributesCompat; import androidx.media3.common.AudioAttributes; @@ -71,23 +70,6 @@ public void setUp() { bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader()); } - @Test - public void convertToBrowserItem() { - String mediaId = "testId"; - CharSequence trackTitle = "testTitle"; - MediaItem mediaItem = - new MediaItem.Builder() - .setMediaId(mediaId) - .setMediaMetadata(new MediaMetadata.Builder().setTitle(trackTitle).build()) - .build(); - - MediaBrowserCompat.MediaItem browserItem = MediaUtils.convertToBrowserItem(mediaItem); - - assertThat(browserItem.getDescription()).isNotNull(); - assertThat(browserItem.getDescription().getMediaId()).isEqualTo(mediaId); - assertThat(TextUtils.equals(browserItem.getDescription().getTitle(), trackTitle)).isTrue(); - } - @Test public void convertToMediaItem_browserItemToMediaItem() { String mediaId = "testId"; @@ -115,18 +97,6 @@ public void convertToMediaItem_queueItemToMediaItem() { assertThat(mediaItem.mediaMetadata.title.toString()).isEqualTo(title); } - @Test - public void convertToBrowserItemList() { - int size = 3; - List mediaItems = MediaTestUtils.createMediaItems(size); - List browserItems = - MediaUtils.convertToBrowserItemList(mediaItems); - assertThat(browserItems).hasSize(size); - for (int i = 0; i < size; ++i) { - assertThat(browserItems.get(i).getMediaId()).isEqualTo(mediaItems.get(i).mediaId); - } - } - @Test public void convertBrowserItemListToMediaItemList() { int size = 3; diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java index cfad2d7550a..e3023a00a28 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java @@ -51,6 +51,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE; import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.fail; import android.app.PendingIntent; import android.app.Service; @@ -72,6 +73,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; @@ -92,6 +94,8 @@ public class MockMediaLibraryService extends MediaLibraryService { public static final String CONNECTION_HINTS_KEY_REMOVE_COMMAND_CODE_LIBRARY_SEARCH = "CONNECTION_HINTS_KEY_REMOVE_SEARCH_SESSION_COMMAND"; + private static final String TEST_IMAGE_PATH = "media/png/non-motion-photo-shortened.png"; + public static final MediaItem ROOT_ITEM = new MediaItem.Builder() .setMediaId(ROOT_ID) @@ -115,6 +119,8 @@ public class MockMediaLibraryService extends MediaLibraryService { @Nullable private static LibraryParams expectedParams; + @Nullable private static byte[] testArtworkData; + MediaLibrarySession session; TestHandler handler; HandlerThread handlerThread; @@ -238,7 +244,8 @@ public ListenableFuture> onGetItem( LibraryResult.ofItem(createBrowsableMediaItem(mediaId), /* params= */ null)); case MEDIA_ID_GET_PLAYABLE_ITEM: return Futures.immediateFuture( - LibraryResult.ofItem(createPlayableMediaItem(mediaId), /* params= */ null)); + LibraryResult.ofItem( + createPlayableMediaItemWithArtworkData(mediaId), /* params= */ null)); case MEDIA_ID_GET_ITEM_WITH_METADATA: return Futures.immediateFuture( LibraryResult.ofItem(createMediaItemWithMetadata(mediaId), /* params= */ null)); @@ -445,20 +452,32 @@ private List getPaginatedResult(List items, int page, int pag // Create a list of MediaItem from the list of media IDs. List result = new ArrayList<>(); for (int i = 0; i < paginatedMediaIdList.size(); i++) { - result.add(createPlayableMediaItem(paginatedMediaIdList.get(i))); + result.add(createPlayableMediaItemWithArtworkData(paginatedMediaIdList.get(i))); } return result; } - private static MediaItem createBrowsableMediaItem(String mediaId) { + private MediaItem createBrowsableMediaItem(String mediaId) { MediaMetadata mediaMetadata = new MediaMetadata.Builder() .setFolderType(MediaMetadata.FOLDER_TYPE_MIXED) .setIsPlayable(false) + .setArtworkData(getArtworkData(), MediaMetadata.PICTURE_TYPE_FRONT_COVER) .build(); return new MediaItem.Builder().setMediaId(mediaId).setMediaMetadata(mediaMetadata).build(); } + private MediaItem createPlayableMediaItemWithArtworkData(String mediaId) { + MediaItem mediaItem = createPlayableMediaItem(mediaId); + MediaMetadata mediaMetadataWithArtwork = + mediaItem + .mediaMetadata + .buildUpon() + .setArtworkData(getArtworkData(), MediaMetadata.PICTURE_TYPE_FRONT_COVER) + .build(); + return mediaItem.buildUpon().setMediaMetadata(mediaMetadataWithArtwork).build(); + } + private static MediaItem createPlayableMediaItem(String mediaId) { Bundle extras = new Bundle(); extras.putInt(EXTRAS_KEY_COMPLETION_STATUS, EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED); @@ -471,15 +490,32 @@ private static MediaItem createPlayableMediaItem(String mediaId) { return new MediaItem.Builder().setMediaId(mediaId).setMediaMetadata(mediaMetadata).build(); } - private static MediaItem createMediaItemWithMetadata(String mediaId) { - MediaMetadata mediaMetadata = MediaTestUtils.createMediaMetadata(); + private MediaItem createMediaItemWithMetadata(String mediaId) { + MediaMetadata mediaMetadataWithArtwork = + MediaTestUtils.createMediaMetadata() + .buildUpon() + .setArtworkData(getArtworkData(), MediaMetadata.PICTURE_TYPE_FRONT_COVER) + .build(); return new MediaItem.Builder() .setMediaId(mediaId) .setRequestMetadata( new MediaItem.RequestMetadata.Builder() .setMediaUri(CommonConstants.METADATA_MEDIA_URI) .build()) - .setMediaMetadata(mediaMetadata) + .setMediaMetadata(mediaMetadataWithArtwork) .build(); } + + private byte[] getArtworkData() { + if (testArtworkData != null) { + return testArtworkData; + } + try { + testArtworkData = + TestUtils.getByteArrayForScaledBitmap(getApplicationContext(), TEST_IMAGE_PATH); + } catch (IOException e) { + fail(e.getMessage()); + } + return testArtworkData; + } } From 91c51fe94d7846b35d46278860a64a52b0d52cf4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 17 Nov 2022 15:53:26 +0000 Subject: [PATCH 007/141] Mark broadcast receivers as not exported They are called from the system only and don't need to be exported to be visible to other apps. PiperOrigin-RevId: 489210264 (cherry picked from commit 22ccc1a1286803868970fb2b1eafe63e9c669a5c) --- .../main/java/androidx/media3/session/MediaSessionImpl.java | 3 +-- .../java/androidx/media3/ui/PlayerNotificationManager.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 4cbbb832226..705115745fd 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -216,8 +216,7 @@ public MediaSessionImpl( broadcastReceiver = new MediaButtonReceiver(); IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON); filter.addDataScheme(castNonNull(sessionUri.getScheme())); - // TODO(b/197817693): Explicitly indicate whether the receiver should be exported. - context.registerReceiver(broadcastReceiver, filter); + Util.registerReceiverNotExported(context, broadcastReceiver, filter); } else { // Has MediaSessionService to revive playback after it's dead. Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, sessionUri); diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java index 1a09119032d..30e610ed14f 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java @@ -1164,8 +1164,7 @@ private void startOrUpdateNotification(Player player, @Nullable Bitmap bitmap) { Notification notification = builder.build(); notificationManager.notify(notificationId, notification); if (!isNotificationStarted) { - // TODO(b/197817693): Explicitly indicate whether the receiver should be exported. - context.registerReceiver(notificationBroadcastReceiver, intentFilter); + Util.registerReceiverNotExported(context, notificationBroadcastReceiver, intentFilter); } if (notificationListener != null) { // Always pass true for ongoing with the first notification to tell a service to go into From 9ac5062041e5f3c7419fc7ae89525bcbed7ca9e9 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 17 Nov 2022 17:41:02 +0000 Subject: [PATCH 008/141] Throw exception if a released player is passed to TestPlayerRunHelper I considered moving this enforcement inside the ExoPlayerImpl implementation, but it might lead to app crashes in cases that apps (incorrectly) call a released player, but it wasn't actually causing a problem. PiperOrigin-RevId: 489233917 (cherry picked from commit cba65c8c61122c5f0a41bd95a767002e11a1bae4) --- .../robolectric/TestPlayerRunHelper.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java index 658e9f56bec..54d62208ef8 100644 --- a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java +++ b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java @@ -17,6 +17,7 @@ package androidx.media3.test.utils.robolectric; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil; import android.os.Looper; @@ -55,6 +56,9 @@ private TestPlayerRunHelper() {} public static void runUntilPlaybackState(Player player, @Player.State int expectedState) throws TimeoutException { verifyMainTestThread(player); + if (player instanceof ExoPlayer) { + verifyPlaybackThreadIsAlive((ExoPlayer) player); + } runMainLooperUntil( () -> player.getPlaybackState() == expectedState || player.getPlayerError() != null); if (player.getPlayerError() != null) { @@ -76,6 +80,9 @@ public static void runUntilPlaybackState(Player player, @Player.State int expect public static void runUntilPlayWhenReady(Player player, boolean expectedPlayWhenReady) throws TimeoutException { verifyMainTestThread(player); + if (player instanceof ExoPlayer) { + verifyPlaybackThreadIsAlive((ExoPlayer) player); + } runMainLooperUntil( () -> player.getPlayWhenReady() == expectedPlayWhenReady || player.getPlayerError() != null); @@ -98,6 +105,9 @@ public static void runUntilPlayWhenReady(Player player, boolean expectedPlayWhen public static void runUntilTimelineChanged(Player player, Timeline expectedTimeline) throws TimeoutException { verifyMainTestThread(player); + if (player instanceof ExoPlayer) { + verifyPlaybackThreadIsAlive((ExoPlayer) player); + } runMainLooperUntil( () -> expectedTimeline.equals(player.getCurrentTimeline()) @@ -151,6 +161,9 @@ public void onTimelineChanged(Timeline timeline, int reason) { public static void runUntilPositionDiscontinuity( Player player, @Player.DiscontinuityReason int expectedReason) throws TimeoutException { verifyMainTestThread(player); + if (player instanceof ExoPlayer) { + verifyPlaybackThreadIsAlive((ExoPlayer) player); + } AtomicBoolean receivedCallback = new AtomicBoolean(false); Player.Listener listener = new Player.Listener() { @@ -180,6 +193,8 @@ public void onPositionDiscontinuity( */ public static ExoPlaybackException runUntilError(ExoPlayer player) throws TimeoutException { verifyMainTestThread(player); + verifyPlaybackThreadIsAlive(player); + runMainLooperUntil(() -> player.getPlayerError() != null); return checkNotNull(player.getPlayerError()); } @@ -199,6 +214,8 @@ public static ExoPlaybackException runUntilError(ExoPlayer player) throws Timeou public static void runUntilSleepingForOffload(ExoPlayer player, boolean expectedSleepForOffload) throws TimeoutException { verifyMainTestThread(player); + verifyPlaybackThreadIsAlive(player); + AtomicBoolean receiverCallback = new AtomicBoolean(false); ExoPlayer.AudioOffloadListener listener = new ExoPlayer.AudioOffloadListener() { @@ -228,6 +245,8 @@ public void onExperimentalSleepingForOffloadChanged(boolean sleepingForOffload) */ public static void runUntilRenderedFirstFrame(ExoPlayer player) throws TimeoutException { verifyMainTestThread(player); + verifyPlaybackThreadIsAlive(player); + AtomicBoolean receivedCallback = new AtomicBoolean(false); Player.Listener listener = new Player.Listener() { @@ -259,6 +278,7 @@ public void onRenderedFirstFrame() { public static void playUntilPosition(ExoPlayer player, int mediaItemIndex, long positionMs) throws TimeoutException { verifyMainTestThread(player); + verifyPlaybackThreadIsAlive(player); Looper applicationLooper = Util.getCurrentOrMainLooper(); AtomicBoolean messageHandled = new AtomicBoolean(false); player @@ -319,6 +339,8 @@ public static void playUntilStartOfMediaItem(ExoPlayer player, int mediaItemInde public static void runUntilPendingCommandsAreFullyHandled(ExoPlayer player) throws TimeoutException { verifyMainTestThread(player); + verifyPlaybackThreadIsAlive(player); + // Send message to player that will arrive after all other pending commands. Thus, the message // execution on the app thread will also happen after all other pending command // acknowledgements have arrived back on the app thread. @@ -336,4 +358,10 @@ private static void verifyMainTestThread(Player player) { throw new IllegalStateException(); } } + + private static void verifyPlaybackThreadIsAlive(ExoPlayer player) { + checkState( + player.getPlaybackLooper().getThread().isAlive(), + "Playback thread is not alive, has the player been released?"); + } } From 68a1571c1ecec3da11388a1d172df68c477a74ab Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 17 Nov 2022 17:52:17 +0000 Subject: [PATCH 009/141] Add additional codecs to the eosPropagationWorkaround list. Issue: google/ExoPlayer#10756 PiperOrigin-RevId: 489236336 (cherry picked from commit d1b470e4cc26a15525b583d1953529c8ec73a950) --- .../media3/exoplayer/mediacodec/MediaCodecRenderer.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java index 4a4c43e6d4a..815b4c369ea 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java @@ -2433,7 +2433,11 @@ private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecIn || (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name)) || (Util.SDK_INT <= 29 && ("OMX.broadcom.video_decoder.tunnel".equals(name) - || "OMX.broadcom.video_decoder.tunnel.secure".equals(name))) + || "OMX.broadcom.video_decoder.tunnel.secure".equals(name) + || "OMX.bcm.vdec.avc.tunnel".equals(name) + || "OMX.bcm.vdec.avc.tunnel.secure".equals(name) + || "OMX.bcm.vdec.hevc.tunnel".equals(name) + || "OMX.bcm.vdec.hevc.tunnel.secure".equals(name))) || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure); } From 0e628fb48768cd99646f5c203856c37e131271c1 Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 17 Nov 2022 18:00:55 +0000 Subject: [PATCH 010/141] Pass correct frame size for passthrough playback When estimating the AudioTrack min buffer size, we must use a PCM frame of 1 when doing direct playback (passthrough). The code was passing -1 (C.LENGTH_UNSET). PiperOrigin-RevId: 489238392 (cherry picked from commit 07d25bf41d9fa4d81daade6787a9b15682e9cf1f) --- .../java/androidx/media3/exoplayer/audio/DefaultAudioSink.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 9ca07c700a7..605f5f0d443 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -788,7 +788,7 @@ public void configure(Format inputFormat, int specifiedBufferSize, @Nullable int getAudioTrackMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding), outputEncoding, outputMode, - outputPcmFrameSize, + outputPcmFrameSize != C.LENGTH_UNSET ? outputPcmFrameSize : 1, outputSampleRate, enableAudioTrackPlaybackParams ? MAX_PLAYBACK_SPEED : DEFAULT_PLAYBACK_SPEED); From 9e42426645f1638b5bd673c66fa30de0765c0eb1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 18 Nov 2022 18:10:57 +0000 Subject: [PATCH 011/141] Add remaining state and getters to SimpleBasePlayer This adds the full Builders and State representation needed to implement all Player getter methods and listener invocations. PiperOrigin-RevId: 489503319 (cherry picked from commit 81918d8da7a4e80a08b65ade85ecb37c995934e7) --- .../media3/common/SimpleBasePlayer.java | 2364 ++++++++++++++++- .../media3/common/SimpleBasePlayerTest.java | 1576 ++++++++++- 2 files changed, 3829 insertions(+), 111 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index f3a073be7fc..350a23920c1 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -15,14 +15,21 @@ */ package androidx.media3.common; +import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull; +import static androidx.media3.common.util.Util.usToMs; +import static java.lang.Math.max; import android.os.Looper; +import android.os.SystemClock; +import android.util.Pair; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import androidx.annotation.FloatRange; +import androidx.annotation.IntRange; import androidx.annotation.Nullable; import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Clock; @@ -32,10 +39,12 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.ForOverride; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; @@ -90,18 +99,138 @@ public static final class Builder { private Commands availableCommands; private boolean playWhenReady; private @PlayWhenReadyChangeReason int playWhenReadyChangeReason; + private @Player.State int playbackState; + private @PlaybackSuppressionReason int playbackSuppressionReason; + @Nullable private PlaybackException playerError; + private @RepeatMode int repeatMode; + private boolean shuffleModeEnabled; + private boolean isLoading; + private long seekBackIncrementMs; + private long seekForwardIncrementMs; + private long maxSeekToPreviousPositionMs; + private PlaybackParameters playbackParameters; + private TrackSelectionParameters trackSelectionParameters; + private AudioAttributes audioAttributes; + private float volume; + private VideoSize videoSize; + private CueGroup currentCues; + private DeviceInfo deviceInfo; + private int deviceVolume; + private boolean isDeviceMuted; + private int audioSessionId; + private boolean skipSilenceEnabled; + private Size surfaceSize; + private boolean newlyRenderedFirstFrame; + private Metadata timedMetadata; + private ImmutableList playlistItems; + private Timeline timeline; + private MediaMetadata playlistMetadata; + private int currentMediaItemIndex; + private int currentPeriodIndex; + private int currentAdGroupIndex; + private int currentAdIndexInAdGroup; + private long contentPositionMs; + private PositionSupplier contentPositionMsSupplier; + private long adPositionMs; + private PositionSupplier adPositionMsSupplier; + private PositionSupplier contentBufferedPositionMsSupplier; + private PositionSupplier adBufferedPositionMsSupplier; + private PositionSupplier totalBufferedDurationMsSupplier; + private boolean hasPositionDiscontinuity; + private @Player.DiscontinuityReason int positionDiscontinuityReason; + private long discontinuityPositionMs; /** Creates the builder. */ public Builder() { availableCommands = Commands.EMPTY; playWhenReady = false; playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST; + playbackState = Player.STATE_IDLE; + playbackSuppressionReason = Player.PLAYBACK_SUPPRESSION_REASON_NONE; + playerError = null; + repeatMode = Player.REPEAT_MODE_OFF; + shuffleModeEnabled = false; + isLoading = false; + seekBackIncrementMs = C.DEFAULT_SEEK_BACK_INCREMENT_MS; + seekForwardIncrementMs = C.DEFAULT_SEEK_FORWARD_INCREMENT_MS; + maxSeekToPreviousPositionMs = C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS; + playbackParameters = PlaybackParameters.DEFAULT; + trackSelectionParameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT; + audioAttributes = AudioAttributes.DEFAULT; + volume = 1f; + videoSize = VideoSize.UNKNOWN; + currentCues = CueGroup.EMPTY_TIME_ZERO; + deviceInfo = DeviceInfo.UNKNOWN; + deviceVolume = 0; + isDeviceMuted = false; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + skipSilenceEnabled = false; + surfaceSize = Size.UNKNOWN; + newlyRenderedFirstFrame = false; + timedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET); + playlistItems = ImmutableList.of(); + timeline = Timeline.EMPTY; + playlistMetadata = MediaMetadata.EMPTY; + currentMediaItemIndex = 0; + currentPeriodIndex = C.INDEX_UNSET; + currentAdGroupIndex = C.INDEX_UNSET; + currentAdIndexInAdGroup = C.INDEX_UNSET; + contentPositionMs = C.TIME_UNSET; + contentPositionMsSupplier = PositionSupplier.ZERO; + adPositionMs = C.TIME_UNSET; + adPositionMsSupplier = PositionSupplier.ZERO; + contentBufferedPositionMsSupplier = PositionSupplier.ZERO; + adBufferedPositionMsSupplier = PositionSupplier.ZERO; + totalBufferedDurationMsSupplier = PositionSupplier.ZERO; + hasPositionDiscontinuity = false; + positionDiscontinuityReason = Player.DISCONTINUITY_REASON_INTERNAL; + discontinuityPositionMs = 0; } private Builder(State state) { this.availableCommands = state.availableCommands; this.playWhenReady = state.playWhenReady; this.playWhenReadyChangeReason = state.playWhenReadyChangeReason; + this.playbackState = state.playbackState; + this.playbackSuppressionReason = state.playbackSuppressionReason; + this.playerError = state.playerError; + this.repeatMode = state.repeatMode; + this.shuffleModeEnabled = state.shuffleModeEnabled; + this.isLoading = state.isLoading; + this.seekBackIncrementMs = state.seekBackIncrementMs; + this.seekForwardIncrementMs = state.seekForwardIncrementMs; + this.maxSeekToPreviousPositionMs = state.maxSeekToPreviousPositionMs; + this.playbackParameters = state.playbackParameters; + this.trackSelectionParameters = state.trackSelectionParameters; + this.audioAttributes = state.audioAttributes; + this.volume = state.volume; + this.videoSize = state.videoSize; + this.currentCues = state.currentCues; + this.deviceInfo = state.deviceInfo; + this.deviceVolume = state.deviceVolume; + this.isDeviceMuted = state.isDeviceMuted; + this.audioSessionId = state.audioSessionId; + this.skipSilenceEnabled = state.skipSilenceEnabled; + this.surfaceSize = state.surfaceSize; + this.newlyRenderedFirstFrame = state.newlyRenderedFirstFrame; + this.timedMetadata = state.timedMetadata; + this.playlistItems = state.playlistItems; + this.timeline = state.timeline; + this.playlistMetadata = state.playlistMetadata; + this.currentMediaItemIndex = state.currentMediaItemIndex; + this.currentPeriodIndex = state.currentPeriodIndex; + this.currentAdGroupIndex = state.currentAdGroupIndex; + this.currentAdIndexInAdGroup = state.currentAdIndexInAdGroup; + this.contentPositionMs = C.TIME_UNSET; + this.contentPositionMsSupplier = state.contentPositionMsSupplier; + this.adPositionMs = C.TIME_UNSET; + this.adPositionMsSupplier = state.adPositionMsSupplier; + this.contentBufferedPositionMsSupplier = state.contentBufferedPositionMsSupplier; + this.adBufferedPositionMsSupplier = state.adBufferedPositionMsSupplier; + this.totalBufferedDurationMsSupplier = state.totalBufferedDurationMsSupplier; + this.hasPositionDiscontinuity = state.hasPositionDiscontinuity; + this.positionDiscontinuityReason = state.positionDiscontinuityReason; + this.discontinuityPositionMs = state.discontinuityPositionMs; } /** @@ -132,26 +261,1646 @@ public Builder setPlayWhenReady( return this; } + /** + * Sets the {@linkplain Player.State state} of the player. + * + *

    If the {@linkplain #setPlaylist playlist} is empty, the state must be either {@link + * Player#STATE_IDLE} or {@link Player#STATE_ENDED}. + * + * @param playbackState The {@linkplain Player.State state} of the player. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPlaybackState(@Player.State int playbackState) { + this.playbackState = playbackState; + return this; + } + + /** + * Sets the reason why playback is suppressed even if {@link #getPlayWhenReady()} is true. + * + * @param playbackSuppressionReason The {@link Player.PlaybackSuppressionReason} why playback + * is suppressed even if {@link #getPlayWhenReady()} is true. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPlaybackSuppressionReason( + @Player.PlaybackSuppressionReason int playbackSuppressionReason) { + this.playbackSuppressionReason = playbackSuppressionReason; + return this; + } + + /** + * Sets last error that caused playback to fail, or null if there was no error. + * + *

    The {@linkplain #setPlaybackState playback state} must be set to {@link + * Player#STATE_IDLE} while an error is set. + * + * @param playerError The last error that caused playback to fail, or null if there was no + * error. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPlayerError(@Nullable PlaybackException playerError) { + this.playerError = playerError; + return this; + } + + /** + * Sets the {@link RepeatMode} used for playback. + * + * @param repeatMode The {@link RepeatMode} used for playback. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setRepeatMode(@Player.RepeatMode int repeatMode) { + this.repeatMode = repeatMode; + return this; + } + + /** + * Sets whether shuffling of media items is enabled. + * + * @param shuffleModeEnabled Whether shuffling of media items is enabled. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setShuffleModeEnabled(boolean shuffleModeEnabled) { + this.shuffleModeEnabled = shuffleModeEnabled; + return this; + } + + /** + * Sets whether the player is currently loading its source. + * + *

    The player can not be marked as loading if the {@linkplain #setPlaybackState state} is + * {@link Player#STATE_IDLE} or {@link Player#STATE_ENDED}. + * + * @param isLoading Whether the player is currently loading its source. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setIsLoading(boolean isLoading) { + this.isLoading = isLoading; + return this; + } + + /** + * Sets the {@link Player#seekBack()} increment in milliseconds. + * + * @param seekBackIncrementMs The {@link Player#seekBack()} increment in milliseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setSeekBackIncrementMs(long seekBackIncrementMs) { + this.seekBackIncrementMs = seekBackIncrementMs; + return this; + } + + /** + * Sets the {@link Player#seekForward()} increment in milliseconds. + * + * @param seekForwardIncrementMs The {@link Player#seekForward()} increment in milliseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setSeekForwardIncrementMs(long seekForwardIncrementMs) { + this.seekForwardIncrementMs = seekForwardIncrementMs; + return this; + } + + /** + * Sets the maximum position for which {@link #seekToPrevious()} seeks to the previous item, + * in milliseconds. + * + * @param maxSeekToPreviousPositionMs The maximum position for which {@link #seekToPrevious()} + * seeks to the previous item, in milliseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setMaxSeekToPreviousPositionMs(long maxSeekToPreviousPositionMs) { + this.maxSeekToPreviousPositionMs = maxSeekToPreviousPositionMs; + return this; + } + + /** + * Sets the currently active {@link PlaybackParameters}. + * + * @param playbackParameters The currently active {@link PlaybackParameters}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPlaybackParameters(PlaybackParameters playbackParameters) { + this.playbackParameters = playbackParameters; + return this; + } + + /** + * Sets the currently active {@link TrackSelectionParameters}. + * + * @param trackSelectionParameters The currently active {@link TrackSelectionParameters}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setTrackSelectionParameters( + TrackSelectionParameters trackSelectionParameters) { + this.trackSelectionParameters = trackSelectionParameters; + return this; + } + + /** + * Sets the current {@link AudioAttributes}. + * + * @param audioAttributes The current {@link AudioAttributes}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setAudioAttributes(AudioAttributes audioAttributes) { + this.audioAttributes = audioAttributes; + return this; + } + + /** + * Sets the current audio volume, with 0 being silence and 1 being unity gain (signal + * unchanged). + * + * @param volume The current audio volume, with 0 being silence and 1 being unity gain (signal + * unchanged). + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setVolume(@FloatRange(from = 0, to = 1.0) float volume) { + checkArgument(volume >= 0.0f && volume <= 1.0f); + this.volume = volume; + return this; + } + + /** + * Sets the current video size. + * + * @param videoSize The current video size. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setVideoSize(VideoSize videoSize) { + this.videoSize = videoSize; + return this; + } + + /** + * Sets the current {@linkplain CueGroup cues}. + * + * @param currentCues The current {@linkplain CueGroup cues}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setCurrentCues(CueGroup currentCues) { + this.currentCues = currentCues; + return this; + } + + /** + * Sets the {@link DeviceInfo}. + * + * @param deviceInfo The {@link DeviceInfo}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setDeviceInfo(DeviceInfo deviceInfo) { + this.deviceInfo = deviceInfo; + return this; + } + + /** + * Sets the current device volume. + * + * @param deviceVolume The current device volume. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setDeviceVolume(@IntRange(from = 0) int deviceVolume) { + checkArgument(deviceVolume >= 0); + this.deviceVolume = deviceVolume; + return this; + } + + /** + * Sets whether the device is muted. + * + * @param isDeviceMuted Whether the device is muted. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setIsDeviceMuted(boolean isDeviceMuted) { + this.isDeviceMuted = isDeviceMuted; + return this; + } + + /** + * Sets the audio session id. + * + * @param audioSessionId The audio session id. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setAudioSessionId(int audioSessionId) { + this.audioSessionId = audioSessionId; + return this; + } + + /** + * Sets whether skipping silences in the audio stream is enabled. + * + * @param skipSilenceEnabled Whether skipping silences in the audio stream is enabled. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setSkipSilenceEnabled(boolean skipSilenceEnabled) { + this.skipSilenceEnabled = skipSilenceEnabled; + return this; + } + + /** + * Sets the size of the surface onto which the video is being rendered. + * + * @param surfaceSize The surface size. Dimensions may be {@link C#LENGTH_UNSET} if unknown, + * or 0 if the video is not rendered onto a surface. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setSurfaceSize(Size surfaceSize) { + this.surfaceSize = surfaceSize; + return this; + } + + /** + * Sets whether a frame has been rendered for the first time since setting the surface, a + * rendering reset, or since the stream being rendered was changed. + * + *

    Note: As this will trigger a {@link Listener#onRenderedFirstFrame()} event, the flag + * should only be set for the first {@link State} update after the first frame was rendered. + * + * @param newlyRenderedFirstFrame Whether the first frame was newly rendered. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setNewlyRenderedFirstFrame(boolean newlyRenderedFirstFrame) { + this.newlyRenderedFirstFrame = newlyRenderedFirstFrame; + return this; + } + + /** + * Sets the most recent timed {@link Metadata}. + * + *

    Metadata with a {@link Metadata#presentationTimeUs} of {@link C#TIME_UNSET} will not be + * forwarded to listeners. + * + * @param timedMetadata The most recent timed {@link Metadata}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setTimedMetadata(Metadata timedMetadata) { + this.timedMetadata = timedMetadata; + return this; + } + + /** + * Sets the playlist items. + * + *

    All playlist items must have unique {@linkplain PlaylistItem.Builder#setUid UIDs}. + * + * @param playlistItems The list of playlist items. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPlaylist(List playlistItems) { + HashSet uids = new HashSet<>(); + for (int i = 0; i < playlistItems.size(); i++) { + checkArgument(uids.add(playlistItems.get(i).uid)); + } + this.playlistItems = ImmutableList.copyOf(playlistItems); + this.timeline = new PlaylistTimeline(this.playlistItems); + return this; + } + + /** + * Sets the playlist {@link MediaMetadata}. + * + * @param playlistMetadata The playlist {@link MediaMetadata}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPlaylistMetadata(MediaMetadata playlistMetadata) { + this.playlistMetadata = playlistMetadata; + return this; + } + + /** + * Sets the current media item index. + * + *

    The media item index must be less than the number of {@linkplain #setPlaylist playlist + * items}, if set. + * + * @param currentMediaItemIndex The current media item index. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setCurrentMediaItemIndex(int currentMediaItemIndex) { + this.currentMediaItemIndex = currentMediaItemIndex; + return this; + } + + /** + * Sets the current period index, or {@link C#INDEX_UNSET} to assume the first period of the + * current playlist item is played. + * + *

    The period index must be less than the total number of {@linkplain + * PlaylistItem.Builder#setPeriods periods} in the playlist, if set, and the period at the + * specified index must be part of the {@linkplain #setCurrentMediaItemIndex current playlist + * item}. + * + * @param currentPeriodIndex The current period index, or {@link C#INDEX_UNSET} to assume the + * first period of the current playlist item is played. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setCurrentPeriodIndex(int currentPeriodIndex) { + checkArgument(currentPeriodIndex == C.INDEX_UNSET || currentPeriodIndex >= 0); + this.currentPeriodIndex = currentPeriodIndex; + return this; + } + + /** + * Sets the current ad indices, or {@link C#INDEX_UNSET} if no ad is playing. + * + *

    Either both indices need to be {@link C#INDEX_UNSET} or both are not {@link + * C#INDEX_UNSET}. + * + *

    Ads indices can only be set if there is a corresponding {@link AdPlaybackState} defined + * in the current {@linkplain PlaylistItem.Builder#setPeriods period}. + * + * @param adGroupIndex The current ad group index, or {@link C#INDEX_UNSET} if no ad is + * playing. + * @param adIndexInAdGroup The current ad index in the ad group, or {@link C#INDEX_UNSET} if + * no ad is playing. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setCurrentAd(int adGroupIndex, int adIndexInAdGroup) { + checkArgument((adGroupIndex == C.INDEX_UNSET) == (adIndexInAdGroup == C.INDEX_UNSET)); + this.currentAdGroupIndex = adGroupIndex; + this.currentAdIndexInAdGroup = adIndexInAdGroup; + return this; + } + + /** + * Sets the current content playback position in milliseconds. + * + *

    This position will be converted to an advancing {@link PositionSupplier} if the overall + * state indicates an advancing playback position. + * + * @param positionMs The current content playback position in milliseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setContentPositionMs(long positionMs) { + this.contentPositionMs = positionMs; + return this; + } + + /** + * Sets the {@link PositionSupplier} for the current content playback position in + * milliseconds. + * + *

    The supplier is expected to return the updated position on every call if the playback is + * advancing, for example by using {@link PositionSupplier#getExtrapolating}. + * + * @param contentPositionMsSupplier The {@link PositionSupplier} for the current content + * playback position in milliseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setContentPositionMs(PositionSupplier contentPositionMsSupplier) { + this.contentPositionMs = C.TIME_UNSET; + this.contentPositionMsSupplier = contentPositionMsSupplier; + return this; + } + + /** + * Sets the current ad playback position in milliseconds. The * value is unused if no ad is + * playing. + * + *

    This position will be converted to an advancing {@link PositionSupplier} if the overall + * state indicates an advancing ad playback position. + * + * @param positionMs The current ad playback position in milliseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setAdPositionMs(long positionMs) { + this.adPositionMs = positionMs; + return this; + } + + /** + * Sets the {@link PositionSupplier} for the current ad playback position in milliseconds. The + * value is unused if no ad is playing. + * + *

    The supplier is expected to return the updated position on every call if the playback is + * advancing, for example by using {@link PositionSupplier#getExtrapolating}. + * + * @param adPositionMsSupplier The {@link PositionSupplier} for the current ad playback + * position in milliseconds. The value is unused if no ad is playing. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setAdPositionMs(PositionSupplier adPositionMsSupplier) { + this.adPositionMs = C.TIME_UNSET; + this.adPositionMsSupplier = adPositionMsSupplier; + return this; + } + + /** + * Sets the {@link PositionSupplier} for the estimated position up to which the currently + * playing content is buffered, in milliseconds. + * + * @param contentBufferedPositionMsSupplier The {@link PositionSupplier} for the estimated + * position up to which the currently playing content is buffered, in milliseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setContentBufferedPositionMs( + PositionSupplier contentBufferedPositionMsSupplier) { + this.contentBufferedPositionMsSupplier = contentBufferedPositionMsSupplier; + return this; + } + + /** + * Sets the {@link PositionSupplier} for the estimated position up to which the currently + * playing ad is buffered, in milliseconds. The value is unused if no ad is playing. + * + * @param adBufferedPositionMsSupplier The {@link PositionSupplier} for the estimated position + * up to which the currently playing ad is buffered, in milliseconds. The value is unused + * if no ad is playing. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setAdBufferedPositionMs(PositionSupplier adBufferedPositionMsSupplier) { + this.adBufferedPositionMsSupplier = adBufferedPositionMsSupplier; + return this; + } + + /** + * Sets the {@link PositionSupplier} for the estimated total buffered duration in + * milliseconds. + * + * @param totalBufferedDurationMsSupplier The {@link PositionSupplier} for the estimated total + * buffered duration in milliseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setTotalBufferedDurationMs(PositionSupplier totalBufferedDurationMsSupplier) { + this.totalBufferedDurationMsSupplier = totalBufferedDurationMsSupplier; + return this; + } + + /** + * Signals that a position discontinuity happened since the last player update and sets the + * reason for it. + * + * @param positionDiscontinuityReason The {@linkplain Player.DiscontinuityReason reason} for + * the discontinuity. + * @param discontinuityPositionMs The position, in milliseconds, in the current content or ad + * from which playback continues after the discontinuity. + * @return This builder. + * @see #clearPositionDiscontinuity + */ + @CanIgnoreReturnValue + public Builder setPositionDiscontinuity( + @Player.DiscontinuityReason int positionDiscontinuityReason, + long discontinuityPositionMs) { + this.hasPositionDiscontinuity = true; + this.positionDiscontinuityReason = positionDiscontinuityReason; + this.discontinuityPositionMs = discontinuityPositionMs; + return this; + } + + /** + * Clears a previously set position discontinuity signal. + * + * @return This builder. + * @see #hasPositionDiscontinuity + */ + @CanIgnoreReturnValue + public Builder clearPositionDiscontinuity() { + this.hasPositionDiscontinuity = false; + return this; + } + /** Builds the {@link State}. */ public State build() { return new State(this); } - } + } + + /** The available {@link Commands}. */ + public final Commands availableCommands; + /** Whether playback should proceed when ready and not suppressed. */ + public final boolean playWhenReady; + /** The last reason for changing {@link #playWhenReady}. */ + public final @PlayWhenReadyChangeReason int playWhenReadyChangeReason; + /** The {@linkplain Player.State state} of the player. */ + public final @Player.State int playbackState; + /** The reason why playback is suppressed even if {@link #getPlayWhenReady()} is true. */ + public final @PlaybackSuppressionReason int playbackSuppressionReason; + /** The last error that caused playback to fail, or null if there was no error. */ + @Nullable public final PlaybackException playerError; + /** The {@link RepeatMode} used for playback. */ + public final @RepeatMode int repeatMode; + /** Whether shuffling of media items is enabled. */ + public final boolean shuffleModeEnabled; + /** Whether the player is currently loading its source. */ + public final boolean isLoading; + /** The {@link Player#seekBack()} increment in milliseconds. */ + public final long seekBackIncrementMs; + /** The {@link Player#seekForward()} increment in milliseconds. */ + public final long seekForwardIncrementMs; + /** + * The maximum position for which {@link #seekToPrevious()} seeks to the previous item, in + * milliseconds. + */ + public final long maxSeekToPreviousPositionMs; + /** The currently active {@link PlaybackParameters}. */ + public final PlaybackParameters playbackParameters; + /** The currently active {@link TrackSelectionParameters}. */ + public final TrackSelectionParameters trackSelectionParameters; + /** The current {@link AudioAttributes}. */ + public final AudioAttributes audioAttributes; + /** The current audio volume, with 0 being silence and 1 being unity gain (signal unchanged). */ + @FloatRange(from = 0, to = 1.0) + public final float volume; + /** The current video size. */ + public final VideoSize videoSize; + /** The current {@linkplain CueGroup cues}. */ + public final CueGroup currentCues; + /** The {@link DeviceInfo}. */ + public final DeviceInfo deviceInfo; + /** The current device volume. */ + @IntRange(from = 0) + public final int deviceVolume; + /** Whether the device is muted. */ + public final boolean isDeviceMuted; + /** The audio session id. */ + public final int audioSessionId; + /** Whether skipping silences in the audio stream is enabled. */ + public final boolean skipSilenceEnabled; + /** The size of the surface onto which the video is being rendered. */ + public final Size surfaceSize; + /** + * Whether a frame has been rendered for the first time since setting the surface, a rendering + * reset, or since the stream being rendered was changed. + */ + public final boolean newlyRenderedFirstFrame; + /** The most recent timed metadata. */ + public final Metadata timedMetadata; + /** The playlist items. */ + public final ImmutableList playlistItems; + /** The {@link Timeline} derived from the {@linkplain #playlistItems playlist items}. */ + public final Timeline timeline; + /** The playlist {@link MediaMetadata}. */ + public final MediaMetadata playlistMetadata; + /** The current media item index. */ + public final int currentMediaItemIndex; + /** + * The current period index, or {@link C#INDEX_UNSET} to assume the first period of the current + * playlist item is played. + */ + public final int currentPeriodIndex; + /** The current ad group index, or {@link C#INDEX_UNSET} if no ad is playing. */ + public final int currentAdGroupIndex; + /** The current ad index in the ad group, or {@link C#INDEX_UNSET} if no ad is playing. */ + public final int currentAdIndexInAdGroup; + /** The {@link PositionSupplier} for the current content playback position in milliseconds. */ + public final PositionSupplier contentPositionMsSupplier; + /** + * The {@link PositionSupplier} for the current ad playback position in milliseconds. The value + * is unused if no ad is playing. + */ + public final PositionSupplier adPositionMsSupplier; + /** + * The {@link PositionSupplier} for the estimated position up to which the currently playing + * content is buffered, in milliseconds. + */ + public final PositionSupplier contentBufferedPositionMsSupplier; + /** + * The {@link PositionSupplier} for the estimated position up to which the currently playing ad + * is buffered, in milliseconds. The value is unused if no ad is playing. + */ + public final PositionSupplier adBufferedPositionMsSupplier; + /** The {@link PositionSupplier} for the estimated total buffered duration in milliseconds. */ + public final PositionSupplier totalBufferedDurationMsSupplier; + /** Signals that a position discontinuity happened since the last update to the player. */ + public final boolean hasPositionDiscontinuity; + /** + * The {@linkplain Player.DiscontinuityReason reason} for the last position discontinuity. The + * value is unused if {@link #hasPositionDiscontinuity} is {@code false}. + */ + public final @Player.DiscontinuityReason int positionDiscontinuityReason; + /** + * The position, in milliseconds, in the current content or ad from which playback continued + * after the discontinuity. The value is unused if {@link #hasPositionDiscontinuity} is {@code + * false}. + */ + public final long discontinuityPositionMs; + + private State(Builder builder) { + if (builder.timeline.isEmpty()) { + checkArgument( + builder.playbackState == Player.STATE_IDLE + || builder.playbackState == Player.STATE_ENDED); + } else { + checkArgument(builder.currentMediaItemIndex < builder.timeline.getWindowCount()); + if (builder.currentPeriodIndex != C.INDEX_UNSET) { + checkArgument(builder.currentPeriodIndex < builder.timeline.getPeriodCount()); + checkArgument( + builder.timeline.getPeriod(builder.currentPeriodIndex, new Timeline.Period()) + .windowIndex + == builder.currentMediaItemIndex); + } + if (builder.currentAdGroupIndex != C.INDEX_UNSET) { + int periodIndex = + builder.currentPeriodIndex != C.INDEX_UNSET + ? builder.currentPeriodIndex + : builder.timeline.getWindow(builder.currentMediaItemIndex, new Timeline.Window()) + .firstPeriodIndex; + Timeline.Period period = builder.timeline.getPeriod(periodIndex, new Timeline.Period()); + checkArgument(builder.currentAdGroupIndex < period.getAdGroupCount()); + int adCountInGroup = period.getAdCountInAdGroup(builder.currentAdGroupIndex); + if (adCountInGroup != C.LENGTH_UNSET) { + checkArgument(builder.currentAdIndexInAdGroup < adCountInGroup); + } + } + } + if (builder.playerError != null) { + checkArgument(builder.playbackState == Player.STATE_IDLE); + } + if (builder.playbackState == Player.STATE_IDLE + || builder.playbackState == Player.STATE_ENDED) { + checkArgument(!builder.isLoading); + } + PositionSupplier contentPositionMsSupplier = builder.contentPositionMsSupplier; + if (builder.contentPositionMs != C.TIME_UNSET) { + if (builder.currentAdGroupIndex == C.INDEX_UNSET + && builder.playWhenReady + && builder.playbackState == Player.STATE_READY + && builder.playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE) { + contentPositionMsSupplier = + PositionSupplier.getExtrapolating( + builder.contentPositionMs, builder.playbackParameters.speed); + } else { + contentPositionMsSupplier = PositionSupplier.getConstant(builder.contentPositionMs); + } + } + PositionSupplier adPositionMsSupplier = builder.adPositionMsSupplier; + if (builder.adPositionMs != C.TIME_UNSET) { + if (builder.currentAdGroupIndex != C.INDEX_UNSET + && builder.playWhenReady + && builder.playbackState == Player.STATE_READY + && builder.playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE) { + adPositionMsSupplier = + PositionSupplier.getExtrapolating(builder.adPositionMs, /* playbackSpeed= */ 1f); + } else { + adPositionMsSupplier = PositionSupplier.getConstant(builder.adPositionMs); + } + } + this.availableCommands = builder.availableCommands; + this.playWhenReady = builder.playWhenReady; + this.playWhenReadyChangeReason = builder.playWhenReadyChangeReason; + this.playbackState = builder.playbackState; + this.playbackSuppressionReason = builder.playbackSuppressionReason; + this.playerError = builder.playerError; + this.repeatMode = builder.repeatMode; + this.shuffleModeEnabled = builder.shuffleModeEnabled; + this.isLoading = builder.isLoading; + this.seekBackIncrementMs = builder.seekBackIncrementMs; + this.seekForwardIncrementMs = builder.seekForwardIncrementMs; + this.maxSeekToPreviousPositionMs = builder.maxSeekToPreviousPositionMs; + this.playbackParameters = builder.playbackParameters; + this.trackSelectionParameters = builder.trackSelectionParameters; + this.audioAttributes = builder.audioAttributes; + this.volume = builder.volume; + this.videoSize = builder.videoSize; + this.currentCues = builder.currentCues; + this.deviceInfo = builder.deviceInfo; + this.deviceVolume = builder.deviceVolume; + this.isDeviceMuted = builder.isDeviceMuted; + this.audioSessionId = builder.audioSessionId; + this.skipSilenceEnabled = builder.skipSilenceEnabled; + this.surfaceSize = builder.surfaceSize; + this.newlyRenderedFirstFrame = builder.newlyRenderedFirstFrame; + this.timedMetadata = builder.timedMetadata; + this.playlistItems = builder.playlistItems; + this.timeline = builder.timeline; + this.playlistMetadata = builder.playlistMetadata; + this.currentMediaItemIndex = builder.currentMediaItemIndex; + this.currentPeriodIndex = builder.currentPeriodIndex; + this.currentAdGroupIndex = builder.currentAdGroupIndex; + this.currentAdIndexInAdGroup = builder.currentAdIndexInAdGroup; + this.contentPositionMsSupplier = contentPositionMsSupplier; + this.adPositionMsSupplier = adPositionMsSupplier; + this.contentBufferedPositionMsSupplier = builder.contentBufferedPositionMsSupplier; + this.adBufferedPositionMsSupplier = builder.adBufferedPositionMsSupplier; + this.totalBufferedDurationMsSupplier = builder.totalBufferedDurationMsSupplier; + this.hasPositionDiscontinuity = builder.hasPositionDiscontinuity; + this.positionDiscontinuityReason = builder.positionDiscontinuityReason; + this.discontinuityPositionMs = builder.discontinuityPositionMs; + } + + /** Returns a {@link Builder} pre-populated with the current state values. */ + public Builder buildUpon() { + return new Builder(this); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof State)) { + return false; + } + State state = (State) o; + return playWhenReady == state.playWhenReady + && playWhenReadyChangeReason == state.playWhenReadyChangeReason + && availableCommands.equals(state.availableCommands) + && playbackState == state.playbackState + && playbackSuppressionReason == state.playbackSuppressionReason + && Util.areEqual(playerError, state.playerError) + && repeatMode == state.repeatMode + && shuffleModeEnabled == state.shuffleModeEnabled + && isLoading == state.isLoading + && seekBackIncrementMs == state.seekBackIncrementMs + && seekForwardIncrementMs == state.seekForwardIncrementMs + && maxSeekToPreviousPositionMs == state.maxSeekToPreviousPositionMs + && playbackParameters.equals(state.playbackParameters) + && trackSelectionParameters.equals(state.trackSelectionParameters) + && audioAttributes.equals(state.audioAttributes) + && volume == state.volume + && videoSize.equals(state.videoSize) + && currentCues.equals(state.currentCues) + && deviceInfo.equals(state.deviceInfo) + && deviceVolume == state.deviceVolume + && isDeviceMuted == state.isDeviceMuted + && audioSessionId == state.audioSessionId + && skipSilenceEnabled == state.skipSilenceEnabled + && surfaceSize.equals(state.surfaceSize) + && newlyRenderedFirstFrame == state.newlyRenderedFirstFrame + && timedMetadata.equals(state.timedMetadata) + && playlistItems.equals(state.playlistItems) + && playlistMetadata.equals(state.playlistMetadata) + && currentMediaItemIndex == state.currentMediaItemIndex + && currentPeriodIndex == state.currentPeriodIndex + && currentAdGroupIndex == state.currentAdGroupIndex + && currentAdIndexInAdGroup == state.currentAdIndexInAdGroup + && contentPositionMsSupplier.equals(state.contentPositionMsSupplier) + && adPositionMsSupplier.equals(state.adPositionMsSupplier) + && contentBufferedPositionMsSupplier.equals(state.contentBufferedPositionMsSupplier) + && adBufferedPositionMsSupplier.equals(state.adBufferedPositionMsSupplier) + && totalBufferedDurationMsSupplier.equals(state.totalBufferedDurationMsSupplier) + && hasPositionDiscontinuity == state.hasPositionDiscontinuity + && positionDiscontinuityReason == state.positionDiscontinuityReason + && discontinuityPositionMs == state.discontinuityPositionMs; + } + + @Override + public int hashCode() { + int result = 7; + result = 31 * result + availableCommands.hashCode(); + result = 31 * result + (playWhenReady ? 1 : 0); + result = 31 * result + playWhenReadyChangeReason; + result = 31 * result + playbackState; + result = 31 * result + playbackSuppressionReason; + result = 31 * result + (playerError == null ? 0 : playerError.hashCode()); + result = 31 * result + repeatMode; + result = 31 * result + (shuffleModeEnabled ? 1 : 0); + result = 31 * result + (isLoading ? 1 : 0); + result = 31 * result + (int) (seekBackIncrementMs ^ (seekBackIncrementMs >>> 32)); + result = 31 * result + (int) (seekForwardIncrementMs ^ (seekForwardIncrementMs >>> 32)); + result = + 31 * result + (int) (maxSeekToPreviousPositionMs ^ (maxSeekToPreviousPositionMs >>> 32)); + result = 31 * result + playbackParameters.hashCode(); + result = 31 * result + trackSelectionParameters.hashCode(); + result = 31 * result + audioAttributes.hashCode(); + result = 31 * result + Float.floatToRawIntBits(volume); + result = 31 * result + videoSize.hashCode(); + result = 31 * result + currentCues.hashCode(); + result = 31 * result + deviceInfo.hashCode(); + result = 31 * result + deviceVolume; + result = 31 * result + (isDeviceMuted ? 1 : 0); + result = 31 * result + audioSessionId; + result = 31 * result + (skipSilenceEnabled ? 1 : 0); + result = 31 * result + surfaceSize.hashCode(); + result = 31 * result + (newlyRenderedFirstFrame ? 1 : 0); + result = 31 * result + timedMetadata.hashCode(); + result = 31 * result + playlistItems.hashCode(); + result = 31 * result + playlistMetadata.hashCode(); + result = 31 * result + currentMediaItemIndex; + result = 31 * result + currentPeriodIndex; + result = 31 * result + currentAdGroupIndex; + result = 31 * result + currentAdIndexInAdGroup; + result = 31 * result + contentPositionMsSupplier.hashCode(); + result = 31 * result + adPositionMsSupplier.hashCode(); + result = 31 * result + contentBufferedPositionMsSupplier.hashCode(); + result = 31 * result + adBufferedPositionMsSupplier.hashCode(); + result = 31 * result + totalBufferedDurationMsSupplier.hashCode(); + result = 31 * result + (hasPositionDiscontinuity ? 1 : 0); + result = 31 * result + positionDiscontinuityReason; + result = 31 * result + (int) (discontinuityPositionMs ^ (discontinuityPositionMs >>> 32)); + return result; + } + } + + private static final class PlaylistTimeline extends Timeline { + + private final ImmutableList playlistItems; + private final int[] firstPeriodIndexByWindowIndex; + private final int[] windowIndexByPeriodIndex; + private final HashMap periodIndexByUid; + + public PlaylistTimeline(ImmutableList playlistItems) { + int playlistItemCount = playlistItems.size(); + this.playlistItems = playlistItems; + this.firstPeriodIndexByWindowIndex = new int[playlistItemCount]; + int periodCount = 0; + for (int i = 0; i < playlistItemCount; i++) { + PlaylistItem playlistItem = playlistItems.get(i); + firstPeriodIndexByWindowIndex[i] = periodCount; + periodCount += getPeriodCountInPlaylistItem(playlistItem); + } + this.windowIndexByPeriodIndex = new int[periodCount]; + this.periodIndexByUid = new HashMap<>(); + int periodIndex = 0; + for (int i = 0; i < playlistItemCount; i++) { + PlaylistItem playlistItem = playlistItems.get(i); + for (int j = 0; j < getPeriodCountInPlaylistItem(playlistItem); j++) { + periodIndexByUid.put(playlistItem.getPeriodUid(j), periodIndex); + windowIndexByPeriodIndex[periodIndex] = i; + periodIndex++; + } + } + } + + @Override + public int getWindowCount() { + return playlistItems.size(); + } + + @Override + public int getNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) { + // TODO: Support shuffle order. + return super.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); + } + + @Override + public int getPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) { + // TODO: Support shuffle order. + return super.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + // TODO: Support shuffle order. + return super.getLastWindowIndex(shuffleModeEnabled); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + // TODO: Support shuffle order. + return super.getFirstWindowIndex(shuffleModeEnabled); + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + return playlistItems + .get(windowIndex) + .getWindow(firstPeriodIndexByWindowIndex[windowIndex], window); + } + + @Override + public int getPeriodCount() { + return windowIndexByPeriodIndex.length; + } + + @Override + public Period getPeriodByUid(Object periodUid, Period period) { + int periodIndex = checkNotNull(periodIndexByUid.get(periodUid)); + return getPeriod(periodIndex, period, /* setIds= */ true); + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + int windowIndex = windowIndexByPeriodIndex[periodIndex]; + int periodIndexInWindow = periodIndex - firstPeriodIndexByWindowIndex[windowIndex]; + return playlistItems.get(windowIndex).getPeriod(windowIndex, periodIndexInWindow, period); + } + + @Override + public int getIndexOfPeriod(Object uid) { + @Nullable Integer index = periodIndexByUid.get(uid); + return index == null ? C.INDEX_UNSET : index; + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + int windowIndex = windowIndexByPeriodIndex[periodIndex]; + int periodIndexInWindow = periodIndex - firstPeriodIndexByWindowIndex[windowIndex]; + return playlistItems.get(windowIndex).getPeriodUid(periodIndexInWindow); + } + + private static int getPeriodCountInPlaylistItem(PlaylistItem playlistItem) { + return playlistItem.periods.isEmpty() ? 1 : playlistItem.periods.size(); + } + } + + /** + * An immutable description of a playlist item, containing both static setup information like + * {@link MediaItem} and dynamic data that is generally read from the media like the duration. + */ + protected static final class PlaylistItem { + + /** A builder for {@link PlaylistItem} objects. */ + public static final class Builder { + + private Object uid; + private Tracks tracks; + private MediaItem mediaItem; + @Nullable private MediaMetadata mediaMetadata; + @Nullable private Object manifest; + @Nullable private MediaItem.LiveConfiguration liveConfiguration; + private long presentationStartTimeMs; + private long windowStartTimeMs; + private long elapsedRealtimeEpochOffsetMs; + private boolean isSeekable; + private boolean isDynamic; + private long defaultPositionUs; + private long durationUs; + private long positionInFirstPeriodUs; + private boolean isPlaceholder; + private ImmutableList periods; + + /** + * Creates the builder. + * + * @param uid The unique identifier of the playlist item within a playlist. This value will be + * set as {@link Timeline.Window#uid} for this item. + */ + public Builder(Object uid) { + this.uid = uid; + tracks = Tracks.EMPTY; + mediaItem = MediaItem.EMPTY; + mediaMetadata = null; + manifest = null; + liveConfiguration = null; + presentationStartTimeMs = C.TIME_UNSET; + windowStartTimeMs = C.TIME_UNSET; + elapsedRealtimeEpochOffsetMs = C.TIME_UNSET; + isSeekable = false; + isDynamic = false; + defaultPositionUs = 0; + durationUs = C.TIME_UNSET; + positionInFirstPeriodUs = 0; + isPlaceholder = false; + periods = ImmutableList.of(); + } + + private Builder(PlaylistItem playlistItem) { + this.uid = playlistItem.uid; + this.tracks = playlistItem.tracks; + this.mediaItem = playlistItem.mediaItem; + this.mediaMetadata = playlistItem.mediaMetadata; + this.manifest = playlistItem.manifest; + this.liveConfiguration = playlistItem.liveConfiguration; + this.presentationStartTimeMs = playlistItem.presentationStartTimeMs; + this.windowStartTimeMs = playlistItem.windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = playlistItem.elapsedRealtimeEpochOffsetMs; + this.isSeekable = playlistItem.isSeekable; + this.isDynamic = playlistItem.isDynamic; + this.defaultPositionUs = playlistItem.defaultPositionUs; + this.durationUs = playlistItem.durationUs; + this.positionInFirstPeriodUs = playlistItem.positionInFirstPeriodUs; + this.isPlaceholder = playlistItem.isPlaceholder; + this.periods = playlistItem.periods; + } + + /** + * Sets the unique identifier of this playlist item within a playlist. + * + *

    This value will be set as {@link Timeline.Window#uid} for this item. + * + * @param uid The unique identifier of this playlist item within a playlist. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setUid(Object uid) { + this.uid = uid; + return this; + } + + /** + * Sets the {@link Tracks} of this playlist item. + * + * @param tracks The {@link Tracks} of this playlist item. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setTracks(Tracks tracks) { + this.tracks = tracks; + return this; + } + + /** + * Sets the {@link MediaItem} for this playlist item. + * + * @param mediaItem The {@link MediaItem} for this playlist item. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setMediaItem(MediaItem mediaItem) { + this.mediaItem = mediaItem; + return this; + } + + /** + * Sets the {@link MediaMetadata}. + * + *

    This data includes static data from the {@link MediaItem#mediaMetadata MediaItem} and + * the media's {@link Format#metadata Format}, as well any dynamic metadata that has been + * parsed from the media. If null, the metadata is assumed to be the simple combination of the + * {@link MediaItem#mediaMetadata MediaItem} metadata and the metadata of the selected {@link + * Format#metadata Formats}. + * + * @param mediaMetadata The {@link MediaMetadata}, or null to assume that the metadata is the + * simple combination of the {@link MediaItem#mediaMetadata MediaItem} metadata and the + * metadata of the selected {@link Format#metadata Formats}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setMediaMetadata(@Nullable MediaMetadata mediaMetadata) { + this.mediaMetadata = mediaMetadata; + return this; + } + + /** + * Sets the manifest of the playlist item. + * + * @param manifest The manifest of the playlist item, or null if not applicable. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setManifest(@Nullable Object manifest) { + this.manifest = manifest; + return this; + } + + /** + * Sets the active {@link MediaItem.LiveConfiguration}, or null if the playlist item is not + * live. + * + * @param liveConfiguration The active {@link MediaItem.LiveConfiguration}, or null if the + * playlist item is not live. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setLiveConfiguration(@Nullable MediaItem.LiveConfiguration liveConfiguration) { + this.liveConfiguration = liveConfiguration; + return this; + } + + /** + * Sets the start time of the live presentation. + * + *

    This value can only be set to anything other than {@link C#TIME_UNSET} if the stream is + * {@linkplain #setLiveConfiguration live}. + * + * @param presentationStartTimeMs The start time of the live presentation, in milliseconds + * since the Unix epoch, or {@link C#TIME_UNSET} if unknown or not applicable. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPresentationStartTimeMs(long presentationStartTimeMs) { + this.presentationStartTimeMs = presentationStartTimeMs; + return this; + } + + /** + * Sets the start time of the live window. + * + *

    This value can only be set to anything other than {@link C#TIME_UNSET} if the stream is + * {@linkplain #setLiveConfiguration live}. The value should also be greater or equal than the + * {@linkplain #setPresentationStartTimeMs presentation start time}, if set. + * + * @param windowStartTimeMs The start time of the live window, in milliseconds since the Unix + * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setWindowStartTimeMs(long windowStartTimeMs) { + this.windowStartTimeMs = windowStartTimeMs; + return this; + } + + /** + * Sets the offset between {@link SystemClock#elapsedRealtime()} and the time since the Unix + * epoch according to the clock of the media origin server. + * + *

    This value can only be set to anything other than {@link C#TIME_UNSET} if the stream is + * {@linkplain #setLiveConfiguration live}. + * + * @param elapsedRealtimeEpochOffsetMs The offset between {@link + * SystemClock#elapsedRealtime()} and the time since the Unix epoch according to the clock + * of the media origin server, or {@link C#TIME_UNSET} if unknown or not applicable. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setElapsedRealtimeEpochOffsetMs(long elapsedRealtimeEpochOffsetMs) { + this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; + return this; + } + + /** + * Sets whether it's possible to seek within this playlist item. + * + * @param isSeekable Whether it's possible to seek within this playlist item. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setIsSeekable(boolean isSeekable) { + this.isSeekable = isSeekable; + return this; + } + + /** + * Sets whether this playlist item may change over time, for example a moving live window. + * + * @param isDynamic Whether this playlist item may change over time, for example a moving live + * window. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setIsDynamic(boolean isDynamic) { + this.isDynamic = isDynamic; + return this; + } + + /** + * Sets the default position relative to the start of the playlist item at which to begin + * playback, in microseconds. + * + *

    The default position must be less or equal to the {@linkplain #setDurationUs duration}, + * is set. + * + * @param defaultPositionUs The default position relative to the start of the playlist item at + * which to begin playback, in microseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setDefaultPositionUs(long defaultPositionUs) { + checkArgument(defaultPositionUs >= 0); + this.defaultPositionUs = defaultPositionUs; + return this; + } + + /** + * Sets the duration of the playlist item, in microseconds. + * + *

    If both this duration and all {@linkplain #setPeriods period} durations are set, the sum + * of this duration and the {@linkplain #setPositionInFirstPeriodUs offset in the first + * period} must match the total duration of all periods. + * + * @param durationUs The duration of the playlist item, in microseconds, or {@link + * C#TIME_UNSET} if unknown. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setDurationUs(long durationUs) { + checkArgument(durationUs == C.TIME_UNSET || durationUs >= 0); + this.durationUs = durationUs; + return this; + } + + /** + * Sets the position of the start of this playlist item relative to the start of the first + * period belonging to it, in microseconds. + * + * @param positionInFirstPeriodUs The position of the start of this playlist item relative to + * the start of the first period belonging to it, in microseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPositionInFirstPeriodUs(long positionInFirstPeriodUs) { + checkArgument(positionInFirstPeriodUs >= 0); + this.positionInFirstPeriodUs = positionInFirstPeriodUs; + return this; + } + + /** + * Sets whether this playlist item contains placeholder information because the real + * information has yet to be loaded. + * + * @param isPlaceholder Whether this playlist item contains placeholder information because + * the real information has yet to be loaded. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setIsPlaceholder(boolean isPlaceholder) { + this.isPlaceholder = isPlaceholder; + return this; + } + + /** + * Sets the list of {@linkplain PeriodData periods} in this playlist item. + * + *

    All periods must have unique {@linkplain PeriodData.Builder#setUid UIDs} and only the + * last period is allowed to have an unset {@linkplain PeriodData.Builder#setDurationUs + * duration}. + * + * @param periods The list of {@linkplain PeriodData periods} in this playlist item, or an + * empty list to assume a single period without ads and the same duration as the playlist + * item. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPeriods(List periods) { + int periodCount = periods.size(); + for (int i = 0; i < periodCount - 1; i++) { + checkArgument(periods.get(i).durationUs != C.TIME_UNSET); + for (int j = i + 1; j < periodCount; j++) { + checkArgument(!periods.get(i).uid.equals(periods.get(j).uid)); + } + } + this.periods = ImmutableList.copyOf(periods); + return this; + } + + /** Builds the {@link PlaylistItem}. */ + public PlaylistItem build() { + return new PlaylistItem(this); + } + } + + /** The unique identifier of this playlist item. */ + public final Object uid; + /** The {@link Tracks} of this playlist item. */ + public final Tracks tracks; + /** The {@link MediaItem} for this playlist item. */ + public final MediaItem mediaItem; + /** + * The {@link MediaMetadata}, including static data from the {@link MediaItem#mediaMetadata + * MediaItem} and the media's {@link Format#metadata Format}, as well any dynamic metadata that + * has been parsed from the media. If null, the metadata is assumed to be the simple combination + * of the {@link MediaItem#mediaMetadata MediaItem} metadata and the metadata of the selected + * {@link Format#metadata Formats}. + */ + @Nullable public final MediaMetadata mediaMetadata; + /** The manifest of the playlist item, or null if not applicable. */ + @Nullable public final Object manifest; + /** The active {@link MediaItem.LiveConfiguration}, or null if the playlist item is not live. */ + @Nullable public final MediaItem.LiveConfiguration liveConfiguration; + /** + * The start time of the live presentation, in milliseconds since the Unix epoch, or {@link + * C#TIME_UNSET} if unknown or not applicable. + */ + public final long presentationStartTimeMs; + /** + * The start time of the live window, in milliseconds since the Unix epoch, or {@link + * C#TIME_UNSET} if unknown or not applicable. + */ + public final long windowStartTimeMs; + /** + * The offset between {@link SystemClock#elapsedRealtime()} and the time since the Unix epoch + * according to the clock of the media origin server, or {@link C#TIME_UNSET} if unknown or not + * applicable. + */ + public final long elapsedRealtimeEpochOffsetMs; + /** Whether it's possible to seek within this playlist item. */ + public final boolean isSeekable; + /** Whether this playlist item may change over time, for example a moving live window. */ + public final boolean isDynamic; + /** + * The default position relative to the start of the playlist item at which to begin playback, + * in microseconds. + */ + public final long defaultPositionUs; + /** The duration of the playlist item, in microseconds, or {@link C#TIME_UNSET} if unknown. */ + public final long durationUs; + /** + * The position of the start of this playlist item relative to the start of the first period + * belonging to it, in microseconds. + */ + public final long positionInFirstPeriodUs; + /** + * Whether this playlist item contains placeholder information because the real information has + * yet to be loaded. + */ + public final boolean isPlaceholder; + /** + * The list of {@linkplain PeriodData periods} in this playlist item, or an empty list to assume + * a single period without ads and the same duration as the playlist item. + */ + public final ImmutableList periods; + + private final long[] periodPositionInWindowUs; + private final MediaMetadata combinedMediaMetadata; + + private PlaylistItem(Builder builder) { + if (builder.liveConfiguration == null) { + checkArgument(builder.presentationStartTimeMs == C.TIME_UNSET); + checkArgument(builder.windowStartTimeMs == C.TIME_UNSET); + checkArgument(builder.elapsedRealtimeEpochOffsetMs == C.TIME_UNSET); + } else if (builder.presentationStartTimeMs != C.TIME_UNSET + && builder.windowStartTimeMs != C.TIME_UNSET) { + checkArgument(builder.windowStartTimeMs >= builder.presentationStartTimeMs); + } + int periodCount = builder.periods.size(); + if (builder.durationUs != C.TIME_UNSET) { + checkArgument(builder.defaultPositionUs <= builder.durationUs); + } + this.uid = builder.uid; + this.tracks = builder.tracks; + this.mediaItem = builder.mediaItem; + this.mediaMetadata = builder.mediaMetadata; + this.manifest = builder.manifest; + this.liveConfiguration = builder.liveConfiguration; + this.presentationStartTimeMs = builder.presentationStartTimeMs; + this.windowStartTimeMs = builder.windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = builder.elapsedRealtimeEpochOffsetMs; + this.isSeekable = builder.isSeekable; + this.isDynamic = builder.isDynamic; + this.defaultPositionUs = builder.defaultPositionUs; + this.durationUs = builder.durationUs; + this.positionInFirstPeriodUs = builder.positionInFirstPeriodUs; + this.isPlaceholder = builder.isPlaceholder; + this.periods = builder.periods; + periodPositionInWindowUs = new long[periods.size()]; + if (!periods.isEmpty()) { + periodPositionInWindowUs[0] = -positionInFirstPeriodUs; + for (int i = 0; i < periodCount - 1; i++) { + periodPositionInWindowUs[i + 1] = periodPositionInWindowUs[i] + periods.get(i).durationUs; + } + } + combinedMediaMetadata = + mediaMetadata != null ? mediaMetadata : getCombinedMediaMetadata(mediaItem, tracks); + } + + /** Returns a {@link Builder} pre-populated with the current values. */ + public Builder buildUpon() { + return new Builder(this); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PlaylistItem)) { + return false; + } + PlaylistItem playlistItem = (PlaylistItem) o; + return this.uid.equals(playlistItem.uid) + && this.tracks.equals(playlistItem.tracks) + && this.mediaItem.equals(playlistItem.mediaItem) + && Util.areEqual(this.mediaMetadata, playlistItem.mediaMetadata) + && Util.areEqual(this.manifest, playlistItem.manifest) + && Util.areEqual(this.liveConfiguration, playlistItem.liveConfiguration) + && this.presentationStartTimeMs == playlistItem.presentationStartTimeMs + && this.windowStartTimeMs == playlistItem.windowStartTimeMs + && this.elapsedRealtimeEpochOffsetMs == playlistItem.elapsedRealtimeEpochOffsetMs + && this.isSeekable == playlistItem.isSeekable + && this.isDynamic == playlistItem.isDynamic + && this.defaultPositionUs == playlistItem.defaultPositionUs + && this.durationUs == playlistItem.durationUs + && this.positionInFirstPeriodUs == playlistItem.positionInFirstPeriodUs + && this.isPlaceholder == playlistItem.isPlaceholder + && this.periods.equals(playlistItem.periods); + } + + @Override + public int hashCode() { + int result = 7; + result = 31 * result + uid.hashCode(); + result = 31 * result + tracks.hashCode(); + result = 31 * result + mediaItem.hashCode(); + result = 31 * result + (mediaMetadata == null ? 0 : mediaMetadata.hashCode()); + result = 31 * result + (manifest == null ? 0 : manifest.hashCode()); + result = 31 * result + (liveConfiguration == null ? 0 : liveConfiguration.hashCode()); + result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32)); + result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32)); + result = + 31 * result + + (int) (elapsedRealtimeEpochOffsetMs ^ (elapsedRealtimeEpochOffsetMs >>> 32)); + result = 31 * result + (isSeekable ? 1 : 0); + result = 31 * result + (isDynamic ? 1 : 0); + result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32)); + result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); + result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32)); + result = 31 * result + (isPlaceholder ? 1 : 0); + result = 31 * result + periods.hashCode(); + return result; + } + + private Timeline.Window getWindow(int firstPeriodIndex, Timeline.Window window) { + int periodCount = periods.isEmpty() ? 1 : periods.size(); + window.set( + uid, + mediaItem, + manifest, + presentationStartTimeMs, + windowStartTimeMs, + elapsedRealtimeEpochOffsetMs, + isSeekable, + isDynamic, + liveConfiguration, + defaultPositionUs, + durationUs, + firstPeriodIndex, + /* lastPeriodIndex= */ firstPeriodIndex + periodCount - 1, + positionInFirstPeriodUs); + window.isPlaceholder = isPlaceholder; + return window; + } + + private Timeline.Period getPeriod( + int windowIndex, int periodIndexInPlaylistItem, Timeline.Period period) { + if (periods.isEmpty()) { + period.set( + /* id= */ uid, + uid, + windowIndex, + /* durationUs= */ positionInFirstPeriodUs + durationUs, + /* positionInWindowUs= */ 0, + AdPlaybackState.NONE, + isPlaceholder); + } else { + PeriodData periodData = periods.get(periodIndexInPlaylistItem); + Object periodId = periodData.uid; + Object periodUid = Pair.create(uid, periodId); + period.set( + periodId, + periodUid, + windowIndex, + periodData.durationUs, + periodPositionInWindowUs[periodIndexInPlaylistItem], + periodData.adPlaybackState, + periodData.isPlaceholder); + } + return period; + } + + private Object getPeriodUid(int periodIndexInPlaylistItem) { + if (periods.isEmpty()) { + return uid; + } + Object periodId = periods.get(periodIndexInPlaylistItem).uid; + return Pair.create(uid, periodId); + } + + private static MediaMetadata getCombinedMediaMetadata(MediaItem mediaItem, Tracks tracks) { + MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder(); + int trackGroupCount = tracks.getGroups().size(); + for (int i = 0; i < trackGroupCount; i++) { + Tracks.Group group = tracks.getGroups().get(i); + for (int j = 0; j < group.length; j++) { + if (group.isTrackSelected(j)) { + Format format = group.getTrackFormat(j); + if (format.metadata != null) { + for (int k = 0; k < format.metadata.length(); k++) { + format.metadata.get(k).populateMediaMetadata(metadataBuilder); + } + } + } + } + } + return metadataBuilder.populate(mediaItem.mediaMetadata).build(); + } + } + + /** Data describing the properties of a period inside a {@link PlaylistItem}. */ + protected static final class PeriodData { + + /** A builder for {@link PeriodData} objects. */ + public static final class Builder { + + private Object uid; + private long durationUs; + private AdPlaybackState adPlaybackState; + private boolean isPlaceholder; + + /** + * Creates the builder. + * + * @param uid The unique identifier of the period within its playlist item. + */ + public Builder(Object uid) { + this.uid = uid; + this.durationUs = 0; + this.adPlaybackState = AdPlaybackState.NONE; + this.isPlaceholder = false; + } + + private Builder(PeriodData periodData) { + this.uid = periodData.uid; + this.durationUs = periodData.durationUs; + this.adPlaybackState = periodData.adPlaybackState; + this.isPlaceholder = periodData.isPlaceholder; + } + + /** + * Sets the unique identifier of the period within its playlist item. + * + * @param uid The unique identifier of the period within its playlist item. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setUid(Object uid) { + this.uid = uid; + return this; + } - /** The available {@link Commands}. */ - public final Commands availableCommands; - /** Whether playback should proceed when ready and not suppressed. */ - public final boolean playWhenReady; - /** The last reason for changing {@link #playWhenReady}. */ - public final @PlayWhenReadyChangeReason int playWhenReadyChangeReason; + /** + * Sets the total duration of the period, in microseconds, or {@link C#TIME_UNSET} if unknown. + * + *

    Only the last period in a playlist item can have an unknown duration. + * + * @param durationUs The total duration of the period, in microseconds, or {@link + * C#TIME_UNSET} if unknown. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setDurationUs(long durationUs) { + checkArgument(durationUs == C.TIME_UNSET || durationUs >= 0); + this.durationUs = durationUs; + return this; + } - private State(Builder builder) { - this.availableCommands = builder.availableCommands; - this.playWhenReady = builder.playWhenReady; - this.playWhenReadyChangeReason = builder.playWhenReadyChangeReason; + /** + * Sets the {@link AdPlaybackState}. + * + * @param adPlaybackState The {@link AdPlaybackState}, or {@link AdPlaybackState#NONE} if + * there are no ads. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setAdPlaybackState(AdPlaybackState adPlaybackState) { + this.adPlaybackState = adPlaybackState; + return this; + } + + /** + * Sets whether this period contains placeholder information because the real information has + * yet to be loaded + * + * @param isPlaceholder Whether this period contains placeholder information because the real + * information has yet to be loaded. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setIsPlaceholder(boolean isPlaceholder) { + this.isPlaceholder = isPlaceholder; + return this; + } + + /** Builds the {@link PeriodData}. */ + public PeriodData build() { + return new PeriodData(this); + } } - /** Returns a {@link Builder} pre-populated with the current state values. */ + /** The unique identifier of the period within its playlist item. */ + public final Object uid; + /** + * The total duration of the period, in microseconds, or {@link C#TIME_UNSET} if unknown. Only + * the last period in a playlist item can have an unknown duration. + */ + public final long durationUs; + /** + * The {@link AdPlaybackState} of the period, or {@link AdPlaybackState#NONE} if there are no + * ads. + */ + public final AdPlaybackState adPlaybackState; + /** + * Whether this period contains placeholder information because the real information has yet to + * be loaded. + */ + public final boolean isPlaceholder; + + private PeriodData(Builder builder) { + this.uid = builder.uid; + this.durationUs = builder.durationUs; + this.adPlaybackState = builder.adPlaybackState; + this.isPlaceholder = builder.isPlaceholder; + } + + /** Returns a {@link Builder} pre-populated with the current values. */ public Builder buildUpon() { return new Builder(this); } @@ -161,29 +1910,71 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof State)) { + if (!(o instanceof PeriodData)) { return false; } - State state = (State) o; - return playWhenReady == state.playWhenReady - && playWhenReadyChangeReason == state.playWhenReadyChangeReason - && availableCommands.equals(state.availableCommands); + PeriodData periodData = (PeriodData) o; + return this.uid.equals(periodData.uid) + && this.durationUs == periodData.durationUs + && this.adPlaybackState.equals(periodData.adPlaybackState) + && this.isPlaceholder == periodData.isPlaceholder; } @Override public int hashCode() { int result = 7; - result = 31 * result + availableCommands.hashCode(); - result = 31 * result + (playWhenReady ? 1 : 0); - result = 31 * result + playWhenReadyChangeReason; + result = 31 * result + uid.hashCode(); + result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); + result = 31 * result + adPlaybackState.hashCode(); + result = 31 * result + (isPlaceholder ? 1 : 0); return result; } } + /** A supplier for a position. */ + protected interface PositionSupplier { + + /** An instance returning a constant position of zero. */ + PositionSupplier ZERO = getConstant(/* positionMs= */ 0); + + /** + * Returns an instance that returns a constant value. + * + * @param positionMs The constant position to return, in milliseconds. + */ + static PositionSupplier getConstant(long positionMs) { + return () -> positionMs; + } + + /** + * Returns an instance that extrapolates the provided position into the future. + * + * @param currentPositionMs The current position in milliseconds. + * @param playbackSpeed The playback speed with which the position is assumed to increase. + */ + static PositionSupplier getExtrapolating(long currentPositionMs, float playbackSpeed) { + long startTimeMs = SystemClock.elapsedRealtime(); + return () -> { + long currentTimeMs = SystemClock.elapsedRealtime(); + return currentPositionMs + (long) ((currentTimeMs - startTimeMs) * playbackSpeed); + }; + } + + /** Returns the position. */ + long get(); + } + + /** + * Position difference threshold below which we do not automatically report a position + * discontinuity, in milliseconds. + */ + private static final long POSITION_DISCONTINUITY_THRESHOLD_MS = 1000; + private final ListenerSet listeners; private final Looper applicationLooper; private final HandlerWrapper applicationHandler; private final HashSet> pendingOperations; + private final Timeline.Period period; private @MonotonicNonNull State state; @@ -208,6 +1999,7 @@ protected SimpleBasePlayer(Looper applicationLooper, Clock clock) { this.applicationLooper = applicationLooper; applicationHandler = clock.createHandler(applicationLooper, /* callback= */ null); pendingOperations = new HashSet<>(); + period = new Timeline.Period(); @SuppressWarnings("nullness:argument.type.incompatible") // Using this in constructor. ListenerSet listenerSet = new ListenerSet<>( @@ -302,34 +2094,36 @@ public final void prepare() { } @Override + @Player.State public final int getPlaybackState() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.playbackState; } @Override public final int getPlaybackSuppressionReason() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.playbackSuppressionReason; } @Nullable @Override public final PlaybackException getPlayerError() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.playerError; } @Override - public final void setRepeatMode(int repeatMode) { + public final void setRepeatMode(@Player.RepeatMode int repeatMode) { // TODO: implement. throw new IllegalStateException(); } @Override + @Player.RepeatMode public final int getRepeatMode() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.repeatMode; } @Override @@ -340,14 +2134,14 @@ public final void setShuffleModeEnabled(boolean shuffleModeEnabled) { @Override public final boolean getShuffleModeEnabled() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.shuffleModeEnabled; } @Override public final boolean isLoading() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.isLoading; } @Override @@ -358,20 +2152,20 @@ public final void seekTo(int mediaItemIndex, long positionMs) { @Override public final long getSeekBackIncrement() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.seekBackIncrementMs; } @Override public final long getSeekForwardIncrement() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.seekForwardIncrementMs; } @Override public final long getMaxSeekToPreviousPosition() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.maxSeekToPreviousPositionMs; } @Override @@ -382,8 +2176,8 @@ public final void setPlaybackParameters(PlaybackParameters playbackParameters) { @Override public final PlaybackParameters getPlaybackParameters() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.playbackParameters; } @Override @@ -406,14 +2200,14 @@ public final void release() { @Override public final Tracks getCurrentTracks() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return getCurrentTracksInternal(state); } @Override public final TrackSelectionParameters getTrackSelectionParameters() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.trackSelectionParameters; } @Override @@ -424,14 +2218,14 @@ public final void setTrackSelectionParameters(TrackSelectionParameters parameter @Override public final MediaMetadata getMediaMetadata() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return getMediaMetadataInternal(state); } @Override public final MediaMetadata getPlaylistMetadata() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.playlistMetadata; } @Override @@ -442,80 +2236,89 @@ public final void setPlaylistMetadata(MediaMetadata mediaMetadata) { @Override public final Timeline getCurrentTimeline() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.timeline; } @Override public final int getCurrentPeriodIndex() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return getCurrentPeriodIndexInternal(state, window); } @Override public final int getCurrentMediaItemIndex() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.currentMediaItemIndex; } @Override public final long getDuration() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + if (isPlayingAd()) { + state.timeline.getPeriod(getCurrentPeriodIndex(), period); + long adDurationUs = + period.getAdDurationUs(state.currentAdGroupIndex, state.currentAdIndexInAdGroup); + return Util.usToMs(adDurationUs); + } + return getContentDuration(); } @Override public final long getCurrentPosition() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return isPlayingAd() ? state.adPositionMsSupplier.get() : getContentPosition(); } @Override public final long getBufferedPosition() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return isPlayingAd() + ? max(state.adBufferedPositionMsSupplier.get(), state.adPositionMsSupplier.get()) + : getContentBufferedPosition(); } @Override public final long getTotalBufferedDuration() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.totalBufferedDurationMsSupplier.get(); } @Override public final boolean isPlayingAd() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.currentAdGroupIndex != C.INDEX_UNSET; } @Override public final int getCurrentAdGroupIndex() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.currentAdGroupIndex; } @Override public final int getCurrentAdIndexInAdGroup() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.currentAdIndexInAdGroup; } @Override public final long getContentPosition() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.contentPositionMsSupplier.get(); } @Override public final long getContentBufferedPosition() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return max( + state.contentBufferedPositionMsSupplier.get(), state.contentPositionMsSupplier.get()); } @Override public final AudioAttributes getAudioAttributes() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.audioAttributes; } @Override @@ -526,8 +2329,8 @@ public final void setVolume(float volume) { @Override public final float getVolume() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.volume; } @Override @@ -586,38 +2389,38 @@ public final void clearVideoTextureView(@Nullable TextureView textureView) { @Override public final VideoSize getVideoSize() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.videoSize; } @Override public final Size getSurfaceSize() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.surfaceSize; } @Override public final CueGroup getCurrentCues() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.currentCues; } @Override public final DeviceInfo getDeviceInfo() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.deviceInfo; } @Override public final int getDeviceVolume() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.deviceVolume; } @Override public final boolean isDeviceMuted() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.isDeviceMuted; } @Override @@ -720,11 +2523,95 @@ private void updateStateAndInformListeners(State newState) { this.state = newState; boolean playWhenReadyChanged = previousState.playWhenReady != newState.playWhenReady; - if (playWhenReadyChanged /* TODO: || playbackStateChanged */) { + boolean playbackStateChanged = previousState.playbackState != newState.playbackState; + Tracks previousTracks = getCurrentTracksInternal(previousState); + Tracks newTracks = getCurrentTracksInternal(newState); + MediaMetadata previousMediaMetadata = getMediaMetadataInternal(previousState); + MediaMetadata newMediaMetadata = getMediaMetadataInternal(newState); + int positionDiscontinuityReason = + getPositionDiscontinuityReason(previousState, newState, window, period); + boolean timelineChanged = !previousState.timeline.equals(newState.timeline); + int mediaItemTransitionReason = + getMediaItemTransitionReason(previousState, newState, positionDiscontinuityReason, window); + + if (timelineChanged) { + @Player.TimelineChangeReason + int timelineChangeReason = + getTimelineChangeReason(previousState.playlistItems, newState.playlistItems); + listeners.queueEvent( + Player.EVENT_TIMELINE_CHANGED, + listener -> listener.onTimelineChanged(newState.timeline, timelineChangeReason)); + } + if (positionDiscontinuityReason != C.INDEX_UNSET) { + PositionInfo previousPositionInfo = + getPositionInfo(previousState, /* useDiscontinuityPosition= */ false, window, period); + PositionInfo positionInfo = + getPositionInfo( + newState, + /* useDiscontinuityPosition= */ state.hasPositionDiscontinuity, + window, + period); + listeners.queueEvent( + Player.EVENT_POSITION_DISCONTINUITY, + listener -> { + listener.onPositionDiscontinuity(positionDiscontinuityReason); + listener.onPositionDiscontinuity( + previousPositionInfo, positionInfo, positionDiscontinuityReason); + }); + } + if (mediaItemTransitionReason != C.INDEX_UNSET) { + @Nullable + MediaItem mediaItem = + state.timeline.isEmpty() + ? null + : state.playlistItems.get(state.currentMediaItemIndex).mediaItem; + listeners.queueEvent( + Player.EVENT_MEDIA_ITEM_TRANSITION, + listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason)); + } + if (!Util.areEqual(previousState.playerError, newState.playerError)) { + listeners.queueEvent( + Player.EVENT_PLAYER_ERROR, + listener -> listener.onPlayerErrorChanged(newState.playerError)); + if (newState.playerError != null) { + listeners.queueEvent( + Player.EVENT_PLAYER_ERROR, + listener -> listener.onPlayerError(castNonNull(newState.playerError))); + } + } + if (!previousState.trackSelectionParameters.equals(newState.trackSelectionParameters)) { + listeners.queueEvent( + Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, + listener -> + listener.onTrackSelectionParametersChanged(newState.trackSelectionParameters)); + } + if (!previousTracks.equals(newTracks)) { + listeners.queueEvent( + Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(newTracks)); + } + if (!previousMediaMetadata.equals(newMediaMetadata)) { + listeners.queueEvent( + EVENT_MEDIA_METADATA_CHANGED, + listener -> listener.onMediaMetadataChanged(newMediaMetadata)); + } + if (previousState.isLoading != newState.isLoading) { + listeners.queueEvent( + Player.EVENT_IS_LOADING_CHANGED, + listener -> { + listener.onLoadingChanged(newState.isLoading); + listener.onIsLoadingChanged(newState.isLoading); + }); + } + if (playWhenReadyChanged || playbackStateChanged) { listeners.queueEvent( /* eventFlag= */ C.INDEX_UNSET, listener -> - listener.onPlayerStateChanged(newState.playWhenReady, /* TODO */ Player.STATE_IDLE)); + listener.onPlayerStateChanged(newState.playWhenReady, newState.playbackState)); + } + if (playbackStateChanged) { + listeners.queueEvent( + Player.EVENT_PLAYBACK_STATE_CHANGED, + listener -> listener.onPlaybackStateChanged(newState.playbackState)); } if (playWhenReadyChanged || previousState.playWhenReadyChangeReason != newState.playWhenReadyChangeReason) { @@ -734,11 +2621,115 @@ private void updateStateAndInformListeners(State newState) { listener.onPlayWhenReadyChanged( newState.playWhenReady, newState.playWhenReadyChangeReason)); } + if (previousState.playbackSuppressionReason != newState.playbackSuppressionReason) { + listeners.queueEvent( + Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, + listener -> + listener.onPlaybackSuppressionReasonChanged(newState.playbackSuppressionReason)); + } if (isPlaying(previousState) != isPlaying(newState)) { listeners.queueEvent( Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying(newState))); } + if (!previousState.playbackParameters.equals(newState.playbackParameters)) { + listeners.queueEvent( + Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, + listener -> listener.onPlaybackParametersChanged(newState.playbackParameters)); + } + if (previousState.skipSilenceEnabled != newState.skipSilenceEnabled) { + listeners.queueEvent( + Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED, + listener -> listener.onSkipSilenceEnabledChanged(newState.skipSilenceEnabled)); + } + if (previousState.repeatMode != newState.repeatMode) { + listeners.queueEvent( + Player.EVENT_REPEAT_MODE_CHANGED, + listener -> listener.onRepeatModeChanged(newState.repeatMode)); + } + if (previousState.shuffleModeEnabled != newState.shuffleModeEnabled) { + listeners.queueEvent( + Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, + listener -> listener.onShuffleModeEnabledChanged(newState.shuffleModeEnabled)); + } + if (previousState.seekBackIncrementMs != newState.seekBackIncrementMs) { + listeners.queueEvent( + Player.EVENT_SEEK_BACK_INCREMENT_CHANGED, + listener -> listener.onSeekBackIncrementChanged(newState.seekBackIncrementMs)); + } + if (previousState.seekForwardIncrementMs != newState.seekForwardIncrementMs) { + listeners.queueEvent( + Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED, + listener -> listener.onSeekForwardIncrementChanged(newState.seekForwardIncrementMs)); + } + if (previousState.maxSeekToPreviousPositionMs != newState.maxSeekToPreviousPositionMs) { + listeners.queueEvent( + Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED, + listener -> + listener.onMaxSeekToPreviousPositionChanged(newState.maxSeekToPreviousPositionMs)); + } + if (!previousState.audioAttributes.equals(newState.audioAttributes)) { + listeners.queueEvent( + Player.EVENT_AUDIO_ATTRIBUTES_CHANGED, + listener -> listener.onAudioAttributesChanged(newState.audioAttributes)); + } + if (!previousState.videoSize.equals(newState.videoSize)) { + listeners.queueEvent( + Player.EVENT_VIDEO_SIZE_CHANGED, + listener -> listener.onVideoSizeChanged(newState.videoSize)); + } + if (!previousState.deviceInfo.equals(newState.deviceInfo)) { + listeners.queueEvent( + Player.EVENT_DEVICE_INFO_CHANGED, + listener -> listener.onDeviceInfoChanged(newState.deviceInfo)); + } + if (!previousState.playlistMetadata.equals(newState.playlistMetadata)) { + listeners.queueEvent( + Player.EVENT_PLAYLIST_METADATA_CHANGED, + listener -> listener.onPlaylistMetadataChanged(newState.playlistMetadata)); + } + if (previousState.audioSessionId != newState.audioSessionId) { + listeners.queueEvent( + Player.EVENT_AUDIO_SESSION_ID, + listener -> listener.onAudioSessionIdChanged(newState.audioSessionId)); + } + if (newState.newlyRenderedFirstFrame) { + listeners.queueEvent(Player.EVENT_RENDERED_FIRST_FRAME, Listener::onRenderedFirstFrame); + } + if (!previousState.surfaceSize.equals(newState.surfaceSize)) { + listeners.queueEvent( + Player.EVENT_SURFACE_SIZE_CHANGED, + listener -> + listener.onSurfaceSizeChanged( + newState.surfaceSize.getWidth(), newState.surfaceSize.getHeight())); + } + if (previousState.volume != newState.volume) { + listeners.queueEvent( + Player.EVENT_VOLUME_CHANGED, listener -> listener.onVolumeChanged(newState.volume)); + } + if (previousState.deviceVolume != newState.deviceVolume + || previousState.isDeviceMuted != newState.isDeviceMuted) { + listeners.queueEvent( + Player.EVENT_DEVICE_VOLUME_CHANGED, + listener -> + listener.onDeviceVolumeChanged(newState.deviceVolume, newState.isDeviceMuted)); + } + if (!previousState.currentCues.equals(newState.currentCues)) { + listeners.queueEvent( + Player.EVENT_CUES, + listener -> { + listener.onCues(newState.currentCues.cues); + listener.onCues(newState.currentCues); + }); + } + if (!previousState.timedMetadata.equals(newState.timedMetadata) + && newState.timedMetadata.presentationTimeUs != C.TIME_UNSET) { + listeners.queueEvent( + Player.EVENT_METADATA, listener -> listener.onMetadata(newState.timedMetadata)); + } + if (false /* TODO: add flag to know when a seek request has been resolved */) { + listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onSeekProcessed); + } if (!previousState.availableCommands.equals(newState.availableCommands)) { listeners.queueEvent( Player.EVENT_AVAILABLE_COMMANDS_CHANGED, @@ -776,7 +2767,7 @@ private void updateStateForPendingOperation( updateStateAndInformListeners(getPlaceholderState(suggestedPlaceholderState)); pendingOperation.addListener( () -> { - castNonNull(state); // Already check by method @RequiresNonNull pre-condition. + castNonNull(state); // Already checked by method @RequiresNonNull pre-condition. pendingOperations.remove(pendingOperation); if (pendingOperations.isEmpty()) { updateStateAndInformListeners(getState()); @@ -795,8 +2786,191 @@ private void postOrRunOnApplicationHandler(Runnable runnable) { } private static boolean isPlaying(State state) { - return state.playWhenReady && false; - // TODO: && state.playbackState == Player.STATE_READY - // && state.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE + return state.playWhenReady + && state.playbackState == Player.STATE_READY + && state.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; + } + + private static Tracks getCurrentTracksInternal(State state) { + return state.playlistItems.isEmpty() + ? Tracks.EMPTY + : state.playlistItems.get(state.currentMediaItemIndex).tracks; + } + + private static MediaMetadata getMediaMetadataInternal(State state) { + return state.playlistItems.isEmpty() + ? MediaMetadata.EMPTY + : state.playlistItems.get(state.currentMediaItemIndex).combinedMediaMetadata; + } + + private static int getCurrentPeriodIndexInternal(State state, Timeline.Window window) { + if (state.currentPeriodIndex != C.INDEX_UNSET) { + return state.currentPeriodIndex; + } + if (state.timeline.isEmpty()) { + return state.currentMediaItemIndex; + } + return state.timeline.getWindow(state.currentMediaItemIndex, window).firstPeriodIndex; + } + + private static @Player.TimelineChangeReason int getTimelineChangeReason( + List previousPlaylist, List newPlaylist) { + if (previousPlaylist.size() != newPlaylist.size()) { + return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; + } + for (int i = 0; i < previousPlaylist.size(); i++) { + if (!previousPlaylist.get(i).uid.equals(newPlaylist.get(i).uid)) { + return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; + } + } + return Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE; + } + + private static int getPositionDiscontinuityReason( + State previousState, State newState, Timeline.Window window, Timeline.Period period) { + if (newState.hasPositionDiscontinuity) { + // We were asked to report a discontinuity. + return newState.positionDiscontinuityReason; + } + if (previousState.playlistItems.isEmpty()) { + // First change from an empty timeline is not reported as a discontinuity. + return C.INDEX_UNSET; + } + if (newState.playlistItems.isEmpty()) { + // The playlist became empty. + return Player.DISCONTINUITY_REASON_REMOVE; + } + Object previousPeriodUid = + previousState.timeline.getUidOfPeriod(getCurrentPeriodIndexInternal(previousState, window)); + Object newPeriodUid = + newState.timeline.getUidOfPeriod(getCurrentPeriodIndexInternal(newState, window)); + if (!newPeriodUid.equals(previousPeriodUid) + || previousState.currentAdGroupIndex != newState.currentAdGroupIndex + || previousState.currentAdIndexInAdGroup != newState.currentAdIndexInAdGroup) { + // The current period or ad inside a period changed. + if (newState.timeline.getIndexOfPeriod(previousPeriodUid) == C.INDEX_UNSET) { + // The previous period no longer exists. + return Player.DISCONTINUITY_REASON_REMOVE; + } + // Check if reached the previous period's or ad's duration to assume an auto-transition. + long previousPositionMs = + getCurrentPeriodOrAdPositionMs(previousState, previousPeriodUid, period); + long previousDurationMs = getPeriodOrAdDurationMs(previousState, previousPeriodUid, period); + return previousDurationMs != C.TIME_UNSET && previousPositionMs >= previousDurationMs + ? Player.DISCONTINUITY_REASON_AUTO_TRANSITION + : Player.DISCONTINUITY_REASON_SKIP; + } + // We are in the same content period or ad. Check if the position deviates more than a + // reasonable threshold from the previous one. + long previousPositionMs = + getCurrentPeriodOrAdPositionMs(previousState, previousPeriodUid, period); + long newPositionMs = getCurrentPeriodOrAdPositionMs(newState, newPeriodUid, period); + if (Math.abs(previousPositionMs - newPositionMs) < POSITION_DISCONTINUITY_THRESHOLD_MS) { + return C.INDEX_UNSET; + } + // Check if we previously reached the end of the item to assume an auto-repetition. + long previousDurationMs = getPeriodOrAdDurationMs(previousState, previousPeriodUid, period); + return previousDurationMs != C.TIME_UNSET && previousPositionMs >= previousDurationMs + ? Player.DISCONTINUITY_REASON_AUTO_TRANSITION + : Player.DISCONTINUITY_REASON_INTERNAL; + } + + private static long getCurrentPeriodOrAdPositionMs( + State state, Object currentPeriodUid, Timeline.Period period) { + return state.currentAdGroupIndex != C.INDEX_UNSET + ? state.adPositionMsSupplier.get() + : state.contentPositionMsSupplier.get() + - state.timeline.getPeriodByUid(currentPeriodUid, period).getPositionInWindowMs(); + } + + private static long getPeriodOrAdDurationMs( + State state, Object currentPeriodUid, Timeline.Period period) { + state.timeline.getPeriodByUid(currentPeriodUid, period); + long periodOrAdDurationUs = + state.currentAdGroupIndex == C.INDEX_UNSET + ? period.durationUs + : period.getAdDurationUs(state.currentAdGroupIndex, state.currentAdIndexInAdGroup); + return usToMs(periodOrAdDurationUs); + } + + private static PositionInfo getPositionInfo( + State state, + boolean useDiscontinuityPosition, + Timeline.Window window, + Timeline.Period period) { + @Nullable Object windowUid = null; + @Nullable Object periodUid = null; + int mediaItemIndex = state.currentMediaItemIndex; + int periodIndex = C.INDEX_UNSET; + @Nullable MediaItem mediaItem = null; + if (!state.timeline.isEmpty()) { + periodIndex = getCurrentPeriodIndexInternal(state, window); + periodUid = state.timeline.getPeriod(periodIndex, period, /* setIds= */ true).uid; + windowUid = state.timeline.getWindow(mediaItemIndex, window).uid; + mediaItem = window.mediaItem; + } + long contentPositionMs; + long positionMs; + if (useDiscontinuityPosition) { + positionMs = state.discontinuityPositionMs; + contentPositionMs = + state.currentAdGroupIndex == C.INDEX_UNSET + ? positionMs + : state.contentPositionMsSupplier.get(); + } else { + contentPositionMs = state.contentPositionMsSupplier.get(); + positionMs = + state.currentAdGroupIndex != C.INDEX_UNSET + ? state.adPositionMsSupplier.get() + : contentPositionMs; + } + return new PositionInfo( + windowUid, + mediaItemIndex, + mediaItem, + periodUid, + periodIndex, + positionMs, + contentPositionMs, + state.currentAdGroupIndex, + state.currentAdIndexInAdGroup); + } + + private static int getMediaItemTransitionReason( + State previousState, + State newState, + int positionDiscontinuityReason, + Timeline.Window window) { + Timeline previousTimeline = previousState.timeline; + Timeline newTimeline = newState.timeline; + if (newTimeline.isEmpty() && previousTimeline.isEmpty()) { + return C.INDEX_UNSET; + } else if (newTimeline.isEmpty() != previousTimeline.isEmpty()) { + return MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED; + } + Object previousWindowUid = + previousState.timeline.getWindow(previousState.currentMediaItemIndex, window).uid; + Object newWindowUid = newState.timeline.getWindow(newState.currentMediaItemIndex, window).uid; + if (!previousWindowUid.equals(newWindowUid)) { + if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) { + return MEDIA_ITEM_TRANSITION_REASON_AUTO; + } else if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK) { + return MEDIA_ITEM_TRANSITION_REASON_SEEK; + } else { + return MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED; + } + } + // Only mark changes within the current item as a transition if we are repeating automatically + // or via a seek to next/previous. + if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION + && previousState.contentPositionMsSupplier.get() + > newState.contentPositionMsSupplier.get()) { + return MEDIA_ITEM_TRANSITION_REASON_REPEAT; + } + if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK + && /* TODO: mark repetition seeks to detect this case */ false) { + return MEDIA_ITEM_TRANSITION_REASON_SEEK; + } + return C.INDEX_UNSET; } } diff --git a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java index 1b13cb00fc7..aef72f644fa 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -16,15 +16,28 @@ package androidx.media3.common; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import android.os.Looper; +import android.os.SystemClock; import androidx.media3.common.Player.Commands; import androidx.media3.common.Player.Listener; import androidx.media3.common.SimpleBasePlayer.State; +import androidx.media3.common.text.Cue; +import androidx.media3.common.text.CueGroup; +import androidx.media3.common.util.Size; +import androidx.media3.test.utils.FakeMetadataEntry; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; @@ -35,6 +48,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowLooper; /** Unit test for {@link SimpleBasePlayer}. */ @RunWith(AndroidJUnit4.class) @@ -61,6 +75,64 @@ public void stateBuildUpon_build_isEqual() { /* playWhenReady= */ true, /* playWhenReadyChangeReason= */ Player .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) + .setPlaybackState(Player.STATE_IDLE) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .setPlayerError( + new PlaybackException( + /* message= */ null, + /* cause= */ null, + PlaybackException.ERROR_CODE_DECODING_FAILED)) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(true) + .setIsLoading(false) + .setSeekBackIncrementMs(5000) + .setSeekForwardIncrementMs(4000) + .setMaxSeekToPreviousPositionMs(3000) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)) + .setTrackSelectionParameters(TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT) + .setAudioAttributes( + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build()) + .setVolume(0.5f) + .setVideoSize(new VideoSize(/* width= */ 200, /* height= */ 400)) + .setCurrentCues( + new CueGroup( + ImmutableList.of(new Cue.Builder().setText("text").build()), + /* presentationTimeUs= */ 123)) + .setDeviceInfo( + new DeviceInfo( + DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 3, /* maxVolume= */ 7)) + .setIsDeviceMuted(true) + .setAudioSessionId(78) + .setSkipSilenceEnabled(true) + .setSurfaceSize(new Size(480, 360)) + .setNewlyRenderedFirstFrame(true) + .setTimedMetadata(new Metadata()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), + /* adGroupTimesUs= */ 555, + 666)) + .build())) + .build())) + .setPlaylistMetadata(new MediaMetadata.Builder().setArtist("artist").build()) + .setCurrentMediaItemIndex(1) + .setCurrentPeriodIndex(1) + .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2) + .setContentPositionMs(() -> 456) + .setAdPositionMs(() -> 6678) + .setContentBufferedPositionMs(() -> 999) + .setAdBufferedPositionMs(() -> 888) + .setTotalBufferedDurationMs(() -> 567) + .setPositionDiscontinuity( + Player.DISCONTINUITY_REASON_SEEK, /* discontinuityPositionMs= */ 400) .build(); State newState = state.buildUpon().build(); @@ -70,29 +142,622 @@ public void stateBuildUpon_build_isEqual() { } @Test - public void stateBuilderSetAvailableCommands_setsAvailableCommands() { - Commands commands = - new Commands.Builder() - .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE) + public void playlistItemBuildUpon_build_isEqual() { + SimpleBasePlayer.PlaylistItem playlistItem = + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setTracks( + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true})))) + .setMediaItem(new MediaItem.Builder().setMediaId("id").build()) + .setMediaMetadata(new MediaMetadata.Builder().setTitle("title").build()) + .setManifest(new Object()) + .setLiveConfiguration( + new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build()) + .setPresentationStartTimeMs(12) + .setWindowStartTimeMs(23) + .setElapsedRealtimeEpochOffsetMs(10234) + .setIsSeekable(true) + .setIsDynamic(true) + .setDefaultPositionUs(456_789) + .setDurationUs(500_000) + .setPositionInFirstPeriodUs(100_000) + .setIsPlaceholder(true) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()).build())) .build(); - State state = new State.Builder().setAvailableCommands(commands).build(); - assertThat(state.availableCommands).isEqualTo(commands); + SimpleBasePlayer.PlaylistItem newPlaylistItem = playlistItem.buildUpon().build(); + + assertThat(newPlaylistItem).isEqualTo(playlistItem); + assertThat(newPlaylistItem.hashCode()).isEqualTo(playlistItem.hashCode()); + } + + @Test + public void periodDataBuildUpon_build_isEqual() { + SimpleBasePlayer.PeriodData periodData = + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setIsPlaceholder(true) + .setDurationUs(600_000) + .setAdPlaybackState( + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666)) + .build(); + + SimpleBasePlayer.PeriodData newPeriodData = periodData.buildUpon().build(); + + assertThat(newPeriodData).isEqualTo(periodData); + assertThat(newPeriodData.hashCode()).isEqualTo(periodData.hashCode()); } @Test - public void stateBuilderSetPlayWhenReady_setsStatePlayWhenReadyAndReason() { + public void stateBuilderBuild_setsCorrectValues() { + Commands commands = + new Commands.Builder() + .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE) + .build(); + PlaybackException error = + new PlaybackException( + /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_DECODING_FAILED); + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 2f); + TrackSelectionParameters trackSelectionParameters = + TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT + .buildUpon() + .setMaxVideoBitrate(1000) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(); + VideoSize videoSize = new VideoSize(/* width= */ 200, /* height= */ 400); + CueGroup cueGroup = + new CueGroup( + ImmutableList.of(new Cue.Builder().setText("text").build()), + /* presentationTimeUs= */ 123); + Metadata timedMetadata = new Metadata(new FakeMetadataEntry("data")); + Size surfaceSize = new Size(480, 360); + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 3, /* maxVolume= */ 7); + ImmutableList playlist = + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666)) + .build())) + .build()); + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build(); + SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456; + SimpleBasePlayer.PositionSupplier adPositionSupplier = () -> 6678; + SimpleBasePlayer.PositionSupplier contentBufferedPositionSupplier = () -> 999; + SimpleBasePlayer.PositionSupplier adBufferedPositionSupplier = () -> 888; + SimpleBasePlayer.PositionSupplier totalBufferedPositionSupplier = () -> 567; + State state = new State.Builder() + .setAvailableCommands(commands) .setPlayWhenReady( /* playWhenReady= */ true, /* playWhenReadyChangeReason= */ Player .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) + .setPlaybackState(Player.STATE_IDLE) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .setPlayerError(error) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(true) + .setIsLoading(false) + .setSeekBackIncrementMs(5000) + .setSeekForwardIncrementMs(4000) + .setMaxSeekToPreviousPositionMs(3000) + .setPlaybackParameters(playbackParameters) + .setTrackSelectionParameters(trackSelectionParameters) + .setAudioAttributes(audioAttributes) + .setVolume(0.5f) + .setVideoSize(videoSize) + .setCurrentCues(cueGroup) + .setDeviceInfo(deviceInfo) + .setDeviceVolume(5) + .setIsDeviceMuted(true) + .setAudioSessionId(78) + .setSkipSilenceEnabled(true) + .setSurfaceSize(surfaceSize) + .setNewlyRenderedFirstFrame(true) + .setTimedMetadata(timedMetadata) + .setPlaylist(playlist) + .setPlaylistMetadata(playlistMetadata) + .setCurrentMediaItemIndex(1) + .setCurrentPeriodIndex(1) + .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2) + .setContentPositionMs(contentPositionSupplier) + .setAdPositionMs(adPositionSupplier) + .setContentBufferedPositionMs(contentBufferedPositionSupplier) + .setAdBufferedPositionMs(adBufferedPositionSupplier) + .setTotalBufferedDurationMs(totalBufferedPositionSupplier) + .setPositionDiscontinuity( + Player.DISCONTINUITY_REASON_SEEK, /* discontinuityPositionMs= */ 400) .build(); + assertThat(state.availableCommands).isEqualTo(commands); assertThat(state.playWhenReady).isTrue(); assertThat(state.playWhenReadyChangeReason) .isEqualTo(Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS); + assertThat(state.playbackState).isEqualTo(Player.STATE_IDLE); + assertThat(state.playbackSuppressionReason) + .isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + assertThat(state.playerError).isEqualTo(error); + assertThat(state.repeatMode).isEqualTo(Player.REPEAT_MODE_ALL); + assertThat(state.shuffleModeEnabled).isTrue(); + assertThat(state.isLoading).isFalse(); + assertThat(state.seekBackIncrementMs).isEqualTo(5000); + assertThat(state.seekForwardIncrementMs).isEqualTo(4000); + assertThat(state.maxSeekToPreviousPositionMs).isEqualTo(3000); + assertThat(state.playbackParameters).isEqualTo(playbackParameters); + assertThat(state.trackSelectionParameters).isEqualTo(trackSelectionParameters); + assertThat(state.audioAttributes).isEqualTo(audioAttributes); + assertThat(state.volume).isEqualTo(0.5f); + assertThat(state.videoSize).isEqualTo(videoSize); + assertThat(state.currentCues).isEqualTo(cueGroup); + assertThat(state.deviceInfo).isEqualTo(deviceInfo); + assertThat(state.deviceVolume).isEqualTo(5); + assertThat(state.isDeviceMuted).isTrue(); + assertThat(state.audioSessionId).isEqualTo(78); + assertThat(state.skipSilenceEnabled).isTrue(); + assertThat(state.surfaceSize).isEqualTo(surfaceSize); + assertThat(state.newlyRenderedFirstFrame).isTrue(); + assertThat(state.timedMetadata).isEqualTo(timedMetadata); + assertThat(state.playlistItems).isEqualTo(playlist); + assertThat(state.playlistMetadata).isEqualTo(playlistMetadata); + assertThat(state.currentMediaItemIndex).isEqualTo(1); + assertThat(state.currentPeriodIndex).isEqualTo(1); + assertThat(state.currentAdGroupIndex).isEqualTo(1); + assertThat(state.currentAdIndexInAdGroup).isEqualTo(2); + assertThat(state.contentPositionMsSupplier).isEqualTo(contentPositionSupplier); + assertThat(state.adPositionMsSupplier).isEqualTo(adPositionSupplier); + assertThat(state.contentBufferedPositionMsSupplier).isEqualTo(contentBufferedPositionSupplier); + assertThat(state.adBufferedPositionMsSupplier).isEqualTo(adBufferedPositionSupplier); + assertThat(state.totalBufferedDurationMsSupplier).isEqualTo(totalBufferedPositionSupplier); + assertThat(state.hasPositionDiscontinuity).isTrue(); + assertThat(state.positionDiscontinuityReason).isEqualTo(Player.DISCONTINUITY_REASON_SEEK); + assertThat(state.discontinuityPositionMs).isEqualTo(400); + } + + @Test + public void stateBuilderBuild_emptyTimelineWithReadyState_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist(ImmutableList.of()) + .setPlaybackState(Player.STATE_READY) + .build()); + } + + @Test + public void stateBuilderBuild_emptyTimelineWithBufferingState_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist(ImmutableList.of()) + .setPlaybackState(Player.STATE_BUFFERING) + .build()); + } + + @Test + public void stateBuilderBuild_idleStateWithIsLoading_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaybackState(Player.STATE_IDLE) + .setIsLoading(true) + .build()); + } + + @Test + public void stateBuilderBuild_currentWindowIndexExceedsPlaylistLength_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + .setCurrentMediaItemIndex(2) + .build()); + } + + @Test + public void stateBuilderBuild_currentPeriodIndexExceedsPlaylistLength_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + .setCurrentPeriodIndex(2) + .build()); + } + + @Test + public void stateBuilderBuild_currentPeriodIndexInOtherMediaItem_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + .setCurrentMediaItemIndex(0) + .setCurrentPeriodIndex(1) + .build()); + } + + @Test + public void stateBuilderBuild_currentAdGroupIndexExceedsAdGroupCount_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), + /* adGroupTimesUs= */ 123)) + .build())) + .build())) + .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2) + .build()); + } + + @Test + public void stateBuilderBuild_currentAdIndexExceedsAdCountInAdGroup_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), + /* adGroupTimesUs= */ 123) + .withAdCount( + /* adGroupIndex= */ 0, /* adCount= */ 2)) + .build())) + .build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2) + .build()); + } + + @Test + public void stateBuilderBuild_playerErrorInNonIdleState_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaybackState(Player.STATE_READY) + .setPlayerError( + new PlaybackException( + /* message= */ null, + /* cause= */ null, + PlaybackException.ERROR_CODE_DECODING_FAILED)) + .build()); + } + + @Test + public void stateBuilderBuild_multiplePlaylistItemsWithSameIds_throwsException() { + Object uid = new Object(); + + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(uid).build(), + new SimpleBasePlayer.PlaylistItem.Builder(uid).build())) + .build()); + } + + @Test + public void stateBuilderBuild_adGroupIndexWithUnsetAdIndex_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setCurrentAd(/* adGroupIndex= */ C.INDEX_UNSET, /* adIndexInAdGroup= */ 0)); + } + + @Test + public void stateBuilderBuild_unsetAdGroupIndexWithSetAdIndex_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ C.INDEX_UNSET)); + } + + @Test + public void stateBuilderBuild_unsetAdGroupIndexAndAdIndex_doesNotThrow() { + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setCurrentAd(/* adGroupIndex= */ C.INDEX_UNSET, /* adIndexInAdGroup= */ C.INDEX_UNSET) + .build(); + + assertThat(state.currentAdGroupIndex).isEqualTo(C.INDEX_UNSET); + assertThat(state.currentAdIndexInAdGroup).isEqualTo(C.INDEX_UNSET); + } + + @Test + public void stateBuilderBuild_returnsAdvancingContentPositionWhenPlaying() { + SystemClock.setCurrentTimeMillis(10000); + + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + .setContentPositionMs(4000) + .setPlayWhenReady(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setPlaybackState(Player.STATE_READY) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)) + .build(); + long position1 = state.contentPositionMsSupplier.get(); + SystemClock.setCurrentTimeMillis(12000); + long position2 = state.contentPositionMsSupplier.get(); + + assertThat(position1).isEqualTo(4000); + assertThat(position2).isEqualTo(8000); + } + + @Test + public void stateBuilderBuild_returnsConstantContentPositionWhenNotPlaying() { + SystemClock.setCurrentTimeMillis(10000); + + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + .setContentPositionMs(4000) + .setPlaybackState(Player.STATE_BUFFERING) + .build(); + long position1 = state.contentPositionMsSupplier.get(); + SystemClock.setCurrentTimeMillis(12000); + long position2 = state.contentPositionMsSupplier.get(); + + assertThat(position1).isEqualTo(4000); + assertThat(position2).isEqualTo(4000); + } + + @Test + public void stateBuilderBuild_returnsAdvancingAdPositionWhenPlaying() { + SystemClock.setCurrentTimeMillis(10000); + + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), + /* adGroupTimesUs= */ 123) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2)) + .build())) + .build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1) + .setAdPositionMs(4000) + .setPlayWhenReady(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setPlaybackState(Player.STATE_READY) + // This should be ignored as ads are assumed to be played with unit speed. + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)) + .build(); + long position1 = state.adPositionMsSupplier.get(); + SystemClock.setCurrentTimeMillis(12000); + long position2 = state.adPositionMsSupplier.get(); + + assertThat(position1).isEqualTo(4000); + assertThat(position2).isEqualTo(6000); + } + + @Test + public void stateBuilderBuild_returnsConstantAdPositionWhenNotPlaying() { + SystemClock.setCurrentTimeMillis(10000); + + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), + /* adGroupTimesUs= */ 123) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2)) + .build())) + .build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1) + .setAdPositionMs(4000) + .setPlaybackState(Player.STATE_BUFFERING) + .build(); + long position1 = state.adPositionMsSupplier.get(); + SystemClock.setCurrentTimeMillis(12000); + long position2 = state.adPositionMsSupplier.get(); + + assertThat(position1).isEqualTo(4000); + assertThat(position2).isEqualTo(4000); + } + + @Test + public void playlistItemBuilderBuild_setsCorrectValues() { + Object uid = new Object(); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("id").build(); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Object manifest = new Object(); + MediaItem.LiveConfiguration liveConfiguration = + new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build(); + ImmutableList periods = + ImmutableList.of(new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()).build()); + + SimpleBasePlayer.PlaylistItem playlistItem = + new SimpleBasePlayer.PlaylistItem.Builder(uid) + .setTracks(tracks) + .setMediaItem(mediaItem) + .setMediaMetadata(mediaMetadata) + .setManifest(manifest) + .setLiveConfiguration(liveConfiguration) + .setPresentationStartTimeMs(12) + .setWindowStartTimeMs(23) + .setElapsedRealtimeEpochOffsetMs(10234) + .setIsSeekable(true) + .setIsDynamic(true) + .setDefaultPositionUs(456_789) + .setDurationUs(500_000) + .setPositionInFirstPeriodUs(100_000) + .setIsPlaceholder(true) + .setPeriods(periods) + .build(); + + assertThat(playlistItem.uid).isEqualTo(uid); + assertThat(playlistItem.tracks).isEqualTo(tracks); + assertThat(playlistItem.mediaItem).isEqualTo(mediaItem); + assertThat(playlistItem.mediaMetadata).isEqualTo(mediaMetadata); + assertThat(playlistItem.manifest).isEqualTo(manifest); + assertThat(playlistItem.liveConfiguration).isEqualTo(liveConfiguration); + assertThat(playlistItem.presentationStartTimeMs).isEqualTo(12); + assertThat(playlistItem.windowStartTimeMs).isEqualTo(23); + assertThat(playlistItem.elapsedRealtimeEpochOffsetMs).isEqualTo(10234); + assertThat(playlistItem.isSeekable).isTrue(); + assertThat(playlistItem.isDynamic).isTrue(); + assertThat(playlistItem.defaultPositionUs).isEqualTo(456_789); + assertThat(playlistItem.durationUs).isEqualTo(500_000); + assertThat(playlistItem.positionInFirstPeriodUs).isEqualTo(100_000); + assertThat(playlistItem.isPlaceholder).isTrue(); + assertThat(playlistItem.periods).isEqualTo(periods); + } + + @Test + public void playlistItemBuilderBuild_presentationStartTimeIfNotLive_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPresentationStartTimeMs(12) + .build()); + } + + @Test + public void playlistItemBuilderBuild_windowStartTimeIfNotLive_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setWindowStartTimeMs(12) + .build()); + } + + @Test + public void playlistItemBuilderBuild_elapsedEpochOffsetIfNotLive_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setElapsedRealtimeEpochOffsetMs(12) + .build()); + } + + @Test + public void + playlistItemBuilderBuild_windowStartTimeLessThanPresentationStartTime_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setLiveConfiguration(MediaItem.LiveConfiguration.UNSET) + .setWindowStartTimeMs(12) + .setPresentationStartTimeMs(13) + .build()); + } + + @Test + public void playlistItemBuilderBuild_multiplePeriodsWithSameUid_throwsException() { + Object uid = new Object(); + + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(uid).build(), + new SimpleBasePlayer.PeriodData.Builder(uid).build())) + .build()); + } + + @Test + public void playlistItemBuilderBuild_defaultPositionGreaterThanDuration_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setDefaultPositionUs(16) + .setDurationUs(15) + .build()); + } + + @Test + public void periodDataBuilderBuild_setsCorrectValues() { + Object uid = new Object(); + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666); + + SimpleBasePlayer.PeriodData periodData = + new SimpleBasePlayer.PeriodData.Builder(uid) + .setIsPlaceholder(true) + .setDurationUs(600_000) + .setAdPlaybackState(adPlaybackState) + .build(); + + assertThat(periodData.uid).isEqualTo(uid); + assertThat(periodData.isPlaceholder).isTrue(); + assertThat(periodData.durationUs).isEqualTo(600_000); + assertThat(periodData.adPlaybackState).isEqualTo(adPlaybackState); } @Test @@ -101,6 +766,72 @@ public void getterMethods_noOtherMethodCalls_returnCurrentState() { new Commands.Builder() .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE) .build(); + PlaybackException error = + new PlaybackException( + /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_DECODING_FAILED); + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 2f); + TrackSelectionParameters trackSelectionParameters = + TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT + .buildUpon() + .setMaxVideoBitrate(1000) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(); + VideoSize videoSize = new VideoSize(/* width= */ 200, /* height= */ 400); + CueGroup cueGroup = + new CueGroup( + ImmutableList.of(new Cue.Builder().setText("text").build()), + /* presentationTimeUs= */ 123); + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 3, /* maxVolume= */ 7); + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build(); + SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456; + SimpleBasePlayer.PositionSupplier contentBufferedPositionSupplier = () -> 499; + SimpleBasePlayer.PositionSupplier totalBufferedPositionSupplier = () -> 567; + Object playlistItemUid = new Object(); + Object periodUid = new Object(); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("id").build(); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Object manifest = new Object(); + Size surfaceSize = new Size(480, 360); + MediaItem.LiveConfiguration liveConfiguration = + new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build(); + ImmutableList playlist = + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(playlistItemUid) + .setTracks(tracks) + .setMediaItem(mediaItem) + .setMediaMetadata(mediaMetadata) + .setManifest(manifest) + .setLiveConfiguration(liveConfiguration) + .setPresentationStartTimeMs(12) + .setWindowStartTimeMs(23) + .setElapsedRealtimeEpochOffsetMs(10234) + .setIsSeekable(true) + .setIsDynamic(true) + .setDefaultPositionUs(456_789) + .setDurationUs(500_000) + .setPositionInFirstPeriodUs(100_000) + .setIsPlaceholder(true) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(periodUid) + .setIsPlaceholder(true) + .setDurationUs(600_000) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666)) + .build())) + .build()); State state = new State.Builder() .setAvailableCommands(commands) @@ -108,8 +839,38 @@ public void getterMethods_noOtherMethodCalls_returnCurrentState() { /* playWhenReady= */ true, /* playWhenReadyChangeReason= */ Player .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) + .setPlaybackState(Player.STATE_IDLE) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .setPlayerError(error) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(true) + .setIsLoading(false) + .setSeekBackIncrementMs(5000) + .setSeekForwardIncrementMs(4000) + .setMaxSeekToPreviousPositionMs(3000) + .setPlaybackParameters(playbackParameters) + .setTrackSelectionParameters(trackSelectionParameters) + .setAudioAttributes(audioAttributes) + .setVolume(0.5f) + .setVideoSize(videoSize) + .setCurrentCues(cueGroup) + .setDeviceInfo(deviceInfo) + .setDeviceVolume(5) + .setIsDeviceMuted(true) + .setAudioSessionId(78) + .setSkipSilenceEnabled(true) + .setSurfaceSize(surfaceSize) + .setPlaylist(playlist) + .setPlaylistMetadata(playlistMetadata) + .setCurrentMediaItemIndex(1) + .setCurrentPeriodIndex(1) + .setContentPositionMs(contentPositionSupplier) + .setContentBufferedPositionMs(contentBufferedPositionSupplier) + .setTotalBufferedDurationMs(totalBufferedPositionSupplier) .build(); - SimpleBasePlayer player = + + Player player = new SimpleBasePlayer(Looper.myLooper()) { @Override protected State getState() { @@ -120,11 +881,178 @@ protected State getState() { assertThat(player.getApplicationLooper()).isEqualTo(Looper.myLooper()); assertThat(player.getAvailableCommands()).isEqualTo(commands); assertThat(player.getPlayWhenReady()).isTrue(); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + assertThat(player.getPlaybackSuppressionReason()) + .isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + assertThat(player.getPlayerError()).isEqualTo(error); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + assertThat(player.getShuffleModeEnabled()).isTrue(); + assertThat(player.isLoading()).isFalse(); + assertThat(player.getSeekBackIncrement()).isEqualTo(5000); + assertThat(player.getSeekForwardIncrement()).isEqualTo(4000); + assertThat(player.getMaxSeekToPreviousPosition()).isEqualTo(3000); + assertThat(player.getPlaybackParameters()).isEqualTo(playbackParameters); + assertThat(player.getCurrentTracks()).isEqualTo(tracks); + assertThat(player.getTrackSelectionParameters()).isEqualTo(trackSelectionParameters); + assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata); + assertThat(player.getPlaylistMetadata()).isEqualTo(playlistMetadata); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getDuration()).isEqualTo(500); + assertThat(player.getCurrentPosition()).isEqualTo(456); + assertThat(player.getBufferedPosition()).isEqualTo(499); + assertThat(player.getTotalBufferedDuration()).isEqualTo(567); + assertThat(player.isPlayingAd()).isFalse(); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(C.INDEX_UNSET); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(C.INDEX_UNSET); + assertThat(player.getContentPosition()).isEqualTo(456); + assertThat(player.getContentBufferedPosition()).isEqualTo(499); + assertThat(player.getAudioAttributes()).isEqualTo(audioAttributes); + assertThat(player.getVolume()).isEqualTo(0.5f); + assertThat(player.getVideoSize()).isEqualTo(videoSize); + assertThat(player.getCurrentCues()).isEqualTo(cueGroup); + assertThat(player.getDeviceInfo()).isEqualTo(deviceInfo); + assertThat(player.getDeviceVolume()).isEqualTo(5); + assertThat(player.isDeviceMuted()).isTrue(); + assertThat(player.getSurfaceSize()).isEqualTo(surfaceSize); + Timeline timeline = player.getCurrentTimeline(); + assertThat(timeline.getPeriodCount()).isEqualTo(2); + assertThat(timeline.getWindowCount()).isEqualTo(2); + Timeline.Window window = timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()); + assertThat(window.defaultPositionUs).isEqualTo(0); + assertThat(window.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(window.elapsedRealtimeEpochOffsetMs).isEqualTo(C.TIME_UNSET); + assertThat(window.firstPeriodIndex).isEqualTo(0); + assertThat(window.isDynamic).isFalse(); + assertThat(window.isPlaceholder).isFalse(); + assertThat(window.isSeekable).isFalse(); + assertThat(window.lastPeriodIndex).isEqualTo(0); + assertThat(window.positionInFirstPeriodUs).isEqualTo(0); + assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.liveConfiguration).isNull(); + assertThat(window.manifest).isNull(); + assertThat(window.mediaItem).isEqualTo(MediaItem.EMPTY); + window = timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()); + assertThat(window.defaultPositionUs).isEqualTo(456_789); + assertThat(window.durationUs).isEqualTo(500_000); + assertThat(window.elapsedRealtimeEpochOffsetMs).isEqualTo(10234); + assertThat(window.firstPeriodIndex).isEqualTo(1); + assertThat(window.isDynamic).isTrue(); + assertThat(window.isPlaceholder).isTrue(); + assertThat(window.isSeekable).isTrue(); + assertThat(window.lastPeriodIndex).isEqualTo(1); + assertThat(window.positionInFirstPeriodUs).isEqualTo(100_000); + assertThat(window.presentationStartTimeMs).isEqualTo(12); + assertThat(window.windowStartTimeMs).isEqualTo(23); + assertThat(window.liveConfiguration).isEqualTo(liveConfiguration); + assertThat(window.manifest).isEqualTo(manifest); + assertThat(window.mediaItem).isEqualTo(mediaItem); + assertThat(window.uid).isEqualTo(playlistItemUid); + Timeline.Period period = + timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true); + assertThat(period.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(period.isPlaceholder).isFalse(); + assertThat(period.positionInWindowUs).isEqualTo(0); + assertThat(period.windowIndex).isEqualTo(0); + assertThat(period.getAdGroupCount()).isEqualTo(0); + period = timeline.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true); + assertThat(period.durationUs).isEqualTo(600_000); + assertThat(period.isPlaceholder).isTrue(); + assertThat(period.positionInWindowUs).isEqualTo(-100_000); + assertThat(period.windowIndex).isEqualTo(1); + assertThat(period.id).isEqualTo(periodUid); + assertThat(period.getAdGroupCount()).isEqualTo(2); + assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 0)).isEqualTo(555); + assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 1)).isEqualTo(666); + } + + @Test + public void getterMethods_duringAd_returnAdState() { + SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456; + SimpleBasePlayer.PositionSupplier contentBufferedPositionSupplier = () -> 499; + SimpleBasePlayer.PositionSupplier totalBufferedPositionSupplier = () -> 567; + SimpleBasePlayer.PositionSupplier adPositionSupplier = () -> 321; + SimpleBasePlayer.PositionSupplier adBufferedPositionSupplier = () -> 345; + ImmutableList playlist = + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setDurationUs(500_000) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setIsPlaceholder(true) + .setDurationUs(600_000) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1) + .withAdDurationsUs( + /* adGroupIndex= */ 0, /* adDurationsUs... */ 700_000) + .withAdDurationsUs( + /* adGroupIndex= */ 1, /* adDurationsUs... */ 800_000)) + .build())) + .build()); + State state = + new State.Builder() + .setPlaylist(playlist) + .setCurrentMediaItemIndex(1) + .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0) + .setContentPositionMs(contentPositionSupplier) + .setContentBufferedPositionMs(contentBufferedPositionSupplier) + .setTotalBufferedDurationMs(totalBufferedPositionSupplier) + .setAdPositionMs(adPositionSupplier) + .setAdBufferedPositionMs(adBufferedPositionSupplier) + .build(); + + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + }; + + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + assertThat(player.getDuration()).isEqualTo(800); + assertThat(player.getCurrentPosition()).isEqualTo(321); + assertThat(player.getBufferedPosition()).isEqualTo(345); + assertThat(player.getTotalBufferedDuration()).isEqualTo(567); + assertThat(player.isPlayingAd()).isTrue(); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(1); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getContentPosition()).isEqualTo(456); + assertThat(player.getContentBufferedPosition()).isEqualTo(499); + } + + @Test + public void getterMethods_withEmptyTimeline_returnPlaceholderValues() { + State state = new State.Builder().setCurrentMediaItemIndex(4).build(); + + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + }; + + assertThat(player.getCurrentTracks()).isEqualTo(Tracks.EMPTY); + assertThat(player.getMediaMetadata()).isEqualTo(MediaMetadata.EMPTY); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(4); + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(4); } @SuppressWarnings("deprecation") // Verifying deprecated listener call. @Test - public void invalidateState_updatesStateAndInformsListeners() { + public void invalidateState_updatesStateAndInformsListeners() throws Exception { + Object mediaItemUid0 = new Object(); + MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem0 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0).setMediaItem(mediaItem0).build(); State state1 = new State.Builder() .setAvailableCommands(new Commands.Builder().addAllCommands().build()) @@ -132,14 +1060,108 @@ public void invalidateState_updatesStateAndInformsListeners() { /* playWhenReady= */ true, /* playWhenReadyChangeReason= */ Player .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) + .setPlaybackState(Player.STATE_READY) + .setPlaybackSuppressionReason(Player.PLAYBACK_SUPPRESSION_REASON_NONE) + .setPlayerError(null) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .setShuffleModeEnabled(false) + .setIsLoading(true) + .setSeekBackIncrementMs(7000) + .setSeekForwardIncrementMs(2000) + .setMaxSeekToPreviousPositionMs(8000) + .setPlaybackParameters(PlaybackParameters.DEFAULT) + .setTrackSelectionParameters(TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT) + .setAudioAttributes(AudioAttributes.DEFAULT) + .setVolume(1f) + .setVideoSize(VideoSize.UNKNOWN) + .setCurrentCues(CueGroup.EMPTY_TIME_ZERO) + .setDeviceInfo(DeviceInfo.UNKNOWN) + .setDeviceVolume(0) + .setIsDeviceMuted(false) + .setPlaylist(ImmutableList.of(playlistItem0)) + .setPlaylistMetadata(MediaMetadata.EMPTY) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(8_000) + .build(); + Object mediaItemUid1 = new Object(); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + SimpleBasePlayer.PlaylistItem playlistItem1 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1) + .setMediaItem(mediaItem1) + .setMediaMetadata(mediaMetadata) + .setTracks(tracks) + .build(); + Commands commands = + new Commands.Builder() + .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE) .build(); - Commands commands = new Commands.Builder().add(Player.COMMAND_GET_TEXT).build(); + PlaybackException error = + new PlaybackException( + /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_DECODING_FAILED); + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 2f); + TrackSelectionParameters trackSelectionParameters = + TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT + .buildUpon() + .setMaxVideoBitrate(1000) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(); + VideoSize videoSize = new VideoSize(/* width= */ 200, /* height= */ 400); + CueGroup cueGroup = + new CueGroup( + ImmutableList.of(new Cue.Builder().setText("text").build()), + /* presentationTimeUs= */ 123); + Metadata timedMetadata = + new Metadata(/* presentationTimeUs= */ 42, new FakeMetadataEntry("data")); + Size surfaceSize = new Size(480, 360); + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 3, /* maxVolume= */ 7); + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build(); State state2 = new State.Builder() .setAvailableCommands(commands) .setPlayWhenReady( /* playWhenReady= */ false, - /* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE) + /* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setPlaybackState(Player.STATE_IDLE) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .setPlayerError(error) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(true) + .setIsLoading(false) + .setSeekBackIncrementMs(5000) + .setSeekForwardIncrementMs(4000) + .setMaxSeekToPreviousPositionMs(3000) + .setPlaybackParameters(playbackParameters) + .setTrackSelectionParameters(trackSelectionParameters) + .setAudioAttributes(audioAttributes) + .setVolume(0.5f) + .setVideoSize(videoSize) + .setCurrentCues(cueGroup) + .setDeviceInfo(deviceInfo) + .setDeviceVolume(5) + .setIsDeviceMuted(true) + .setAudioSessionId(78) + .setSkipSilenceEnabled(true) + .setSurfaceSize(surfaceSize) + .setNewlyRenderedFirstFrame(true) + .setTimedMetadata(timedMetadata) + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setPlaylistMetadata(playlistMetadata) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(12_000) + .setPositionDiscontinuity( + Player.DISCONTINUITY_REASON_SEEK, /* discontinuityPositionMs= */ 11_500) .build(); AtomicBoolean returnState2 = new AtomicBoolean(); SimpleBasePlayer player = @@ -156,18 +1178,521 @@ protected State getState() { returnState2.set(true); player.invalidateState(); - - // Verify updated state. - assertThat(player.getAvailableCommands()).isEqualTo(commands); + // Verify state2 is used. assertThat(player.getPlayWhenReady()).isFalse(); - // Verify listener calls. + // Idle Looper to ensure all callbacks (including onEvents) are delivered. + ShadowLooper.idleMainLooper(); + + // Assert listener calls. verify(listener).onAvailableCommandsChanged(commands); verify(listener) .onPlayWhenReadyChanged( - /* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE); + /* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); verify(listener) .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + verify(listener).onPlaybackStateChanged(Player.STATE_IDLE); + verify(listener) + .onPlaybackSuppressionReasonChanged( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + verify(listener).onIsPlayingChanged(false); + verify(listener).onPlayerError(error); + verify(listener).onPlayerErrorChanged(error); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verify(listener).onShuffleModeEnabledChanged(true); + verify(listener).onLoadingChanged(false); + verify(listener).onIsLoadingChanged(false); + verify(listener).onSeekBackIncrementChanged(5000); + verify(listener).onSeekForwardIncrementChanged(4000); + verify(listener).onMaxSeekToPreviousPositionChanged(3000); + verify(listener).onPlaybackParametersChanged(playbackParameters); + verify(listener).onTrackSelectionParametersChanged(trackSelectionParameters); + verify(listener).onAudioAttributesChanged(audioAttributes); + verify(listener).onVolumeChanged(0.5f); + verify(listener).onVideoSizeChanged(videoSize); + verify(listener).onCues(cueGroup.cues); + verify(listener).onCues(cueGroup); + verify(listener).onDeviceInfoChanged(deviceInfo); + verify(listener).onDeviceVolumeChanged(/* volume= */ 5, /* muted= */ true); + verify(listener) + .onTimelineChanged(state2.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener).onMediaMetadataChanged(mediaMetadata); + verify(listener).onTracksChanged(tracks); + verify(listener).onPlaylistMetadataChanged(playlistMetadata); + verify(listener).onAudioSessionIdChanged(78); + verify(listener).onRenderedFirstFrame(); + verify(listener).onMetadata(timedMetadata); + verify(listener).onSurfaceSizeChanged(surfaceSize.getWidth(), surfaceSize.getHeight()); + verify(listener).onSkipSilenceEnabledChanged(true); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid0, + /* mediaItemIndex= */ 0, + mediaItem0, + /* periodUid= */ mediaItemUid0, + /* periodIndex= */ 0, + /* positionMs= */ 8_000, + /* contentPositionMs= */ 8_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid1, + /* mediaItemIndex= */ 1, + mediaItem1, + /* periodUid= */ mediaItemUid1, + /* periodIndex= */ 1, + /* positionMs= */ 11_500, + /* contentPositionMs= */ 11_500, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onMediaItemTransition(mediaItem1, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener) + .onEvents( + player, + new Player.Events( + new FlagSet.Builder() + .addAll( + Player.EVENT_TIMELINE_CHANGED, + Player.EVENT_MEDIA_ITEM_TRANSITION, + Player.EVENT_TRACKS_CHANGED, + Player.EVENT_IS_LOADING_CHANGED, + Player.EVENT_PLAYBACK_STATE_CHANGED, + Player.EVENT_PLAY_WHEN_READY_CHANGED, + Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, + Player.EVENT_IS_PLAYING_CHANGED, + Player.EVENT_REPEAT_MODE_CHANGED, + Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, + Player.EVENT_PLAYER_ERROR, + Player.EVENT_POSITION_DISCONTINUITY, + Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, + Player.EVENT_AVAILABLE_COMMANDS_CHANGED, + Player.EVENT_MEDIA_METADATA_CHANGED, + Player.EVENT_PLAYLIST_METADATA_CHANGED, + Player.EVENT_SEEK_BACK_INCREMENT_CHANGED, + Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED, + Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED, + Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, + Player.EVENT_AUDIO_ATTRIBUTES_CHANGED, + Player.EVENT_AUDIO_SESSION_ID, + Player.EVENT_VOLUME_CHANGED, + Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED, + Player.EVENT_SURFACE_SIZE_CHANGED, + Player.EVENT_VIDEO_SIZE_CHANGED, + Player.EVENT_RENDERED_FIRST_FRAME, + Player.EVENT_CUES, + Player.EVENT_METADATA, + Player.EVENT_DEVICE_INFO_CHANGED, + Player.EVENT_DEVICE_VOLUME_CHANGED) + .build())); verifyNoMoreInteractions(listener); + // Assert that we actually called all listeners. + for (Method method : Player.Listener.class.getDeclaredMethods()) { + if (method.getName().equals("onSeekProcessed")) { + continue; + } + method.invoke(verify(listener), getAnyArguments(method)); + } + } + + @Test + public void invalidateState_withPlaylistItemDetailChange_reportsTimelineSourceUpdate() { + Object mediaItemUid0 = new Object(); + SimpleBasePlayer.PlaylistItem playlistItem0 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0).build(); + Object mediaItemUid1 = new Object(); + SimpleBasePlayer.PlaylistItem playlistItem1 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).build(); + State state1 = + new State.Builder().setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)).build(); + SimpleBasePlayer.PlaylistItem playlistItem1Updated = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setDurationUs(10_000).build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1Updated)) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onTimelineChanged(state2.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void invalidateState_withCurrentMediaItemRemoval_reportsDiscontinuityReasonRemoved() { + Object mediaItemUid0 = new Object(); + MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem0 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0).setMediaItem(mediaItem0).build(); + Object mediaItemUid1 = new Object(); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + SimpleBasePlayer.PlaylistItem playlistItem1 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(5000) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(2000) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid1, + /* mediaItemIndex= */ 1, + mediaItem1, + /* periodUid= */ mediaItemUid1, + /* periodIndex= */ 1, + /* positionMs= */ 5000, + /* contentPositionMs= */ 5000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid0, + /* mediaItemIndex= */ 0, + mediaItem0, + /* periodUid= */ mediaItemUid0, + /* periodIndex= */ 0, + /* positionMs= */ 2000, + /* contentPositionMs= */ 2000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_REMOVE); + verify(listener) + .onMediaItemTransition(mediaItem0, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + } + + @Test + public void + invalidateState_withTransitionFromEndOfItem_reportsDiscontinuityReasonAutoTransition() { + Object mediaItemUid0 = new Object(); + MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem0 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0) + .setMediaItem(mediaItem0) + .setDurationUs(50_000) + .build(); + Object mediaItemUid1 = new Object(); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + SimpleBasePlayer.PlaylistItem playlistItem1 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(50) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(10) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid0, + /* mediaItemIndex= */ 0, + mediaItem0, + /* periodUid= */ mediaItemUid0, + /* periodIndex= */ 0, + /* positionMs= */ 50, + /* contentPositionMs= */ 50, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid1, + /* mediaItemIndex= */ 1, + mediaItem1, + /* periodUid= */ mediaItemUid1, + /* periodIndex= */ 1, + /* positionMs= */ 10, + /* contentPositionMs= */ 10, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + verify(listener).onMediaItemTransition(mediaItem1, Player.MEDIA_ITEM_TRANSITION_REASON_AUTO); + } + + @Test + public void invalidateState_withTransitionFromMiddleOfItem_reportsDiscontinuityReasonSkip() { + Object mediaItemUid0 = new Object(); + MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem0 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0) + .setMediaItem(mediaItem0) + .setDurationUs(50_000) + .build(); + Object mediaItemUid1 = new Object(); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + SimpleBasePlayer.PlaylistItem playlistItem1 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(20) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(10) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid0, + /* mediaItemIndex= */ 0, + mediaItem0, + /* periodUid= */ mediaItemUid0, + /* periodIndex= */ 0, + /* positionMs= */ 20, + /* contentPositionMs= */ 20, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid1, + /* mediaItemIndex= */ 1, + mediaItem1, + /* periodUid= */ mediaItemUid1, + /* periodIndex= */ 1, + /* positionMs= */ 10, + /* contentPositionMs= */ 10, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_SKIP); + verify(listener) + .onMediaItemTransition(mediaItem1, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + } + + @Test + public void invalidateState_withRepeatingItem_reportsDiscontinuityReasonAutoTransition() { + Object mediaItemUid = new Object(); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid) + .setMediaItem(mediaItem) + .setDurationUs(5_000_000) + .build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(5_000) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(0) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid, + /* mediaItemIndex= */ 0, + mediaItem, + /* periodUid= */ mediaItemUid, + /* periodIndex= */ 0, + /* positionMs= */ 5_000, + /* contentPositionMs= */ 5_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid, + /* mediaItemIndex= */ 0, + mediaItem, + /* periodUid= */ mediaItemUid, + /* periodIndex= */ 0, + /* positionMs= */ 0, + /* contentPositionMs= */ 0, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + verify(listener).onMediaItemTransition(mediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT); + } + + @Test + public void invalidateState_withDiscontinuityInsideItem_reportsDiscontinuityReasonInternal() { + Object mediaItemUid = new Object(); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid) + .setMediaItem(mediaItem) + .setDurationUs(5_000_000) + .build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(1_000) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(3_000) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid, + /* mediaItemIndex= */ 0, + mediaItem, + /* periodUid= */ mediaItemUid, + /* periodIndex= */ 0, + /* positionMs= */ 1_000, + /* contentPositionMs= */ 1_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid, + /* mediaItemIndex= */ 0, + mediaItem, + /* periodUid= */ mediaItemUid, + /* periodIndex= */ 0, + /* positionMs= */ 3_000, + /* contentPositionMs= */ 3_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_INTERNAL); + verify(listener, never()).onMediaItemTransition(any(), anyInt()); + } + + @Test + public void invalidateState_withMinorPositionDrift_doesNotReportsDiscontinuity() { + SimpleBasePlayer.PlaylistItem playlistItem = + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(1_000) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(1_500) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt()); + verify(listener, never()).onMediaItemTransition(any(), anyInt()); } @Test @@ -403,4 +1928,23 @@ protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { assertThat(callForwarded.get()).isFalse(); } + + private static Object[] getAnyArguments(Method method) { + Object[] arguments = new Object[method.getParameterCount()]; + Class[] argumentTypes = method.getParameterTypes(); + for (int i = 0; i < arguments.length; i++) { + if (argumentTypes[i].equals(Integer.TYPE)) { + arguments[i] = anyInt(); + } else if (argumentTypes[i].equals(Long.TYPE)) { + arguments[i] = anyLong(); + } else if (argumentTypes[i].equals(Float.TYPE)) { + arguments[i] = anyFloat(); + } else if (argumentTypes[i].equals(Boolean.TYPE)) { + arguments[i] = anyBoolean(); + } else { + arguments[i] = any(); + } + } + return arguments; + } } From f4f801a80978d77dc189a3795edb0ec31346879f Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 21 Nov 2022 09:31:15 +0000 Subject: [PATCH 012/141] Do not require package visibility when connecting to a Media3 session When we currently call SessionToken.createSessionToken with a legacy token, we call the package manager to get the process UID. This requires visiblity to the target package, which may not be available unless the target runs a service known to the controller app. However, when connecting to a Media3, this UID doesn't have to be known, so we can move the call closer to where it's needed to avoid the unncessary visibility check. In addition, a legacy session may reply with unknown result code to the session token request, which we should handle as well. One of the constructor can be removed since it was only used from a test. PiperOrigin-RevId: 489917706 (cherry picked from commit 2fd4aac310787d1a57207b5142a0ab08d5e1a2a5) --- .../androidx/media3/session/SessionToken.java | 59 +++++++++---------- ...CompatCallbackWithMediaControllerTest.java | 3 +- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java index 5b0e76b8171..e9eba03445f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java @@ -130,6 +130,7 @@ public SessionToken(Context context, ComponentName serviceComponent) { } } + /** Creates a session token connected to a Media3 session. */ /* package */ SessionToken( int uid, int type, @@ -143,21 +144,9 @@ public SessionToken(Context context, ComponentName serviceComponent) { uid, type, libraryVersion, interfaceVersion, packageName, iSession, tokenExtras); } - /* package */ SessionToken(Context context, MediaSessionCompat.Token compatToken) { - checkNotNull(context, "context must not be null"); - checkNotNull(compatToken, "compatToken must not be null"); - - MediaControllerCompat controller = createMediaControllerCompat(context, compatToken); - - String packageName = controller.getPackageName(); - int uid = getUid(context.getPackageManager(), packageName); - Bundle extras = controller.getSessionInfo(); - - impl = new SessionTokenImplLegacy(compatToken, packageName, uid, extras); - } - - /* package */ SessionToken(SessionTokenImpl impl) { - this.impl = impl; + /** Creates a session token connected to a legacy media session. */ + private SessionToken(MediaSessionCompat.Token token, String packageName, int uid, Bundle extras) { + this.impl = new SessionTokenImplLegacy(token, packageName, uid, extras); } private SessionToken(Bundle bundle) { @@ -283,32 +272,37 @@ public static ListenableFuture createSessionToken( MediaControllerCompat controller = createMediaControllerCompat(context, (MediaSessionCompat.Token) compatToken); String packageName = controller.getPackageName(); - int uid = getUid(context.getPackageManager(), packageName); Handler handler = new Handler(thread.getLooper()); + Runnable createFallbackLegacyToken = + () -> { + int uid = getUid(context.getPackageManager(), packageName); + SessionToken resultToken = + new SessionToken( + (MediaSessionCompat.Token) compatToken, + packageName, + uid, + controller.getSessionInfo()); + future.set(resultToken); + }; controller.sendCommand( MediaConstants.SESSION_COMMAND_REQUEST_SESSION3_TOKEN, /* params= */ null, new ResultReceiver(handler) { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { + // Remove timeout callback. handler.removeCallbacksAndMessages(null); - future.set(SessionToken.CREATOR.fromBundle(resultData)); + try { + future.set(SessionToken.CREATOR.fromBundle(resultData)); + } catch (RuntimeException e) { + // Fallback to a legacy token if we receive an unexpected result, e.g. a legacy + // session acknowledging commands by a success callback. + createFallbackLegacyToken.run(); + } } }); - - handler.postDelayed( - () -> { - // Timed out getting session3 token. Handle this as a legacy token. - SessionToken resultToken = - new SessionToken( - new SessionTokenImplLegacy( - (MediaSessionCompat.Token) compatToken, - packageName, - uid, - controller.getSessionInfo())); - future.set(resultToken); - }, - WAIT_TIME_MS_FOR_SESSION3_TOKEN); + // Post creating a fallback token if the command receives no result after a timeout. + handler.postDelayed(createFallbackLegacyToken, WAIT_TIME_MS_FOR_SESSION3_TOKEN); future.addListener(() -> thread.quit(), MoreExecutors.directExecutor()); return future; } @@ -399,7 +393,8 @@ private static int getUid(PackageManager manager, String packageName) { try { return manager.getApplicationInfo(packageName, 0).uid; } catch (PackageManager.NameNotFoundException e) { - throw new IllegalArgumentException("Cannot find package " + packageName, e); + throw new IllegalArgumentException( + "Cannot find package " + packageName + " or package is not visible", e); } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java index ca25291cf43..45ee44b3af6 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java @@ -112,7 +112,8 @@ public void cleanUp() { } private RemoteMediaController createControllerAndWaitConnection() throws Exception { - SessionToken sessionToken = new SessionToken(context, session.getSessionToken()); + SessionToken sessionToken = + SessionToken.createSessionToken(context, session.getSessionToken()).get(); return controllerTestRule.createRemoteController(sessionToken); } From 3476ca9296a9fd648ad550e66f46f927b1c2ddd6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 21 Nov 2022 17:34:53 +0000 Subject: [PATCH 013/141] Add `set -eu` to all shell scripts These flags ensure that any errors cause the script to exit (instead of just carrying on) (`-e`) and that any unrecognised substitution variables cause an error instead of silently resolving to an empty string (`-u`). Issues like Issue: google/ExoPlayer#10791 should be more quickly resolved with `set -e` because the script will clearly fail with an error like `make: command not found` which would give the user a clear pointer towards the cause of the problem. #minor-release PiperOrigin-RevId: 490001419 (cherry picked from commit 45b8fb0ae1314abdc5b0364137622214ac8e5b98) --- libraries/decoder_ffmpeg/src/main/jni/build_ffmpeg.sh | 1 + libraries/decoder_opus/src/main/jni/convert_android_asm.sh | 2 +- .../decoder_vp9/src/main/jni/generate_libvpx_android_configs.sh | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/decoder_ffmpeg/src/main/jni/build_ffmpeg.sh b/libraries/decoder_ffmpeg/src/main/jni/build_ffmpeg.sh index fef653bf6ed..1583c1c9641 100755 --- a/libraries/decoder_ffmpeg/src/main/jni/build_ffmpeg.sh +++ b/libraries/decoder_ffmpeg/src/main/jni/build_ffmpeg.sh @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +set -eu FFMPEG_MODULE_PATH=$1 NDK_PATH=$2 diff --git a/libraries/decoder_opus/src/main/jni/convert_android_asm.sh b/libraries/decoder_opus/src/main/jni/convert_android_asm.sh index 9c79738439b..48b141dca23 100755 --- a/libraries/decoder_opus/src/main/jni/convert_android_asm.sh +++ b/libraries/decoder_opus/src/main/jni/convert_android_asm.sh @@ -15,7 +15,7 @@ # limitations under the License. # -set -e +set -eu ASM_CONVERTER="./libopus/celt/arm/arm2gnu.pl" if [[ ! -x "${ASM_CONVERTER}" ]]; then diff --git a/libraries/decoder_vp9/src/main/jni/generate_libvpx_android_configs.sh b/libraries/decoder_vp9/src/main/jni/generate_libvpx_android_configs.sh index 18f1dd5c698..b121886070e 100755 --- a/libraries/decoder_vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/libraries/decoder_vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -18,7 +18,7 @@ # a bash script that generates the necessary config files for libvpx android ndk # builds. -set -e +set -eu if [ $# -ne 0 ]; then echo "Usage: ${0}" From fa6b8fe06d3c6c8afd83f44ccdfc0bc0c52d883e Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 22 Nov 2022 10:01:44 +0000 Subject: [PATCH 014/141] Do not require package visibility when obtaining SessionTokens The only reason this is required at the moment is to set the process UID field in the token, that is supposed to make it easier for controller apps to identify the session. However, if this visibility is not provided, it shouldn't stop us from creating the controller for this session. Also docuement more clearly what UID means in this context. PiperOrigin-RevId: 490184508 (cherry picked from commit c41a5c842080a7e75b9d92acc06d583bd20c7abb) --- .../java/androidx/media3/session/SessionToken.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java index e9eba03445f..09c5e61de73 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java @@ -36,6 +36,7 @@ import androidx.annotation.Nullable; import androidx.media.MediaBrowserServiceCompat; import androidx.media3.common.Bundleable; +import androidx.media3.common.C; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.util.UnstableApi; import com.google.common.collect.ImmutableSet; @@ -179,7 +180,11 @@ public String toString() { return impl.toString(); } - /** Returns the uid of the session */ + /** + * Returns the UID of the session process, or {@link C#INDEX_UNSET} if the UID can't be determined + * due to missing package + * visibility. + */ public int getUid() { return impl.getUid(); } @@ -393,8 +398,7 @@ private static int getUid(PackageManager manager, String packageName) { try { return manager.getApplicationInfo(packageName, 0).uid; } catch (PackageManager.NameNotFoundException e) { - throw new IllegalArgumentException( - "Cannot find package " + packageName + " or package is not visible", e); + return C.INDEX_UNSET; } } From dddb72b2693b0f0a4a3c122dbd9995af09836d59 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 22 Nov 2022 13:09:17 +0000 Subject: [PATCH 015/141] Add `DefaultExtractorsFactory.setTsSubtitleFormats` ExoPlayer is unable to detect the presence of subtitle tracks in some MPEG-TS files that don't fully declare them. It's possible for a developer to provide the list instead, but doing so is quite awkward without this helper method. This is consistent for how `DefaultExtractorsFactory` allows other aspects of the delegate `Extractor` implementations to be customised. * Issue: google/ExoPlayer#10175 * Issue: google/ExoPlayer#10505 #minor-release PiperOrigin-RevId: 490214619 (cherry picked from commit ff48faec5f9230355907a8be24e44068ec294982) --- .../extractor/DefaultExtractorsFactory.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java b/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java index 0b4e9da76d8..992221c8890 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java @@ -22,6 +22,7 @@ import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.media3.common.FileTypes; +import androidx.media3.common.Format; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.util.TimestampAdjuster; @@ -44,6 +45,7 @@ import androidx.media3.extractor.ts.TsExtractor; import androidx.media3.extractor.ts.TsPayloadReader; import androidx.media3.extractor.wav.WavExtractor; +import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -128,11 +130,13 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { private @Mp3Extractor.Flags int mp3Flags; private @TsExtractor.Mode int tsMode; private @DefaultTsPayloadReaderFactory.Flags int tsFlags; + private ImmutableList tsSubtitleFormats; private int tsTimestampSearchBytes; public DefaultExtractorsFactory() { tsMode = TsExtractor.MODE_SINGLE_PMT; tsTimestampSearchBytes = TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES; + tsSubtitleFormats = ImmutableList.of(); } /** @@ -303,6 +307,20 @@ public synchronized DefaultExtractorsFactory setTsExtractorFlags( return this; } + /** + * Sets a list of subtitle formats to pass to the {@link DefaultTsPayloadReaderFactory} used by + * {@link TsExtractor} instances created by the factory. + * + * @see DefaultTsPayloadReaderFactory#DefaultTsPayloadReaderFactory(int, List) + * @param subtitleFormats The subtitle formats. + * @return The factory, for convenience. + */ + @CanIgnoreReturnValue + public synchronized DefaultExtractorsFactory setTsSubtitleFormats(List subtitleFormats) { + tsSubtitleFormats = ImmutableList.copyOf(subtitleFormats); + return this; + } + /** * Sets the number of bytes searched to find a timestamp for {@link TsExtractor} instances created * by the factory. @@ -416,7 +434,12 @@ private void addExtractorsForFileType(@FileTypes.Type int fileType, List Date: Tue, 22 Nov 2022 14:16:35 +0000 Subject: [PATCH 016/141] Reorder some release notes in other sections. PiperOrigin-RevId: 490224795 (cherry picked from commit fa531b79249e5435af719bfbe168b999b5032b47) From d3d99f01946d351e427fd597f2d358ede962949d Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Tue, 22 Nov 2022 18:55:24 +0000 Subject: [PATCH 017/141] Load bitmaps for `MediaSessionCompat.QueueItem`. When receiving the `onTimelineChanged` callback, we convert the timeline to the list of `QueueItem`s, where decoding a bitmap is needed for building each of the `QueueItem`s. The strategy is similar to what we did in for list of `MediaBrowserCompat.MediaItem` - set the queue item list until the bitmaps decoding for all the `MediaItem`s are completed. PiperOrigin-RevId: 490283587 (cherry picked from commit 8ce1213ddddb98e0483610cfeaeba3daa5ad9a78) --- .../session/MediaSessionLegacyStub.java | 61 +++++++++++++++++-- .../androidx/media3/session/MediaUtils.java | 16 ++--- ...lerCompatCallbackWithMediaSessionTest.java | 3 +- ...aSessionCompatCallbackAggregationTest.java | 52 ++++++++++++++-- ...tateMaskingWithMediaSessionCompatTest.java | 16 ++--- ...aControllerWithMediaSessionCompatTest.java | 36 +++++++---- ...CompatCallbackWithMediaControllerTest.java | 12 ++-- .../media3/session/MediaUtilsTest.java | 24 +++++--- .../media3/session/MediaTestUtils.java | 34 +++++++++++ 9 files changed, 197 insertions(+), 57 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 5e8ae125918..516dcf10d66 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -89,11 +89,14 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; import org.checkerframework.checker.initialization.qual.Initialized; +import org.checkerframework.checker.nullness.compatqual.NullableType; // Getting the commands from MediaControllerCompat' /* package */ class MediaSessionLegacyStub extends MediaSessionCompat.Callback { @@ -394,7 +397,7 @@ public void onSkipToQueueItem(long queueId) { controller -> { PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); // Use queueId as an index as we've published {@link QueueItem} as so. - // see: {@link MediaUtils#convertToQueueItemList}. + // see: {@link MediaUtils#convertToQueueItem}. playerWrapper.seekToDefaultPosition((int) queueId); }, sessionCompat.getCurrentControllerInfo()); @@ -1011,8 +1014,59 @@ public void onTimelineChanged( setQueue(sessionCompat, null); return; } + + updateQueue(timeline); + + // Duration might be unknown at onMediaItemTransition and become available afterward. + updateMetadataIfChanged(); + } + + private void updateQueue(Timeline timeline) { List mediaItemList = MediaUtils.convertToMediaItemList(timeline); - List queueItemList = MediaUtils.convertToQueueItemList(mediaItemList); + List<@NullableType ListenableFuture> bitmapFutures = new ArrayList<>(); + final AtomicInteger resultCount = new AtomicInteger(0); + Runnable handleBitmapFuturesTask = + () -> { + int completedBitmapFutureCount = resultCount.incrementAndGet(); + if (completedBitmapFutureCount == mediaItemList.size()) { + handleBitmapFuturesAllCompletedAndSetQueue(bitmapFutures, timeline, mediaItemList); + } + }; + + for (int i = 0; i < mediaItemList.size(); i++) { + MediaItem mediaItem = mediaItemList.get(i); + MediaMetadata metadata = mediaItem.mediaMetadata; + if (metadata.artworkData == null) { + bitmapFutures.add(null); + handleBitmapFuturesTask.run(); + } else { + ListenableFuture bitmapFuture = + sessionImpl.getBitmapLoader().decodeBitmap(metadata.artworkData); + bitmapFutures.add(bitmapFuture); + bitmapFuture.addListener( + handleBitmapFuturesTask, sessionImpl.getApplicationHandler()::post); + } + } + } + + private void handleBitmapFuturesAllCompletedAndSetQueue( + List<@NullableType ListenableFuture> bitmapFutures, + Timeline timeline, + List mediaItems) { + List queueItemList = new ArrayList<>(); + for (int i = 0; i < bitmapFutures.size(); i++) { + @Nullable ListenableFuture future = bitmapFutures.get(i); + @Nullable Bitmap bitmap = null; + if (future != null) { + try { + bitmap = Futures.getDone(future); + } catch (CancellationException | ExecutionException e) { + Log.d(TAG, "Failed to get bitmap"); + } + } + queueItemList.add(MediaUtils.convertToQueueItem(mediaItems.get(i), i, bitmap)); + } + if (Util.SDK_INT < 21) { // In order to avoid TransactionTooLargeException for below API 21, we need to // cut the list so that it doesn't exceed the binder transaction limit. @@ -1029,9 +1083,6 @@ public void onTimelineChanged( // which means we can safely send long lists. sessionCompat.setQueue(queueItemList); } - - // Duration might be unknown at onMediaItemTransition and become available afterward. - updateMetadataIfChanged(); } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 3f89c5dd737..97d240032c9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -230,18 +230,14 @@ public static List convertToMediaItemList(Timeline timeline) { } /** - * Converts a list of {@link MediaItem} to a list of {@link QueueItem}. The index of the item + * Converts a {@link MediaItem} to a {@link QueueItem}. The index of the item in the playlist * would be used as the queue ID to match the behavior of {@link MediaController}. */ - public static List convertToQueueItemList(List items) { - List result = new ArrayList<>(); - for (int i = 0; i < items.size(); i++) { - MediaItem item = items.get(i); - MediaDescriptionCompat description = convertToMediaDescriptionCompat(item); - long id = convertToQueueItemId(i); - result.add(new QueueItem(description, id)); - } - return result; + public static QueueItem convertToQueueItem( + MediaItem item, int mediaItemIndex, @Nullable Bitmap artworkBitmap) { + MediaDescriptionCompat description = convertToMediaDescriptionCompat(item, artworkBitmap); + long id = convertToQueueItemId(mediaItemIndex); + return new QueueItem(description, id); } /** Converts the index of a {@link MediaItem} in a playlist into id of {@link QueueItem}. */ diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java index 3d4084434bd..77d06b72449 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java @@ -1121,7 +1121,7 @@ public void onQueueChanged(List queue) { MediaItem mediaItem = new MediaItem.Builder() .setMediaId("mediaItem_withSampleMediaMetadata") - .setMediaMetadata(MediaTestUtils.createMediaMetadata()) + .setMediaMetadata(MediaTestUtils.createMediaMetadataWithArtworkData()) .build(); Timeline timeline = new PlaylistTimeline(ImmutableList.of(mediaItem)); @@ -1140,6 +1140,7 @@ public void onQueueChanged(List queue) { .isTrue(); assertThat(description.getIconUri()).isEqualTo(mediaItem.mediaMetadata.artworkUri); assertThat(description.getMediaUri()).isEqualTo(mediaItem.requestMetadata.mediaUri); + assertThat(description.getIconBitmap()).isNotNull(); assertThat(TestUtils.equals(description.getExtras(), mediaItem.mediaMetadata.extras)).isTrue(); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerMediaSessionCompatCallbackAggregationTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerMediaSessionCompatCallbackAggregationTest.java index f090ca8b9ca..6f5697fc346 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerMediaSessionCompatCallbackAggregationTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerMediaSessionCompatCallbackAggregationTest.java @@ -26,8 +26,11 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; import android.content.Context; +import android.graphics.Bitmap; +import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.RatingCompat; import android.support.v4.media.session.MediaSessionCompat; @@ -42,6 +45,7 @@ import androidx.media3.common.Player.PositionInfo; import androidx.media3.common.Timeline; import androidx.media3.common.Timeline.Window; +import androidx.media3.common.util.Util; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -73,11 +77,13 @@ public class MediaControllerMediaSessionCompatCallbackAggregationTest { private Context context; private RemoteMediaSessionCompat session; + private BitmapLoader bitmapLoader; @Before public void setUp() throws Exception { context = ApplicationProvider.getApplicationContext(); session = new RemoteMediaSessionCompat(DEFAULT_TEST_NAME, context); + bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader()); } @After @@ -88,8 +94,8 @@ public void cleanUp() throws Exception { @Test public void getters_withValidQueueAndQueueIdAndMetadata() throws Exception { int testSize = 3; - List testMediaItems = MediaTestUtils.createMediaItems(testSize); - List testQueue = MediaUtils.convertToQueueItemList(testMediaItems); + List testMediaItems = MediaTestUtils.createMediaItemsWithArtworkData(testSize); + List testQueue = convertToQueueItems(testMediaItems); int testMediaItemIndex = 1; MediaMetadataCompat testMediaMetadataCompat = createMediaMetadataCompat(); @RatingCompat.Style int testRatingType = RatingCompat.RATING_HEART; @@ -173,8 +179,28 @@ public void onEvents(Player player, Events events) { assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(mediaItemRef.get()).isEqualTo(testCurrentMediaItem); for (int i = 0; i < timelineRef.get().getWindowCount(); i++) { - assertThat(timelineRef.get().getWindow(i, new Window()).mediaItem) - .isEqualTo(i == testMediaItemIndex ? testCurrentMediaItem : testMediaItems.get(i)); + MediaItem mediaItem = timelineRef.get().getWindow(i, new Window()).mediaItem; + MediaItem expectedMediaItem = + (i == testMediaItemIndex) ? testCurrentMediaItem : testMediaItems.get(i); + if (Util.SDK_INT < 21) { + // Bitmap conversion and back gives not exactly the same byte array below API 21 + MediaMetadata mediaMetadata = + mediaItem + .mediaMetadata + .buildUpon() + .setArtworkData(/* artworkData= */ null, /* artworkDataType= */ null) + .build(); + MediaMetadata expectedMediaMetadata = + expectedMediaItem + .mediaMetadata + .buildUpon() + .setArtworkData(/* artworkData= */ null, /* artworkDataType= */ null) + .build(); + mediaItem = mediaItem.buildUpon().setMediaMetadata(mediaMetadata).build(); + expectedMediaItem = + expectedMediaItem.buildUpon().setMediaMetadata(expectedMediaMetadata).build(); + } + assertThat(mediaItem).isEqualTo(expectedMediaItem); } assertThat(timelineChangeReasonRef.get()).isEqualTo(TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); assertThat(mediaItemTransitionReasonRef.get()) @@ -202,7 +228,7 @@ public void onEvents(Player player, Events events) { public void getters_withValidQueueAndMetadataButWithInvalidQueueId() throws Exception { int testSize = 3; List testMediaItems = MediaTestUtils.createMediaItems(testSize); - List testQueue = MediaUtils.convertToQueueItemList(testMediaItems); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testMediaItems); MediaMetadataCompat testMediaMetadataCompat = createMediaMetadataCompat(); @RatingCompat.Style int testRatingType = RatingCompat.RATING_HEART; MediaMetadata testMediaMetadata = @@ -306,7 +332,7 @@ public void onEvents(Player player, Events events) { public void getters_withValidQueueAndQueueIdWithoutMetadata() throws Exception { int testSize = 3; List testMediaItems = MediaTestUtils.createMediaItems(testSize); - List testQueue = MediaUtils.convertToQueueItemList(testMediaItems); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testMediaItems); @RatingCompat.Style int testRatingType = RatingCompat.RATING_HEART; Events testEvents = new Events( @@ -511,4 +537,18 @@ private static void assertTimelineEqualsToMediaItems( .isEqualTo(mediaItems.get(i)); } } + + private List convertToQueueItems(List mediaItems) + throws Exception { + List list = new ArrayList<>(); + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem item = mediaItems.get(i); + @Nullable + Bitmap bitmap = bitmapLoader.decodeBitmap(item.mediaMetadata.artworkData).get(10, SECONDS); + MediaDescriptionCompat description = MediaUtils.convertToMediaDescriptionCompat(item, bitmap); + long id = MediaUtils.convertToQueueItemId(i); + list.add(new MediaSessionCompat.QueueItem(description, id)); + } + return list; + } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingWithMediaSessionCompatTest.java index e0c56563f7c..e10dd5ae963 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingWithMediaSessionCompatTest.java @@ -418,7 +418,7 @@ public void onEvents(Player player, Player.Events events) { @Test public void seekTo_withNewMediaItemIndex() throws Exception { List mediaItems = MediaTestUtils.createMediaItems(3); - List queue = MediaUtils.convertToQueueItemList(mediaItems); + List queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems); long initialPosition = 8_000; long initialBufferedPosition = 9_200; int initialIndex = 0; @@ -701,7 +701,7 @@ public void onEvents(Player player, Player.Events events) { @Test public void addMediaItems() throws Exception { List mediaItems = MediaTestUtils.createMediaItems("a", "b", "c"); - List queue = MediaUtils.convertToQueueItemList(mediaItems); + List queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems); long testPosition = 200L; int testCurrentMediaItemIndex = 1; MediaItem testCurrentMediaItem = mediaItems.get(testCurrentMediaItemIndex); @@ -767,7 +767,7 @@ public void onEvents(Player player, Player.Events events) { public void addMediaItems_beforeCurrentMediaItemIndex_shiftsCurrentMediaItemIndex() throws Exception { List mediaItems = MediaTestUtils.createMediaItems("a", "b", "c"); - List queue = MediaUtils.convertToQueueItemList(mediaItems); + List queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems); long testPosition = 200L; int initialMediaItemIndex = 2; MediaItem testCurrentMediaItem = mediaItems.get(initialMediaItemIndex); @@ -833,7 +833,7 @@ public void onEvents(Player player, Player.Events events) { @Test public void removeMediaItems() throws Exception { List mediaItems = MediaTestUtils.createMediaItems(5); - List queue = MediaUtils.convertToQueueItemList(mediaItems); + List queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems); long testPosition = 200L; int testCurrentMediaItemIndex = 0; MediaItem testCurrentMediaItem = mediaItems.get(testCurrentMediaItemIndex); @@ -898,7 +898,7 @@ public void onEvents(Player player, Player.Events events) { public void removeMediaItems_beforeCurrentMediaItemIndex_shiftsCurrentMediaItemIndex() throws Exception { List mediaItems = MediaTestUtils.createMediaItems(5); - List queue = MediaUtils.convertToQueueItemList(mediaItems); + List queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems); long testPosition = 200L; int initialMediaItemIndex = 4; MediaItem testCurrentMediaItem = mediaItems.get(initialMediaItemIndex); @@ -963,7 +963,7 @@ public void onEvents(Player player, Player.Events events) { @Test public void removeMediaItems_includeCurrentMediaItem_movesCurrentItem() throws Exception { List mediaItems = MediaTestUtils.createMediaItems(5); - List queue = MediaUtils.convertToQueueItemList(mediaItems); + List queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems); long testPosition = 200L; int initialMediaItemIndex = 2; MediaItem testCurrentMediaItem = mediaItems.get(initialMediaItemIndex); @@ -1025,7 +1025,7 @@ public void onEvents(Player player, Player.Events events) { @Test public void moveMediaItems() throws Exception { List mediaItems = MediaTestUtils.createMediaItems(5); - List queue = MediaUtils.convertToQueueItemList(mediaItems); + List queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems); long testPosition = 200L; int testCurrentMediaItemIndex = 0; MediaItem testCurrentMediaItem = mediaItems.get(testCurrentMediaItemIndex); @@ -1090,7 +1090,7 @@ public void onEvents(Player player, Player.Events events) { @Test public void moveMediaItems_withMovingCurrentMediaItem_changesCurrentItem() throws Exception { List mediaItems = MediaTestUtils.createMediaItems(5); - List queue = MediaUtils.convertToQueueItemList(mediaItems); + List queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems); long testPosition = 200L; int initialCurrentMediaItemIndex = 1; session.setPlaybackState( diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java index 724c82492bf..b735477ce79 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java @@ -106,6 +106,8 @@ public class MediaControllerWithMediaSessionCompatTest { @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); + private static final String TEST_IMAGE_PATH = "media/png/non-motion-photo-shortened.png"; + private final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); private final MediaControllerTestRule controllerTestRule = new MediaControllerTestRule(threadTestRule); @@ -373,12 +375,13 @@ public void onTimelineChanged( Timeline testTimeline = MediaTestUtils.createTimeline(/* windowCount= */ 2); List testQueue = - MediaUtils.convertToQueueItemList(MediaUtils.convertToMediaItemList(testTimeline)); + MediaTestUtils.convertToQueueItemsWithoutBitmap( + MediaUtils.convertToMediaItemList(testTimeline)); session.setQueue(testQueue); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); MediaTestUtils.assertMediaIdEquals(testTimeline, timelineFromParamRef.get()); - MediaTestUtils.assertMediaIdEquals(testTimeline, timelineFromParamRef.get()); + MediaTestUtils.assertMediaIdEquals(testTimeline, timelineFromGetterRef.get()); assertThat(reasonRef.get()).isEqualTo(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); } @@ -386,7 +389,8 @@ public void onTimelineChanged( public void setQueue_withNull_notifiesEmptyTimeline() throws Exception { Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 2); List queue = - MediaUtils.convertToQueueItemList(MediaUtils.convertToMediaItemList(timeline)); + MediaTestUtils.convertToQueueItemsWithoutBitmap( + MediaUtils.convertToMediaItemList(timeline)); session.setQueue(queue); CountDownLatch latch = new CountDownLatch(1); @@ -433,6 +437,9 @@ public void onTimelineChanged( Uri testIconUri = Uri.parse("androidx://media3-session/icon"); Uri testMediaUri = Uri.parse("androidx://media3-session/media"); Bundle testExtras = TestUtils.createTestBundle(); + byte[] testArtworkData = + TestUtils.getByteArrayForScaledBitmap(context.getApplicationContext(), TEST_IMAGE_PATH); + @Nullable Bitmap testBitmap = bitmapLoader.decodeBitmap(testArtworkData).get(10, SECONDS); MediaDescriptionCompat description = new MediaDescriptionCompat.Builder() .setMediaId(testMediaId) @@ -442,6 +449,7 @@ public void onTimelineChanged( .setIconUri(testIconUri) .setMediaUri(testMediaUri) .setExtras(testExtras) + .setIconBitmap(testBitmap) .build(); QueueItem queueItem = new QueueItem(description, /* id= */ 0); session.setQueue(ImmutableList.of(queueItem)); @@ -455,6 +463,10 @@ public void onTimelineChanged( assertThat(TextUtils.equals(metadata.subtitle, testSubtitle)).isTrue(); assertThat(TextUtils.equals(metadata.description, testDescription)).isTrue(); assertThat(metadata.artworkUri).isEqualTo(testIconUri); + if (Util.SDK_INT >= 21) { + // Bitmap conversion and back gives not exactly the same byte array below API 21 + assertThat(metadata.artworkData).isEqualTo(testArtworkData); + } if (Util.SDK_INT < 21 || Util.SDK_INT >= 23) { // TODO(b/199055952): Test mediaUri for all API levels once the bug is fixed. assertThat(mediaItem.requestMetadata.mediaUri).isEqualTo(testMediaUri); @@ -579,7 +591,7 @@ public void onPositionDiscontinuity( public void seekToDefaultPosition_withMediaItemIndex_updatesExpectedMediaItemIndex() throws Exception { List testList = MediaTestUtils.createMediaItems(3); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setPlaybackState(/* state= */ null); int testMediaItemIndex = 2; @@ -613,7 +625,7 @@ public void onPositionDiscontinuity( @Test public void seekTo_withMediaItemIndex_updatesExpectedMediaItemIndex() throws Exception { List testList = MediaTestUtils.createMediaItems(3); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setPlaybackState(/* state= */ null); long testPositionMs = 23L; @@ -652,7 +664,7 @@ public void onPositionDiscontinuity( @Test public void getMediaItemCount_withValidQueueAndQueueId_returnsQueueSize() throws Exception { List testList = MediaTestUtils.createMediaItems(3); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setPlaybackState( new PlaybackStateCompat.Builder() @@ -686,7 +698,7 @@ public void getMediaItemCount_withoutQueueButEmptyMetadata_returnsOne() throws E public void getMediaItemCount_withInvalidQueueIdWithoutMetadata_returnsAdjustedCount() throws Exception { List testList = MediaTestUtils.createMediaItems(3); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); MediaController controller = controllerTestRule.createController(session.getSessionToken()); @@ -698,7 +710,7 @@ public void getMediaItemCount_withInvalidQueueIdWithoutMetadata_returnsAdjustedC public void getMediaItemCount_withInvalidQueueIdWithMetadata_returnsAdjustedCount() throws Exception { List testList = MediaTestUtils.createMediaItems(3); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); MediaItem testRemoveMediaItem = MediaTestUtils.createMediaItem("removed"); MediaMetadataCompat testMetadataCompat = MediaUtils.convertToMediaMetadataCompat( @@ -716,7 +728,7 @@ public void getMediaItemCount_withInvalidQueueIdWithMetadata_returnsAdjustedCoun public void getMediaItemCount_whenQueueIdIsChangedFromInvalidToValid_returnOriginalCount() throws Exception { List testList = MediaTestUtils.createMediaItems(3); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); MediaItem testRemoveMediaItem = MediaTestUtils.createMediaItem("removed"); MediaMetadataCompat testMetadataCompat = MediaUtils.convertToMediaMetadataCompat( @@ -751,7 +763,7 @@ public void onTimelineChanged( public void getCurrentMediaItemIndex_withInvalidQueueIdWithMetadata_returnsEndOfList() throws Exception { List testList = MediaTestUtils.createMediaItems(3); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); MediaItem testRemoveMediaItem = MediaTestUtils.createMediaItem("removed"); MediaMetadataCompat testMetadataCompat = MediaUtils.convertToMediaMetadataCompat( @@ -888,7 +900,7 @@ public void getMediaMetadata_withMediaMetadataCompatWithQueue_returnsMediaMetada public void getMediaMetadata_withoutMediaMetadataCompatWithQueue_returnsEmptyMediaMetadata() throws Exception { List testList = MediaTestUtils.createMediaItems(3); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); int testIndex = 1; long testActiveQueueId = testQueue.get(testIndex).getQueueId(); session.setQueue(testQueue); @@ -904,7 +916,7 @@ public void getMediaMetadata_withoutMediaMetadataCompatWithQueue_returnsEmptyMed @Test public void setPlaybackState_withActiveQueueItemId_notifiesCurrentMediaItem() throws Exception { List testList = MediaTestUtils.createMediaItems(/* size= */ 2); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java index 45ee44b3af6..82e4008c4a1 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java @@ -120,7 +120,7 @@ private RemoteMediaController createControllerAndWaitConnection() throws Excepti @Test public void play() throws Exception { List testList = MediaTestUtils.createMediaItems(/* size= */ 2); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); setPlaybackState(PlaybackStateCompat.STATE_PAUSED); @@ -135,7 +135,7 @@ public void play() throws Exception { @Test public void pause() throws Exception { List testList = MediaTestUtils.createMediaItems(/* size= */ 2); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); setPlaybackState(PlaybackStateCompat.STATE_PLAYING); @@ -150,7 +150,7 @@ public void pause() throws Exception { @Test public void prepare() throws Exception { List testList = MediaTestUtils.createMediaItems(/* size= */ 2); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); RemoteMediaController controller = createControllerAndWaitConnection(); @@ -165,7 +165,7 @@ public void prepare() throws Exception { @Test public void stop() throws Exception { List testList = MediaTestUtils.createMediaItems(/* size= */ 2); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); RemoteMediaController controller = createControllerAndWaitConnection(); @@ -328,7 +328,7 @@ public void setPlaybackParameters_withDefault_notifiesOnSetPlaybackSpeedWithDefa public void addMediaItems() throws Exception { int size = 2; List testList = MediaTestUtils.createMediaItems(size); - List testQueue = MediaUtils.convertToQueueItemList(testList); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); @@ -355,7 +355,7 @@ public void removeMediaItems() throws Exception { int toIndex = 3; int count = toIndex - fromIndex; - session.setQueue(MediaUtils.convertToQueueItemList(testList)); + session.setQueue(MediaTestUtils.convertToQueueItemsWithoutBitmap(testList)); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); setPlaybackState(PlaybackStateCompat.STATE_BUFFERING); RemoteMediaController controller = createControllerAndWaitConnection(); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java index d7a8fca105d..8ff1473cbc2 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java @@ -109,15 +109,21 @@ public void convertBrowserItemListToMediaItemList() { } @Test - public void convertToQueueItemList() { - int size = 3; - List mediaItems = MediaTestUtils.createMediaItems(size); - List queueItems = MediaUtils.convertToQueueItemList(mediaItems); - assertThat(queueItems).hasSize(mediaItems.size()); - for (int i = 0; i < size; ++i) { - assertThat(queueItems.get(i).getDescription().getMediaId()) - .isEqualTo(mediaItems.get(i).mediaId); - } + public void convertToQueueItem_withArtworkData() throws Exception { + MediaItem mediaItem = MediaTestUtils.createMediaItemWithArtworkData("testId"); + MediaMetadata mediaMetadata = mediaItem.mediaMetadata; + ListenableFuture bitmapFuture = bitmapLoader.decodeBitmap(mediaMetadata.artworkData); + @Nullable Bitmap bitmap = bitmapFuture.get(10, SECONDS); + + MediaSessionCompat.QueueItem queueItem = + MediaUtils.convertToQueueItem( + mediaItem, + /** mediaItemIndex= */ + 100, + bitmap); + + assertThat(queueItem.getQueueId()).isEqualTo(100); + assertThat(queueItem.getDescription().getIconBitmap()).isNotNull(); } @Test diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java index 2b1cd045ce7..5d9e56a7c70 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java @@ -115,6 +115,28 @@ public static MediaMetadata createMediaMetadata() { .build(); } + public static MediaMetadata createMediaMetadataWithArtworkData() { + MediaMetadata.Builder mediaMetadataBuilder = + new MediaMetadata.Builder() + .setFolderType(MediaMetadata.FOLDER_TYPE_NONE) + .setIsPlayable(false) + .setTitle(METADATA_TITLE) + .setSubtitle(METADATA_SUBTITLE) + .setDescription(METADATA_DESCRIPTION) + .setArtworkUri(METADATA_ARTWORK_URI) + .setExtras(METADATA_EXTRAS); + + try { + byte[] artworkData = + TestUtils.getByteArrayForScaledBitmap( + ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH); + mediaMetadataBuilder.setArtworkData(artworkData, MediaMetadata.PICTURE_TYPE_FRONT_COVER); + } catch (IOException e) { + fail(e.getMessage()); + } + return mediaMetadataBuilder.build(); + } + public static List getTestControllerInfos(MediaSession session) { List infos = new ArrayList<>(); if (session != null) { @@ -163,6 +185,18 @@ public static List createQueueItems(int size) { return list; } + public static List convertToQueueItemsWithoutBitmap( + List mediaItems) { + List list = new ArrayList<>(); + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem item = mediaItems.get(i); + MediaDescriptionCompat description = MediaUtils.convertToMediaDescriptionCompat(item, null); + long id = MediaUtils.convertToQueueItemId(i); + list.add(new MediaSessionCompat.QueueItem(description, id)); + } + return list; + } + public static Timeline createTimeline(int windowCount) { return new PlaylistTimeline(createMediaItems(/* size= */ windowCount)); } From 782a69e38c422e66862e9b2df008613ab438bd8c Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 23 Nov 2022 01:42:43 +0000 Subject: [PATCH 018/141] Migrate BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS to Media3 PiperOrigin-RevId: 490376734 (cherry picked from commit 1803d1cdb8cf429c3d0a3fdbbecbad25145db8c4) --- .../media3/session/MediaConstants.java | 19 +++++ .../androidx/media3/session/MediaUtils.java | 23 ++++++ .../session/common/MediaBrowserConstants.java | 2 + ...wserCompatWithMediaLibraryServiceTest.java | 28 +++++++ ...wserCompatWithMediaSessionServiceTest.java | 5 +- ...enerWithMediaBrowserServiceCompatTest.java | 45 +++++++++++ .../media3/session/MediaUtilsTest.java | 81 ++++++++++++++++++- .../MockMediaBrowserServiceCompat.java | 15 +++- .../session/MockMediaLibraryService.java | 18 +++++ 9 files changed, 230 insertions(+), 6 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java index 0ebcd49075d..d79385f5e46 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java @@ -389,12 +389,31 @@ public final class MediaConstants { * * @see MediaLibrarySession.Callback#onGetLibraryRoot(MediaLibrarySession, * MediaSession.ControllerInfo, LibraryParams) + * @see MediaBrowser#getLibraryRoot(LibraryParams) * @see LibraryParams#extras */ @UnstableApi public static final String EXTRAS_KEY_ROOT_CHILDREN_LIMIT = androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT; + /** + * {@link Bundle} key used in {@link LibraryParams#extras} passed to {@link + * MediaLibrarySession.Callback#onGetLibraryRoot(MediaLibrarySession, MediaSession.ControllerInfo, + * LibraryParams)} to indicate whether only browsable media items are supported as children of the + * root node by the {@link MediaBrowser}. If true, root children that are not browsable may be + * omitted or made less discoverable. + * + *

    TYPE: boolean. + * + * @see MediaLibrarySession.Callback#onGetLibraryRoot(MediaLibrarySession, + * MediaSession.ControllerInfo, LibraryParams) + * @see MediaBrowser#getLibraryRoot(LibraryParams) + * @see LibraryParams#extras + */ + @UnstableApi + public static final String EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY = + "androidx.media3.session.LibraryParams.Extras.KEY_ROOT_CHILDREN_BROWSABLE_ONLY"; + /** * {@link Bundle} key used in {@link LibraryParams#extras} passed by the {@link MediaBrowser} as * root hints to {@link MediaLibrarySession.Callback#onGetLibraryRoot(MediaLibrarySession, diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 97d240032c9..64a25e0eb97 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -16,6 +16,7 @@ package androidx.media3.session; import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; +import static androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS; import static androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME; import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS; import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM; @@ -38,6 +39,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.constrainValue; +import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; import static java.lang.Math.max; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -996,6 +998,15 @@ public static LibraryParams convertToLibraryParams( } try { legacyBundle.setClassLoader(context.getClassLoader()); + int supportedChildrenFlags = + legacyBundle.getInt( + BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, /* defaultValue= */ -1); + if (supportedChildrenFlags >= 0) { + legacyBundle.remove(BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS); + legacyBundle.putBoolean( + EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY, + supportedChildrenFlags == MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); + } return new LibraryParams.Builder() .setExtras(legacyBundle) .setRecent(legacyBundle.getBoolean(BrowserRoot.EXTRA_RECENT)) @@ -1015,6 +1026,18 @@ public static Bundle convertToRootHints(@Nullable LibraryParams params) { return null; } Bundle rootHints = new Bundle(params.extras); + if (params.extras.containsKey(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY)) { + boolean browsableChildrenSupported = + params.extras.getBoolean( + EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY, /* defaultValue= */ false); + rootHints.remove(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY); + rootHints.putInt( + BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, + browsableChildrenSupported + ? MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + : MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + | MediaBrowserCompat.MediaItem.FLAG_PLAYABLE); + } rootHints.putBoolean(BrowserRoot.EXTRA_RECENT, params.isRecent); rootHints.putBoolean(BrowserRoot.EXTRA_OFFLINE, params.isOffline); rootHints.putBoolean(BrowserRoot.EXTRA_SUGGESTED, params.isSuggested); diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java index 65e7847199d..6cf1ce19e16 100644 --- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java +++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java @@ -25,6 +25,8 @@ public class MediaBrowserConstants { public static final String ROOT_ID = "rootId"; + public static final String ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY = + "root_id_supports_browsable_children_only"; public static final Bundle ROOT_EXTRAS = new Bundle(); public static final String ROOT_EXTRAS_KEY = "root_extras_key"; public static final int ROOT_EXTRAS_VALUE = 4321; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java index 4e36a19ece9..4e3ae965074 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java @@ -45,6 +45,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS_KEY; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS_VALUE; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID; +import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY; import static androidx.media3.test.session.common.MediaBrowserConstants.SEARCH_QUERY; import static androidx.media3.test.session.common.MediaBrowserConstants.SEARCH_QUERY_EMPTY_RESULT; import static androidx.media3.test.session.common.MediaBrowserConstants.SEARCH_QUERY_ERROR; @@ -615,4 +616,31 @@ public void rootBrowserHints_searchNotSupported_reportsSearchNotSupported() thro assertThat(isSearchSupported).isFalse(); } + + @Test + public void rootBrowserHints_legacyBrowsableFlagSet_receivesRootWithBrowsableChildrenOnly() + throws Exception { + Bundle rootHints = new Bundle(); + rootHints.putInt( + androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, + MediaItem.FLAG_BROWSABLE); + connectAndWait(rootHints); + + String root = browserCompat.getRoot(); + + assertThat(root).isEqualTo(ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY); + } + + @Test + public void rootBrowserHints_legacyPlayableFlagSet_receivesDefaultRoot() throws Exception { + Bundle connectionHints = new Bundle(); + connectionHints.putInt( + androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, + MediaItem.FLAG_BROWSABLE | MediaItem.FLAG_PLAYABLE); + connectAndWait(connectionHints); + + String root = browserCompat.getRoot(); + + assertThat(root).isEqualTo(ROOT_ID); + } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java index 12caed0f8e1..4199b8f6108 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java @@ -82,13 +82,12 @@ ComponentName getServiceComponent() { return MOCK_MEDIA3_SESSION_SERVICE; } - void connectAndWait(Bundle connectionHints) throws Exception { + void connectAndWait(Bundle rootHints) throws Exception { handler.postAndSync( () -> { // Make browser's internal handler to be initialized with test thread. browserCompat = - new MediaBrowserCompat( - context, getServiceComponent(), connectionCallback, connectionHints); + new MediaBrowserCompat(context, getServiceComponent(), connectionCallback, rootHints); }); browserCompat.connect(); assertThat(connectionCallback.connectedLatch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS)) diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java index d9f8a0afd0d..30bd1c83a2b 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java @@ -18,10 +18,13 @@ import static androidx.media3.session.LibraryResult.RESULT_SUCCESS; import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMPLETION_STATUS; import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED; +import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA_BROWSER_SERVICE_COMPAT; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS_KEY; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS_VALUE; +import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID; +import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_CONNECT_REJECTED; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_LIBRARY_ROOT; @@ -163,6 +166,48 @@ public void getLibraryRoot_correctExtraKeyAndValue() throws Exception { assertThat(extras.getInt(ROOT_EXTRAS_KEY, ROOT_EXTRAS_VALUE + 1)).isEqualTo(ROOT_EXTRAS_VALUE); } + @Test + public void getLibraryRoot_browsableRootChildrenOnly_receivesRootWithBrowsableChildrenOnly() + throws Exception { + remoteService.setProxyForTest(TEST_GET_LIBRARY_ROOT); + MediaBrowser browser = createBrowser(/* listener= */ null); + + LibraryResult resultForLibraryRoot = + threadTestRule + .getHandler() + .postAndSync( + () -> { + Bundle extras = new Bundle(); + extras.putBoolean(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY, true); + return browser.getLibraryRoot( + new LibraryParams.Builder().setExtras(extras).build()); + }) + .get(TIMEOUT_MS, MILLISECONDS); + + assertThat(resultForLibraryRoot.value.mediaId) + .isEqualTo(ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY); + } + + @Test + public void getLibraryRoot_browsableRootChildrenOnlyFalse_receivesDefaultRoot() throws Exception { + remoteService.setProxyForTest(TEST_GET_LIBRARY_ROOT); + MediaBrowser browser = createBrowser(/* listener= */ null); + + LibraryResult resultForLibraryRoot = + threadTestRule + .getHandler() + .postAndSync( + () -> { + Bundle extras = new Bundle(); + extras.putBoolean(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY, false); + return browser.getLibraryRoot( + new LibraryParams.Builder().setExtras(extras).build()); + }) + .get(TIMEOUT_MS, MILLISECONDS); + + assertThat(resultForLibraryRoot.value.mediaId).isEqualTo(ROOT_ID); + } + @Test public void getChildren_correctMetadataExtras() throws Exception { LibraryParams params = MediaTestUtils.createLibraryParams(); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java index 8ff1473cbc2..80dd8073b46 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java @@ -15,8 +15,12 @@ */ package androidx.media3.session; +import static android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_BROWSABLE; +import static android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABLE; import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION; import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; +import static androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS; +import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; @@ -34,6 +38,7 @@ import android.support.v4.media.session.PlaybackStateCompat; import androidx.annotation.Nullable; import androidx.media.AudioAttributesCompat; +import androidx.media.utils.MediaConstants; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.HeartRating; @@ -271,16 +276,54 @@ public void convertToLibraryParams() { assertThat(MediaUtils.convertToLibraryParams(context, null)).isNull(); Bundle rootHints = new Bundle(); rootHints.putString("key", "value"); + rootHints.putInt( + MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, FLAG_BROWSABLE); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_OFFLINE, true); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_SUGGESTED, true); MediaLibraryService.LibraryParams params = MediaUtils.convertToLibraryParams(context, rootHints); + + assertThat(params.extras.getString("key")).isEqualTo("value"); + assertThat(params.extras.getBoolean(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY)).isTrue(); + assertThat(params.extras.containsKey(BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS)) + .isFalse(); assertThat(params.isOffline).isTrue(); assertThat(params.isRecent).isTrue(); assertThat(params.isSuggested).isTrue(); - assertThat(params.extras.getString("key")).isEqualTo("value"); + } + + @Test + public void convertToLibraryParams_rootHintsBrowsableNoFlagSet_browsableOnlyFalse() { + Bundle rootHints = new Bundle(); + rootHints.putInt(MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, 0); + + MediaLibraryService.LibraryParams params = + MediaUtils.convertToLibraryParams(context, rootHints); + + assertThat(params.extras.getBoolean(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY)).isFalse(); + } + + @Test + public void convertToLibraryParams_rootHintsPlayableFlagSet_browsableOnlyFalse() { + Bundle rootHints = new Bundle(); + rootHints.putInt( + MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, + FLAG_PLAYABLE | FLAG_BROWSABLE); + + MediaLibraryService.LibraryParams params = + MediaUtils.convertToLibraryParams(context, rootHints); + + assertThat(params.extras.getBoolean(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY)).isFalse(); + } + + @Test + public void convertToLibraryParams_rootHintsBrowsableAbsentKey_browsableOnlyFalse() { + MediaLibraryService.LibraryParams params = + MediaUtils.convertToLibraryParams(context, /* legacyBundle= */ Bundle.EMPTY); + + assertThat(params.extras.getBoolean(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY)).isFalse(); } @Test @@ -288,6 +331,7 @@ public void convertToRootHints() { assertThat(MediaUtils.convertToRootHints(null)).isNull(); Bundle extras = new Bundle(); extras.putString("key", "value"); + extras.putBoolean(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY, true); MediaLibraryService.LibraryParams param = new MediaLibraryService.LibraryParams.Builder() .setOffline(true) @@ -295,11 +339,44 @@ public void convertToRootHints() { .setSuggested(true) .setExtras(extras) .build(); + Bundle rootHints = MediaUtils.convertToRootHints(param); + + assertThat( + rootHints.getInt( + BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, /* defaultValue= */ 0)) + .isEqualTo(FLAG_BROWSABLE); + assertThat(rootHints.getString("key")).isEqualTo("value"); + assertThat(rootHints.get(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY)).isNull(); assertThat(rootHints.getBoolean(MediaBrowserService.BrowserRoot.EXTRA_OFFLINE)).isTrue(); assertThat(rootHints.getBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT)).isTrue(); assertThat(rootHints.getBoolean(MediaBrowserService.BrowserRoot.EXTRA_SUGGESTED)).isTrue(); - assertThat(rootHints.getString("key")).isEqualTo("value"); + } + + @Test + public void convertToRootHints_browsableOnlyFalse_correctLegacyBrowsableFlags() { + Bundle extras = new Bundle(); + extras.putBoolean(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY, false); + MediaLibraryService.LibraryParams param = + new MediaLibraryService.LibraryParams.Builder().setExtras(extras).build(); + + Bundle rootHints = MediaUtils.convertToRootHints(param); + + assertThat( + rootHints.getInt( + BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, /* defaultValue= */ -1)) + .isEqualTo(FLAG_BROWSABLE | FLAG_PLAYABLE); + assertThat(rootHints.get(EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY)).isNull(); + } + + @Test + public void convertToRootHints_browsableAbsentKey_noLegacyKeyAdded() { + MediaLibraryService.LibraryParams param = + new MediaLibraryService.LibraryParams.Builder().build(); + + Bundle rootHints = MediaUtils.convertToRootHints(param); + + assertThat(rootHints.get(BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS)).isNull(); } @Test diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java index 65ce255a30b..19bce9153d0 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java @@ -22,6 +22,7 @@ import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID; +import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_CONNECT_REJECTED; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_LIBRARY_ROOT; @@ -37,6 +38,7 @@ import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat.Callback; import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; import androidx.media.MediaBrowserServiceCompat; import androidx.media3.common.util.UnstableApi; import androidx.media3.test.session.common.IRemoteMediaBrowserServiceCompat; @@ -303,7 +305,18 @@ private void setProxyForTestGetLibraryRoot_correctExtraKeyAndValue() { new MockMediaBrowserServiceCompat.Proxy() { @Override public BrowserRoot onGetRoot( - String clientPackageName, int clientUid, Bundle rootHints) { + String clientPackageName, int clientUid, @Nullable Bundle rootHints) { + if (rootHints != null) { + // On API levels lower than 21 root hints are null. + int supportedRootChildrenFlags = + rootHints.getInt( + androidx.media.utils.MediaConstants + .BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, + /* defaultValue= */ 0); + if ((supportedRootChildrenFlags == MediaItem.FLAG_BROWSABLE)) { + return new BrowserRoot(ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY, ROOT_EXTRAS); + } + } return new BrowserRoot(ROOT_ID, ROOT_EXTRAS); } }); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java index e3023a00a28..d460bee20b9 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java @@ -20,6 +20,7 @@ import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT; import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT; import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED; +import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; import static androidx.media3.session.MediaTestUtils.assertLibraryParamsEquals; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION; @@ -39,6 +40,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID; +import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY; import static androidx.media3.test.session.common.MediaBrowserConstants.SEARCH_QUERY; import static androidx.media3.test.session.common.MediaBrowserConstants.SEARCH_QUERY_EMPTY_RESULT; import static androidx.media3.test.session.common.MediaBrowserConstants.SEARCH_QUERY_LONG_LIST; @@ -232,6 +234,22 @@ public ListenableFuture> onGetLibraryRoot( .build()) .build(); } + if (params != null) { + boolean browsableRootChildrenOnly = + params.extras.getBoolean( + EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY, /* defaultValue= */ false); + if (browsableRootChildrenOnly) { + rootItem = + new MediaItem.Builder() + .setMediaId(ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY) + .setMediaMetadata( + new MediaMetadata.Builder() + .setFolderType(MediaMetadata.FOLDER_TYPE_MIXED) + .setIsPlayable(false) + .build()) + .build(); + } + } return Futures.immediateFuture(LibraryResult.ofItem(rootItem, ROOT_PARAMS)); } From 9829ff3d4cb58584bbe87a2f04381cc71318ac47 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Nov 2022 09:45:23 +0000 Subject: [PATCH 019/141] Add helper method to convert platform session token to Media3 token This avoids that apps have to depend on the legacy compat support library when they want to make this conversion. Also add a version to both helper methods that takes a Looper to give apps the option to use an existing Looper, which should be much faster than spinning up a new thread for every method call. Issue: androidx/media#171 PiperOrigin-RevId: 490441913 (cherry picked from commit 03f0b53cf823bb4878f884c055b550b52b9b57ab) --- RELEASENOTES.md | 5 + .../androidx/media3/session/MediaSession.java | 5 +- .../session/MediaStyleNotificationHelper.java | 3 +- .../androidx/media3/session/SessionToken.java | 98 ++++++++++++++----- ...ntrollerWithFrameworkMediaSessionTest.java | 7 +- .../media3/session/SessionTokenTest.java | 27 +++++ 6 files changed, 109 insertions(+), 36 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 50b763ca1ae..854f943d895 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,11 @@ selector, hardware decoder with only functional support will be preferred over software decoder that fully supports the format ([#10604](https://github.com/google/ExoPlayer/issues/10604)). + * Add `ExoPlayer.Builder.setPlaybackLooper` that sets a pre-existing + playback thread for a new ExoPlayer instance. +* Session: + * Add helper method to convert platform session token to Media3 + `SessionToken` ([#171](https://github.com/androidx/media/issues/171)). ### 1.0.0-beta03 (2022-11-22) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 2697fd11dd0..9da5c30fe5e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -807,11 +807,10 @@ public ListenableFuture sendCustomCommand( /** * Returns the {@link MediaSessionCompat.Token} of the {@link MediaSessionCompat} created - * internally by this session. You may cast the {@link Object} to {@link - * MediaSessionCompat.Token}. + * internally by this session. */ @UnstableApi - public Object getSessionCompatToken() { + public MediaSessionCompat.Token getSessionCompatToken() { return impl.getSessionCompat().getSessionToken(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java b/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java index b15df339195..739c0baefa1 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java @@ -501,8 +501,7 @@ public static Notification.MediaStyle fillInMediaStyle( if (actionsToShowInCompact != null) { setShowActionsInCompactView(style, actionsToShowInCompact); } - MediaSessionCompat.Token legacyToken = - (MediaSessionCompat.Token) session.getSessionCompatToken(); + MediaSessionCompat.Token legacyToken = session.getSessionCompatToken(); style.setMediaSession((android.media.session.MediaSession.Token) legacyToken.getToken()); return style; } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java index 09c5e61de73..7a25ccc1c9d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java @@ -28,12 +28,14 @@ import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.os.ResultReceiver; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.media.MediaBrowserServiceCompat; import androidx.media3.common.Bundleable; import androidx.media3.common.C; @@ -258,37 +260,86 @@ public Bundle getExtras() { } /** - * Creates a token from {@link MediaSessionCompat.Token}. + * Creates a token from a {@link android.media.session.MediaSession.Token}. * - * @return a {@link ListenableFuture} of {@link SessionToken} + * @param context A {@link Context}. + * @param token The {@link android.media.session.MediaSession.Token}. + * @return A {@link ListenableFuture} for the {@link SessionToken}. */ + @SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 MediaSession. @UnstableApi + @RequiresApi(21) public static ListenableFuture createSessionToken( - Context context, Object compatToken) { - checkNotNull(context, "context must not be null"); - checkNotNull(compatToken, "compatToken must not be null"); - checkArgument(compatToken instanceof MediaSessionCompat.Token); + Context context, android.media.session.MediaSession.Token token) { + return createSessionToken(context, MediaSessionCompat.Token.fromToken(token)); + } + /** + * Creates a token from a {@link android.media.session.MediaSession.Token}. + * + * @param context A {@link Context}. + * @param token The {@link android.media.session.MediaSession.Token}. + * @param completionLooper The {@link Looper} on which the returned {@link ListenableFuture} + * completes. This {@link Looper} can't be used to call {@code future.get()} on the returned + * {@link ListenableFuture}. + * @return A {@link ListenableFuture} for the {@link SessionToken}. + */ + @SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 MediaSession. + @UnstableApi + @RequiresApi(21) + public static ListenableFuture createSessionToken( + Context context, android.media.session.MediaSession.Token token, Looper completionLooper) { + return createSessionToken(context, MediaSessionCompat.Token.fromToken(token), completionLooper); + } + + /** + * Creates a token from a {@link MediaSessionCompat.Token}. + * + * @param context A {@link Context}. + * @param compatToken The {@link MediaSessionCompat.Token}. + * @return A {@link ListenableFuture} for the {@link SessionToken}. + */ + @UnstableApi + public static ListenableFuture createSessionToken( + Context context, MediaSessionCompat.Token compatToken) { HandlerThread thread = new HandlerThread("SessionTokenThread"); thread.start(); + ListenableFuture tokenFuture = + createSessionToken(context, compatToken, thread.getLooper()); + tokenFuture.addListener(thread::quit, MoreExecutors.directExecutor()); + return tokenFuture; + } + + /** + * Creates a token from a {@link MediaSessionCompat.Token}. + * + * @param context A {@link Context}. + * @param compatToken The {@link MediaSessionCompat.Token}. + * @param completionLooper The {@link Looper} on which the returned {@link ListenableFuture} + * completes. This {@link Looper} can't be used to call {@code future.get()} on the returned + * {@link ListenableFuture}. + * @return A {@link ListenableFuture} for the {@link SessionToken}. + */ + @UnstableApi + public static ListenableFuture createSessionToken( + Context context, MediaSessionCompat.Token compatToken, Looper completionLooper) { + checkNotNull(context, "context must not be null"); + checkNotNull(compatToken, "compatToken must not be null"); SettableFuture future = SettableFuture.create(); // Try retrieving media3 token by connecting to the session. - MediaControllerCompat controller = - createMediaControllerCompat(context, (MediaSessionCompat.Token) compatToken); + MediaControllerCompat controller = new MediaControllerCompat(context, compatToken); String packageName = controller.getPackageName(); - Handler handler = new Handler(thread.getLooper()); + Handler handler = new Handler(completionLooper); Runnable createFallbackLegacyToken = () -> { int uid = getUid(context.getPackageManager(), packageName); SessionToken resultToken = - new SessionToken( - (MediaSessionCompat.Token) compatToken, - packageName, - uid, - controller.getSessionInfo()); + new SessionToken(compatToken, packageName, uid, controller.getSessionInfo()); future.set(resultToken); }; + // Post creating a fallback token if the command receives no result after a timeout. + handler.postDelayed(createFallbackLegacyToken, WAIT_TIME_MS_FOR_SESSION3_TOKEN); controller.sendCommand( MediaConstants.SESSION_COMMAND_REQUEST_SESSION3_TOKEN, /* params= */ null, @@ -306,17 +357,13 @@ protected void onReceiveResult(int resultCode, Bundle resultData) { } } }); - // Post creating a fallback token if the command receives no result after a timeout. - handler.postDelayed(createFallbackLegacyToken, WAIT_TIME_MS_FOR_SESSION3_TOKEN); - future.addListener(() -> thread.quit(), MoreExecutors.directExecutor()); return future; } /** - * Returns a {@link ImmutableSet} of {@link SessionToken} for media session services; {@link - * MediaSessionService}, {@link MediaLibraryService}, and {@link MediaBrowserServiceCompat} - * regardless of their activeness. This set represents media apps that publish {@link - * MediaSession}. + * Returns an {@link ImmutableSet} of {@linkplain SessionToken session tokens} for media session + * services; {@link MediaSessionService}, {@link MediaLibraryService}, and {@link + * MediaBrowserServiceCompat} regardless of their activeness. * *

    The app targeting API level 30 or higher must include a {@code } element in their * manifest to get service tokens of other apps. See the following example and * } */ + // We ask the app to declare the tags, so it's expected that they are missing. + @SuppressWarnings("QueryPermissionsNeeded") public static ImmutableSet getAllServiceTokens(Context context) { PackageManager pm = context.getPackageManager(); List services = new ArrayList<>(); @@ -370,6 +419,8 @@ public static ImmutableSet getAllServiceTokens(Context context) { return sessionServiceTokens.build(); } + // We ask the app to declare the tags, so it's expected that they are missing. + @SuppressWarnings("QueryPermissionsNeeded") private static boolean isInterfaceDeclared( PackageManager manager, String serviceInterface, ComponentName serviceComponent) { Intent serviceIntent = new Intent(serviceInterface); @@ -402,11 +453,6 @@ private static int getUid(PackageManager manager, String packageName) { } } - private static MediaControllerCompat createMediaControllerCompat( - Context context, MediaSessionCompat.Token sessionToken) { - return new MediaControllerCompat(context, sessionToken); - } - /* package */ interface SessionTokenImpl extends Bundleable { boolean isLegacySession(); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithFrameworkMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithFrameworkMediaSessionTest.java index 3c9102296c3..6fe9bd89577 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithFrameworkMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithFrameworkMediaSessionTest.java @@ -26,7 +26,6 @@ import android.media.session.PlaybackState; import android.os.Build; import android.os.HandlerThread; -import android.support.v4.media.session.MediaSessionCompat; import androidx.media3.common.Player; import androidx.media3.common.Player.State; import androidx.media3.common.util.Util; @@ -94,8 +93,7 @@ public void cleanUp() { @Test public void createController() throws Exception { SessionToken token = - SessionToken.createSessionToken( - context, MediaSessionCompat.Token.fromToken(fwkSession.getSessionToken())) + SessionToken.createSessionToken(context, fwkSession.getSessionToken()) .get(TIMEOUT_MS, MILLISECONDS); MediaController controller = new MediaController.Builder(context, token) @@ -111,8 +109,7 @@ public void onPlaybackStateChanged_isNotifiedByFwkSessionChanges() throws Except AtomicInteger playbackStateRef = new AtomicInteger(); AtomicBoolean playWhenReadyRef = new AtomicBoolean(); SessionToken token = - SessionToken.createSessionToken( - context, MediaSessionCompat.Token.fromToken(fwkSession.getSessionToken())) + SessionToken.createSessionToken(context, fwkSession.getSessionToken()) .get(TIMEOUT_MS, MILLISECONDS); MediaController controller = new MediaController.Builder(context, token) diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/SessionTokenTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/SessionTokenTest.java index b2d54e2abf7..f0efaea3de2 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/SessionTokenTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/SessionTokenTest.java @@ -20,6 +20,7 @@ import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import android.content.ComponentName; import android.content.Context; @@ -27,6 +28,7 @@ import android.os.Process; import android.support.v4.media.session.MediaSessionCompat; import androidx.media3.common.MediaLibraryInfo; +import androidx.media3.common.util.Util; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.MainLooperTestRule; import androidx.media3.test.session.common.TestUtils; @@ -68,6 +70,7 @@ public void constructor_sessionService() { context, new ComponentName( context.getPackageName(), MockMediaSessionService.class.getCanonicalName())); + assertThat(token.getPackageName()).isEqualTo(context.getPackageName()); assertThat(token.getUid()).isEqualTo(Process.myUid()); assertThat(token.getType()).isEqualTo(SessionToken.TYPE_SESSION_SERVICE); @@ -80,6 +83,7 @@ public void constructor_libraryService() { ComponentName testComponentName = new ComponentName( context.getPackageName(), MockMediaLibraryService.class.getCanonicalName()); + SessionToken token = new SessionToken(context, testComponentName); assertThat(token.getPackageName()).isEqualTo(context.getPackageName()); @@ -110,15 +114,36 @@ public void getters_whenCreatedBySession() { assertThat(token.getServiceName()).isEmpty(); } + @Test + public void createSessionToken_withPlatformTokenFromMedia1Session_returnsTokenForLegacySession() + throws Exception { + assumeTrue(Util.SDK_INT >= 21); + + MediaSessionCompat sessionCompat = + sessionTestRule.ensureReleaseAfterTest( + new MediaSessionCompat(context, "createSessionToken_withLegacyToken")); + + SessionToken token = + SessionToken.createSessionToken( + context, + (android.media.session.MediaSession.Token) + sessionCompat.getSessionToken().getToken()) + .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + + assertThat(token.isLegacySession()).isTrue(); + } + @Test public void createSessionToken_withCompatTokenFromMedia1Session_returnsTokenForLegacySession() throws Exception { MediaSessionCompat sessionCompat = sessionTestRule.ensureReleaseAfterTest( new MediaSessionCompat(context, "createSessionToken_withLegacyToken")); + SessionToken token = SessionToken.createSessionToken(context, sessionCompat.getSessionToken()) .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertThat(token.isLegacySession()).isTrue(); } @@ -150,6 +175,7 @@ public void getSessionServiceTokens() { ComponentName mockBrowserServiceCompatName = new ComponentName( SUPPORT_APP_PACKAGE_NAME, MockMediaBrowserServiceCompat.class.getCanonicalName()); + Set serviceTokens = SessionToken.getAllServiceTokens(ApplicationProvider.getApplicationContext()); for (SessionToken token : serviceTokens) { @@ -162,6 +188,7 @@ public void getSessionServiceTokens() { hasMockLibraryService2 = true; } } + assertThat(hasMockBrowserServiceCompat).isTrue(); assertThat(hasMockSessionService2).isTrue(); assertThat(hasMockLibraryService2).isTrue(); From a98efd8b977c8af6a5a1eee5cb5a49f3e1fadd26 Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Thu, 24 Nov 2022 14:41:58 +0000 Subject: [PATCH 020/141] Merge pull request #10786 from TiVo:p-aacutil-test-impl PiperOrigin-RevId: 490465182 (cherry picked from commit a32b82f7bd14161b4ba204db28ca842f1dd0bb12) --- .../androidx/media3/extractor/AacUtil.java | 7 +- .../media3/extractor/AacUtilTest.java | 65 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 libraries/extractor/src/test/java/androidx/media3/extractor/AacUtilTest.java diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/AacUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/AacUtil.java index 9c72d879667..82f561561b7 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/AacUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/AacUtil.java @@ -332,11 +332,16 @@ private static int getSamplingFrequency(ParsableBitArray bitArray) throws Parser int samplingFrequency; int frequencyIndex = bitArray.readBits(4); if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) { + if (bitArray.bitsLeft() < 24) { + throw ParserException.createForMalformedContainer( + /* message= */ "AAC header insufficient data", /* cause= */ null); + } samplingFrequency = bitArray.readBits(24); } else if (frequencyIndex < 13) { samplingFrequency = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex]; } else { - throw ParserException.createForMalformedContainer(/* message= */ null, /* cause= */ null); + throw ParserException.createForMalformedContainer( + /* message= */ "AAC header wrong Sampling Frequency Index", /* cause= */ null); } return samplingFrequency; } diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/AacUtilTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/AacUtilTest.java new file mode 100644 index 00000000000..f9d71a3cc42 --- /dev/null +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/AacUtilTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.extractor; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import androidx.media3.common.ParserException; +import androidx.media3.common.util.Util; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link AacUtil}. */ +@RunWith(AndroidJUnit4.class) +public final class AacUtilTest { + private static final byte[] AAC_48K_2CH_HEADER = Util.getBytesFromHexString("1190"); + + private static final byte[] NOT_ENOUGH_ARBITRARY_SAMPLING_FREQ_BITS_HEADER = + Util.getBytesFromHexString("1790"); + + private static final byte[] ARBITRARY_SAMPLING_FREQ_BITS_HEADER = + Util.getBytesFromHexString("1780000790"); + + @Test + public void parseAudioSpecificConfig_twoCh48kAac_parsedCorrectly() throws Exception { + AacUtil.Config aac = AacUtil.parseAudioSpecificConfig(AAC_48K_2CH_HEADER); + + assertThat(aac.channelCount).isEqualTo(2); + assertThat(aac.sampleRateHz).isEqualTo(48000); + assertThat(aac.codecs).isEqualTo("mp4a.40.2"); + } + + @Test + public void parseAudioSpecificConfig_arbitrarySamplingFreqHeader_parsedCorrectly() + throws Exception { + AacUtil.Config aac = AacUtil.parseAudioSpecificConfig(ARBITRARY_SAMPLING_FREQ_BITS_HEADER); + assertThat(aac.channelCount).isEqualTo(2); + assertThat(aac.sampleRateHz).isEqualTo(15); + assertThat(aac.codecs).isEqualTo("mp4a.40.2"); + } + + @Test + public void + parseAudioSpecificConfig_arbitrarySamplingFreqHeaderNotEnoughBits_throwsParserException() { + // ISO 14496-3 1.6.2.1 allows for setting of arbitrary sampling frequency, but if the extra + // frequency bits are missing, make sure the code will throw an exception. + assertThrows( + ParserException.class, + () -> AacUtil.parseAudioSpecificConfig(NOT_ENOUGH_ARBITRARY_SAMPLING_FREQ_BITS_HEADER)); + } +} From 0ba58cc6341d59cb4b9033037ca1d1cd4e22fa75 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 23 Nov 2022 13:39:26 +0000 Subject: [PATCH 021/141] Call future listener on the same handler that created the controller The direct executor is not the proper way to determine on what thread to run the `Future.Listener` and the `MediaControllerCreationListener` because the listener may call the controller passed as argument which must happen on the same thread that built the controller. This change makes sure this is the case. PiperOrigin-RevId: 490478587 (cherry picked from commit 68908be18d0a46478be05ad406a5027c15c38723) --- .../java/androidx/media3/session/MediaControllerTestRule.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTestRule.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTestRule.java index d7cb9695851..e2864b8a49a 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTestRule.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTestRule.java @@ -29,7 +29,6 @@ import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.test.core.app.ApplicationProvider; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import java.util.Map; import java.util.concurrent.ExecutionException; import org.junit.rules.ExternalResource; @@ -206,7 +205,7 @@ private MediaController createControllerOnHandler( controllerCreationListener.onCreated(mediaController); } }, - MoreExecutors.directExecutor()); + handlerThreadTestRule.getHandler()::post); } return future.get(timeoutMs, MILLISECONDS); } From 8b0c0761f3fecc57e8f46a6026bd21603758046b Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 23 Nov 2022 14:10:08 +0000 Subject: [PATCH 022/141] Exclude tracks from `PlayerInfo` if not changed This change includes a change in the `IMediaController.aidl` file and needs to provide backwards compatibility for when a client connects that is of an older or newer version of the current service implementation. This CL proposes to create a new AIDL method `onPlayerInfoChangedWithExtensions` that is easier to extend in the future because it does use an `Bundle` rather than primitives. A `Bundle` can be changed in a backward/forwards compatible way in case we need further changes. The compatibility handling is provided in `MediaSessionStub` and `MediaControllerStub`. The approach is not based on specific AIDL/Binder features but implemented fully in application code. Issue: androidx/media#102 #minor-release PiperOrigin-RevId: 490483068 (cherry picked from commit 3d8c52f28d5d3ef04c14868e15036563a9fc662d) --- .../java/androidx/media3/common/Player.java | 3 +- .../media3/session/IMediaController.aidl | 9 +- .../session/MediaControllerImplBase.java | 47 ++++-- .../media3/session/MediaControllerStub.java | 27 +++- .../androidx/media3/session/MediaSession.java | 3 +- .../media3/session/MediaSessionImpl.java | 130 ++++++++++------ .../media3/session/MediaSessionStub.java | 36 +++-- .../androidx/media3/session/MediaUtils.java | 42 ++++++ .../androidx/media3/session/PlayerInfo.java | 4 + .../session/MediaControllerListenerTest.java | 86 +++++------ .../media3/session/MediaUtilsTest.java | 140 ++++++++++++++++++ 11 files changed, 396 insertions(+), 131 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index 5e05ae63014..2fc70006a36 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -677,7 +677,8 @@ default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reas * to the current {@link #getRepeatMode() repeat mode}. * *

    Note that this callback is also called when the playlist becomes non-empty or empty as a - * consequence of a playlist change. + * consequence of a playlist change or {@linkplain #onAvailableCommandsChanged(Commands) a + * change in available commands}. * *

    {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. diff --git a/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl b/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl index d1a348cd3aa..7c1eb001d2d 100644 --- a/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl +++ b/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl @@ -35,14 +35,19 @@ oneway interface IMediaController { void onSetCustomLayout(int seq, in List commandButtonList) = 3003; void onCustomCommand(int seq, in Bundle command, in Bundle args) = 3004; void onDisconnected(int seq) = 3005; - void onPlayerInfoChanged(int seq, in Bundle playerInfoBundle, boolean isTimelineExcluded) = 3006; + /** Deprecated: Use onPlayerInfoChangedWithExclusions from MediaControllerStub#VERSION_INT=2. */ + void onPlayerInfoChanged( + int seq, in Bundle playerInfoBundle, boolean isTimelineExcluded) = 3006; + /** Introduced to deprecate onPlayerInfoChanged (from MediaControllerStub#VERSION_INT=2). */ + void onPlayerInfoChangedWithExclusions( + int seq, in Bundle playerInfoBundle, in Bundle playerInfoExclusions) = 3012; void onPeriodicSessionPositionInfoChanged(int seq, in Bundle sessionPositionInfo) = 3007; void onAvailableCommandsChangedFromPlayer(int seq, in Bundle commandsBundle) = 3008; void onAvailableCommandsChangedFromSession( int seq, in Bundle sessionCommandsBundle, in Bundle playerCommandsBundle) = 3009; void onRenderedFirstFrame(int seq) = 3010; void onExtrasChanged(int seq, in Bundle extras) = 3011; - // Next Id for MediaController: 3012 + // Next Id for MediaController: 3013 void onChildrenChanged( int seq, String parentId, int itemCount, in @nullable Bundle libraryParams) = 4000; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index 4f7940b30a8..6560cea8566 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -23,6 +23,7 @@ import static androidx.media3.common.util.Util.usToMs; import static androidx.media3.session.MediaUtils.calculateBufferedPercentage; import static androidx.media3.session.MediaUtils.intersect; +import static androidx.media3.session.MediaUtils.mergePlayerInfo; import static java.lang.Math.max; import static java.lang.Math.min; @@ -42,6 +43,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.support.v4.media.MediaBrowserCompat; +import android.util.Pair; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -79,6 +81,7 @@ import androidx.media3.common.util.Size; import androidx.media3.common.util.Util; import androidx.media3.session.MediaController.MediaControllerImpl; +import androidx.media3.session.PlayerInfo.BundlingExclusions; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -129,7 +132,8 @@ @Nullable private IMediaSession iSession; private long lastReturnedCurrentPositionMs; private long lastSetPlayWhenReadyCalledTimeMs; - @Nullable private Timeline pendingPlayerInfoUpdateTimeline; + @Nullable private PlayerInfo pendingPlayerInfo; + @Nullable private BundlingExclusions pendingBundlingExclusions; public MediaControllerImplBase( Context context, @@ -2329,30 +2333,41 @@ void onCustomCommand(int seq, SessionCommand command, Bundle args) { } @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method. - void onPlayerInfoChanged(PlayerInfo newPlayerInfo, boolean isTimelineExcluded) { + void onPlayerInfoChanged(PlayerInfo newPlayerInfo, BundlingExclusions bundlingExclusions) { if (!isConnected()) { return; } + if (pendingPlayerInfo != null && pendingBundlingExclusions != null) { + Pair mergedPlayerInfoUpdate = + mergePlayerInfo( + pendingPlayerInfo, + pendingBundlingExclusions, + newPlayerInfo, + bundlingExclusions, + intersectedPlayerCommands); + newPlayerInfo = mergedPlayerInfoUpdate.first; + bundlingExclusions = mergedPlayerInfoUpdate.second; + } + pendingPlayerInfo = null; + pendingBundlingExclusions = null; if (!pendingMaskingSequencedFutureNumbers.isEmpty()) { // We are still waiting for all pending masking operations to be handled. - if (!isTimelineExcluded) { - pendingPlayerInfoUpdateTimeline = newPlayerInfo.timeline; - } + pendingPlayerInfo = newPlayerInfo; + pendingBundlingExclusions = bundlingExclusions; return; } PlayerInfo oldPlayerInfo = playerInfo; - if (isTimelineExcluded) { - newPlayerInfo = - newPlayerInfo.copyWithTimeline( - pendingPlayerInfoUpdateTimeline != null - ? pendingPlayerInfoUpdateTimeline - : oldPlayerInfo.timeline); - } // Assigning class variable now so that all getters called from listeners see the updated value. // But we need to use a local final variable to ensure listeners get consistent parameters. - playerInfo = newPlayerInfo; - PlayerInfo finalPlayerInfo = newPlayerInfo; - pendingPlayerInfoUpdateTimeline = null; + playerInfo = + mergePlayerInfo( + oldPlayerInfo, + /* oldBundlingExclusions= */ BundlingExclusions.NONE, + newPlayerInfo, + /* newBundlingExclusions= */ bundlingExclusions, + intersectedPlayerCommands) + .first; + PlayerInfo finalPlayerInfo = playerInfo; PlaybackException oldPlayerError = oldPlayerInfo.playerError; PlaybackException playerError = finalPlayerInfo.playerError; boolean errorsMatch = @@ -2397,7 +2412,7 @@ void onPlayerInfoChanged(PlayerInfo newPlayerInfo, boolean isTimelineExcluded) { /* eventFlag= */ Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, listener -> listener.onShuffleModeEnabledChanged(finalPlayerInfo.shuffleModeEnabled)); } - if (!isTimelineExcluded && !Util.areEqual(oldPlayerInfo.timeline, finalPlayerInfo.timeline)) { + if (!Util.areEqual(oldPlayerInfo.timeline, finalPlayerInfo.timeline)) { listeners.queueEvent( /* eventFlag= */ Player.EVENT_TIMELINE_CHANGED, listener -> diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java index 59b49bdda53..f9673ccf05d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java @@ -26,6 +26,7 @@ import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.Log; import androidx.media3.session.MediaLibraryService.LibraryParams; +import androidx.media3.session.PlayerInfo.BundlingExclusions; import java.lang.ref.WeakReference; import java.util.List; import org.checkerframework.checker.nullness.qual.NonNull; @@ -35,7 +36,7 @@ private static final String TAG = "MediaControllerStub"; /** The version of the IMediaController interface. */ - public static final int VERSION_INT = 1; + public static final int VERSION_INT = 2; private final WeakReference controller; @@ -169,8 +170,23 @@ public void onPeriodicSessionPositionInfoChanged(int seq, Bundle sessionPosition controller -> controller.notifyPeriodicSessionPositionInfoChanged(sessionPositionInfo)); } + /** + * @deprecated Use {@link #onPlayerInfoChangedWithExclusions} from {@link #VERSION_INT} 2. + */ @Override + @Deprecated public void onPlayerInfoChanged(int seq, Bundle playerInfoBundle, boolean isTimelineExcluded) { + onPlayerInfoChangedWithExclusions( + seq, + playerInfoBundle, + new BundlingExclusions(isTimelineExcluded, /* areCurrentTracksExcluded= */ true) + .toBundle()); + } + + /** Added in {@link #VERSION_INT} 2. */ + @Override + public void onPlayerInfoChangedWithExclusions( + int seq, Bundle playerInfoBundle, Bundle playerInfoExclusions) { PlayerInfo playerInfo; try { playerInfo = PlayerInfo.CREATOR.fromBundle(playerInfoBundle); @@ -178,8 +194,15 @@ public void onPlayerInfoChanged(int seq, Bundle playerInfoBundle, boolean isTime Log.w(TAG, "Ignoring malformed Bundle for PlayerInfo", e); return; } + BundlingExclusions bundlingExclusions; + try { + bundlingExclusions = BundlingExclusions.CREATOR.fromBundle(playerInfoExclusions); + } catch (RuntimeException e) { + Log.w(TAG, "Ignoring malformed Bundle for BundlingExclusions", e); + return; + } dispatchControllerTaskOnHandler( - controller -> controller.onPlayerInfoChanged(playerInfo, isTimelineExcluded)); + controller -> controller.onPlayerInfoChanged(playerInfo, bundlingExclusions)); } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 9da5c30fe5e..6b25c8d56cd 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -1136,7 +1136,8 @@ default void onPlayerInfoChanged( boolean excludeMediaItemsMetadata, boolean excludeCues, boolean excludeTimeline, - boolean excludeTracks) + boolean excludeTracks, + int controllerInterfaceVersion) throws RemoteException {} default void onPeriodicSessionPositionInfoChanged( diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 705115745fd..d01fb6eee3f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -15,6 +15,10 @@ */ package androidx.media3.session; +import static androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA; +import static androidx.media3.common.Player.COMMAND_GET_TEXT; +import static androidx.media3.common.Player.COMMAND_GET_TIMELINE; +import static androidx.media3.common.Player.COMMAND_GET_TRACKS; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.castNonNull; @@ -274,7 +278,8 @@ private void setPlayerInternal( } playerInfo = newPlayerWrapper.createPlayerInfoForBundling(); - onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false); + onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ false, /* excludeTracks= */ false); } public void release() { @@ -374,7 +379,8 @@ public void setAvailableCommands( controller, (callback, seq) -> callback.onAvailableCommandsChangedFromSession(seq, sessionCommands, playerCommands)); - onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false); + onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ false, /* excludeTracks= */ false); } else { sessionLegacyStub .getConnectedControllersManager() @@ -387,7 +393,8 @@ public void broadcastCustomCommand(SessionCommand command, Bundle args) { (controller, seq) -> controller.sendCustomCommand(seq, command, args)); } - private void dispatchOnPlayerInfoChanged(PlayerInfo playerInfo, boolean excludeTimeline) { + private void dispatchOnPlayerInfoChanged( + PlayerInfo playerInfo, boolean excludeTimeline, boolean excludeTracks) { List controllers = sessionStub.getConnectedControllersManager().getConnectedControllers(); @@ -395,8 +402,9 @@ private void dispatchOnPlayerInfoChanged(PlayerInfo playerInfo, boolean excludeT ControllerInfo controller = controllers.get(i); try { int seq; - SequencedFutureManager manager = - sessionStub.getConnectedControllersManager().getSequencedFutureManager(controller); + ConnectedControllersManager controllersManager = + sessionStub.getConnectedControllersManager(); + SequencedFutureManager manager = controllersManager.getSequencedFutureManager(controller); if (manager != null) { seq = manager.obtainNextSequenceNumber(); } else { @@ -410,19 +418,18 @@ private void dispatchOnPlayerInfoChanged(PlayerInfo playerInfo, boolean excludeT .onPlayerInfoChanged( seq, playerInfo, - /* excludeMediaItems= */ !sessionStub - .getConnectedControllersManager() - .isPlayerCommandAvailable(controller, Player.COMMAND_GET_TIMELINE), - /* excludeMediaItemsMetadata= */ !sessionStub - .getConnectedControllersManager() - .isPlayerCommandAvailable(controller, Player.COMMAND_GET_MEDIA_ITEMS_METADATA), - /* excludeCues= */ !sessionStub - .getConnectedControllersManager() - .isPlayerCommandAvailable(controller, Player.COMMAND_GET_TEXT), - excludeTimeline, - /* excludeTracks= */ !sessionStub - .getConnectedControllersManager() - .isPlayerCommandAvailable(controller, Player.COMMAND_GET_TRACKS)); + /* excludeMediaItems= */ !controllersManager.isPlayerCommandAvailable( + controller, COMMAND_GET_TIMELINE), + /* excludeMediaItemsMetadata= */ !controllersManager.isPlayerCommandAvailable( + controller, COMMAND_GET_MEDIA_ITEMS_METADATA), + /* excludeCues= */ !controllersManager.isPlayerCommandAvailable( + controller, COMMAND_GET_TEXT), + excludeTimeline + || !controllersManager.isPlayerCommandAvailable( + controller, COMMAND_GET_TIMELINE), + excludeTracks + || !controllersManager.isPlayerCommandAvailable(controller, COMMAND_GET_TRACKS), + controller.getInterfaceVersion()); } catch (DeadObjectException e) { onDeadObjectException(controller); } catch (RemoteException e) { @@ -745,7 +752,8 @@ public void onPlayerError(PlaybackException error) { return; } session.playerInfo = session.playerInfo.copyWithPlayerError(error); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPlayerError(seq, error)); } @@ -765,7 +773,8 @@ public void onMediaItemTransition( // Note: OK to omit mediaItem here, because PlayerInfo changed message will copy playerInfo // with sessionPositionInfo, which includes current window index. session.playerInfo = session.playerInfo.copyWithMediaItemTransitionReason(reason); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onMediaItemTransition(seq, mediaItem, reason)); } @@ -785,7 +794,8 @@ public void onPlayWhenReadyChanged( session.playerInfo = session.playerInfo.copyWithPlayWhenReady( playWhenReady, reason, session.playerInfo.playbackSuppressionReason); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPlayWhenReadyChanged(seq, playWhenReady, reason)); } @@ -806,7 +816,8 @@ public void onPlaybackSuppressionReasonChanged(@Player.PlaybackSuppressionReason session.playerInfo.playWhenReady, session.playerInfo.playWhenReadyChangedReason, reason); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPlaybackSuppressionReasonChanged(seq, reason)); } @@ -824,7 +835,8 @@ public void onPlaybackStateChanged(@Player.State int playbackState) { } session.playerInfo = session.playerInfo.copyWithPlaybackState(playbackState, player.getPlayerError()); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> { callback.onPlaybackStateChanged(seq, playbackState, player.getPlayerError()); @@ -843,7 +855,8 @@ public void onIsPlayingChanged(boolean isPlaying) { return; } session.playerInfo = session.playerInfo.copyWithIsPlaying(isPlaying); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onIsPlayingChanged(seq, isPlaying)); } @@ -860,7 +873,8 @@ public void onIsLoadingChanged(boolean isLoading) { return; } session.playerInfo = session.playerInfo.copyWithIsLoading(isLoading); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onIsLoadingChanged(seq, isLoading)); } @@ -880,7 +894,8 @@ public void onPositionDiscontinuity( session.playerInfo = session.playerInfo.copyWithPositionInfos(oldPosition, newPosition, reason); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPositionDiscontinuity(seq, oldPosition, newPosition, reason)); @@ -898,7 +913,8 @@ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { return; } session.playerInfo = session.playerInfo.copyWithPlaybackParameters(playbackParameters); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPlaybackParametersChanged(seq, playbackParameters)); } @@ -915,7 +931,8 @@ public void onSeekBackIncrementChanged(long seekBackIncrementMs) { return; } session.playerInfo = session.playerInfo.copyWithSeekBackIncrement(seekBackIncrementMs); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onSeekBackIncrementChanged(seq, seekBackIncrementMs)); } @@ -932,7 +949,8 @@ public void onSeekForwardIncrementChanged(long seekForwardIncrementMs) { return; } session.playerInfo = session.playerInfo.copyWithSeekForwardIncrement(seekForwardIncrementMs); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onSeekForwardIncrementChanged(seq, seekForwardIncrementMs)); } @@ -951,7 +969,8 @@ public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason in session.playerInfo = session.playerInfo.copyWithTimelineAndSessionPositionInfo( timeline, player.createSessionPositionInfoForBundling()); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ false, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onTimelineChanged(seq, timeline, reason)); } @@ -964,7 +983,8 @@ public void onPlaylistMetadataChanged(MediaMetadata playlistMetadata) { } session.verifyApplicationThread(); session.playerInfo = session.playerInfo.copyWithPlaylistMetadata(playlistMetadata); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPlaylistMetadataChanged(seq, playlistMetadata)); } @@ -981,7 +1001,8 @@ public void onRepeatModeChanged(@RepeatMode int repeatMode) { return; } session.playerInfo = session.playerInfo.copyWithRepeatMode(repeatMode); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onRepeatModeChanged(seq, repeatMode)); } @@ -998,7 +1019,8 @@ public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { return; } session.playerInfo = session.playerInfo.copyWithShuffleModeEnabled(shuffleModeEnabled); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onShuffleModeEnabledChanged(seq, shuffleModeEnabled)); } @@ -1015,7 +1037,8 @@ public void onAudioAttributesChanged(AudioAttributes attributes) { return; } session.playerInfo = session.playerInfo.copyWithAudioAttributes(attributes); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (controller, seq) -> controller.onAudioAttributesChanged(seq, attributes)); } @@ -1028,7 +1051,8 @@ public void onVideoSizeChanged(VideoSize size) { } session.verifyApplicationThread(); session.playerInfo = session.playerInfo.copyWithVideoSize(size); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onVideoSizeChanged(seq, size)); } @@ -1041,7 +1065,8 @@ public void onVolumeChanged(@FloatRange(from = 0, to = 1) float volume) { } session.verifyApplicationThread(); session.playerInfo = session.playerInfo.copyWithVolume(volume); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onVolumeChanged(seq, volume)); } @@ -1058,7 +1083,8 @@ public void onCues(CueGroup cueGroup) { return; } session.playerInfo = new PlayerInfo.Builder(session.playerInfo).setCues(cueGroup).build(); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); } @Override @@ -1073,7 +1099,8 @@ public void onDeviceInfoChanged(DeviceInfo deviceInfo) { return; } session.playerInfo = session.playerInfo.copyWithDeviceInfo(deviceInfo); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onDeviceInfoChanged(seq, deviceInfo)); } @@ -1090,7 +1117,8 @@ public void onDeviceVolumeChanged(int volume, boolean muted) { return; } session.playerInfo = session.playerInfo.copyWithDeviceVolume(volume, muted); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onDeviceVolumeChanged(seq, volume, muted)); } @@ -1106,7 +1134,9 @@ public void onAvailableCommandsChanged(Player.Commands availableCommands) { if (player == null) { return; } - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false); + boolean excludeTracks = !availableCommands.contains(COMMAND_GET_TRACKS); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ false, excludeTracks); session.dispatchRemoteControllerTaskWithoutReturn( (callback, seq) -> callback.onAvailableCommandsChangedFromPlayer(seq, availableCommands)); @@ -1128,7 +1158,8 @@ public void onTracksChanged(Tracks tracks) { return; } session.playerInfo = session.playerInfo.copyWithCurrentTracks(tracks); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ false); session.dispatchRemoteControllerTaskWithoutReturn( (callback, seq) -> callback.onTracksChanged(seq, tracks)); } @@ -1145,7 +1176,8 @@ public void onTrackSelectionParametersChanged(TrackSelectionParameters parameter return; } session.playerInfo = session.playerInfo.copyWithTrackSelectionParameters(parameters); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskWithoutReturn( (callback, seq) -> callback.onTrackSelectionParametersChanged(seq, parameters)); } @@ -1162,7 +1194,8 @@ public void onMediaMetadataChanged(MediaMetadata mediaMetadata) { return; } session.playerInfo = session.playerInfo.copyWithMediaMetadata(mediaMetadata); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onMediaMetadataChanged(seq, mediaMetadata)); } @@ -1190,7 +1223,8 @@ public void onMaxSeekToPreviousPositionChanged(long maxSeekToPreviousPositionMs) } session.playerInfo = session.playerInfo.copyWithMaxSeekToPreviousPositionMs(maxSeekToPreviousPositionMs); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); } @Nullable @@ -1224,10 +1258,12 @@ private class PlayerInfoChangedHandler extends Handler { private static final int MSG_PLAYER_INFO_CHANGED = 1; private boolean excludeTimeline; + private boolean excludeTracks; public PlayerInfoChangedHandler(Looper looper) { super(looper); excludeTimeline = true; + excludeTracks = true; } @Override @@ -1237,15 +1273,17 @@ public void handleMessage(Message msg) { playerInfo.copyWithTimelineAndSessionPositionInfo( getPlayerWrapper().getCurrentTimeline(), getPlayerWrapper().createSessionPositionInfoForBundling()); - dispatchOnPlayerInfoChanged(playerInfo, excludeTimeline); + dispatchOnPlayerInfoChanged(playerInfo, excludeTimeline, excludeTracks); excludeTimeline = true; + excludeTracks = true; } else { throw new IllegalStateException("Invalid message what=" + msg.what); } } - public void sendPlayerInfoChangedMessage(boolean excludeTimeline) { + public void sendPlayerInfoChangedMessage(boolean excludeTimeline, boolean excludeTracks) { this.excludeTimeline = this.excludeTimeline && excludeTimeline; + this.excludeTracks = this.excludeTracks && excludeTracks; if (!onPlayerInfoChangedHandler.hasMessages(MSG_PLAYER_INFO_CHANGED)) { onPlayerInfoChangedHandler.sendEmptyMessage(MSG_PLAYER_INFO_CHANGED); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 7160d1e176e..b13b4d61fb5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -70,6 +70,7 @@ import androidx.media3.common.Player; import androidx.media3.common.Rating; import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.util.Assertions; import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.Consumer; import androidx.media3.common.util.Log; @@ -1596,17 +1597,32 @@ public void onPlayerInfoChanged( boolean excludeMediaItemsMetadata, boolean excludeCues, boolean excludeTimeline, - boolean excludeTracks) + boolean excludeTracks, + int controllerInterfaceVersion) throws RemoteException { - iController.onPlayerInfoChanged( - sequenceNumber, - playerInfo.toBundle( - excludeMediaItems, - excludeMediaItemsMetadata, - excludeCues, - excludeTimeline, - excludeTracks), - /* isTimelineExcluded= */ excludeTimeline); + Assertions.checkState(controllerInterfaceVersion != 0); + if (controllerInterfaceVersion >= 2) { + iController.onPlayerInfoChangedWithExclusions( + sequenceNumber, + playerInfo.toBundle( + excludeMediaItems, + excludeMediaItemsMetadata, + excludeCues, + excludeTimeline, + excludeTracks), + new PlayerInfo.BundlingExclusions(excludeTimeline, excludeTracks).toBundle()); + } else { + //noinspection deprecation + iController.onPlayerInfoChanged( + sequenceNumber, + playerInfo.toBundle( + excludeMediaItems, + excludeMediaItemsMetadata, + excludeCues, + excludeTimeline, + /* excludeTracks= */ true), + excludeTimeline); + } } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 64a25e0eb97..f58882351df 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -62,6 +62,7 @@ import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.media.session.PlaybackStateCompat.CustomAction; import android.text.TextUtils; +import android.util.Pair; import androidx.annotation.Nullable; import androidx.media.AudioAttributesCompat; import androidx.media.MediaBrowserServiceCompat.BrowserRoot; @@ -87,6 +88,7 @@ import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import androidx.media3.session.MediaLibraryService.LibraryParams; +import androidx.media3.session.PlayerInfo.BundlingExclusions; import com.google.common.collect.ImmutableList; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -1288,6 +1290,46 @@ public static Commands intersect(Commands commands1, Commands commands2) { return intersectCommandsBuilder.build(); } + /** + * Merges the excluded fields into the {@code newPlayerInfo} by taking the values of the {@code + * previousPlayerInfo} and taking into account the passed available commands. + * + * @param oldPlayerInfo The old {@link PlayerInfo}. + * @param oldBundlingExclusions The bundling exlusions in the old {@link PlayerInfo}. + * @param newPlayerInfo The new {@link PlayerInfo}. + * @param newBundlingExclusions The bundling exlusions in the new {@link PlayerInfo}. + * @param availablePlayerCommands The available commands to take into account when merging. + * @return A pair with the resulting {@link PlayerInfo} and {@link BundlingExclusions}. + */ + public static Pair mergePlayerInfo( + PlayerInfo oldPlayerInfo, + BundlingExclusions oldBundlingExclusions, + PlayerInfo newPlayerInfo, + BundlingExclusions newBundlingExclusions, + Commands availablePlayerCommands) { + PlayerInfo mergedPlayerInfo = newPlayerInfo; + BundlingExclusions mergedBundlingExclusions = newBundlingExclusions; + if (newBundlingExclusions.isTimelineExcluded + && availablePlayerCommands.contains(Player.COMMAND_GET_TIMELINE) + && !oldBundlingExclusions.isTimelineExcluded) { + // Use the previous timeline if it is excluded in the most recent update. + mergedPlayerInfo = mergedPlayerInfo.copyWithTimeline(oldPlayerInfo.timeline); + mergedBundlingExclusions = + new BundlingExclusions( + /* isTimelineExcluded= */ false, mergedBundlingExclusions.areCurrentTracksExcluded); + } + if (newBundlingExclusions.areCurrentTracksExcluded + && availablePlayerCommands.contains(Player.COMMAND_GET_TRACKS) + && !oldBundlingExclusions.areCurrentTracksExcluded) { + // Use the previous tracks if it is excluded in the most recent update. + mergedPlayerInfo = mergedPlayerInfo.copyWithCurrentTracks(oldPlayerInfo.currentTracks); + mergedBundlingExclusions = + new BundlingExclusions( + mergedBundlingExclusions.isTimelineExcluded, /* areCurrentTracksExcluded= */ false); + } + return new Pair<>(mergedPlayerInfo, mergedBundlingExclusions); + } + private static byte[] convertToByteArray(Bitmap bitmap) throws IOException { try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream); diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java index 8fe6eece283..79c780c36e1 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java @@ -66,6 +66,10 @@ */ public static class BundlingExclusions implements Bundleable { + /** Bundling exclusions with no exclusions. */ + public static final BundlingExclusions NONE = + new BundlingExclusions( + /* isTimelineExcluded= */ false, /* areCurrentTracksExcluded= */ false); /** Whether the {@linkplain PlayerInfo#timeline timeline} is excluded. */ public final boolean isTimelineExcluded; /** Whether the {@linkplain PlayerInfo#currentTracks current tracks} are excluded. */ diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java index 8cb138e0f97..13f7d64d4e7 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java @@ -1052,8 +1052,8 @@ public void onTracksChanged() throws Exception { MediaController controller = controllerTestRule.createController(remoteSession.getToken()); AtomicReference changedCurrentTracksFromParamRef = new AtomicReference<>(); AtomicReference changedCurrentTracksFromGetterRef = new AtomicReference<>(); - AtomicReference changedCurrentTracksFromOnEventsRef = new AtomicReference<>(); - AtomicReference eventsRef = new AtomicReference<>(); + List changedCurrentTracksFromOnEvents = new ArrayList<>(); + List capturedEvents = new ArrayList<>(); CountDownLatch latch = new CountDownLatch(2); Player.Listener listener = new Player.Listener() { @@ -1061,13 +1061,12 @@ public void onTracksChanged() throws Exception { public void onTracksChanged(Tracks currentTracks) { changedCurrentTracksFromParamRef.set(currentTracks); changedCurrentTracksFromGetterRef.set(controller.getCurrentTracks()); - latch.countDown(); } @Override public void onEvents(Player player, Player.Events events) { - eventsRef.set(events); - changedCurrentTracksFromOnEventsRef.set(player.getCurrentTracks()); + capturedEvents.add(events); + changedCurrentTracksFromOnEvents.add(player.getCurrentTracks()); latch.countDown(); } }; @@ -1081,13 +1080,22 @@ public void onEvents(Player player, Player.Events events) { }); player.notifyTracksChanged(currentTracks); + player.notifyIsLoadingChanged(true); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(initialCurrentTracksRef.get()).isEqualTo(Tracks.EMPTY); assertThat(changedCurrentTracksFromParamRef.get()).isEqualTo(currentTracks); assertThat(changedCurrentTracksFromGetterRef.get()).isEqualTo(currentTracks); - assertThat(changedCurrentTracksFromOnEventsRef.get()).isEqualTo(currentTracks); - assertThat(getEventsAsList(eventsRef.get())).containsExactly(Player.EVENT_TRACKS_CHANGED); + assertThat(capturedEvents).hasSize(2); + assertThat(getEventsAsList(capturedEvents.get(0))).containsExactly(Player.EVENT_TRACKS_CHANGED); + assertThat(getEventsAsList(capturedEvents.get(1))) + .containsExactly(Player.EVENT_IS_LOADING_CHANGED); + assertThat(changedCurrentTracksFromOnEvents).hasSize(2); + assertThat(changedCurrentTracksFromOnEvents.get(0)).isEqualTo(currentTracks); + assertThat(changedCurrentTracksFromOnEvents.get(1)).isEqualTo(currentTracks); + // Assert that an equal instance is not re-sent over the binder. + assertThat(changedCurrentTracksFromOnEvents.get(0)) + .isSameInstanceAs(changedCurrentTracksFromOnEvents.get(1)); } @Test @@ -1142,6 +1150,9 @@ public void onEvents(Player player, Player.Events events) { assertThat(capturedCurrentTracks).containsExactly(Tracks.EMPTY); assertThat(initialCurrentTracksWithCommandAvailable.get().getGroups()).hasSize(1); assertThat(capturedCurrentTracksWithCommandAvailable.get().getGroups()).hasSize(1); + // Assert that an equal instance is not re-sent over the binder. + assertThat(initialCurrentTracksWithCommandAvailable.get()) + .isSameInstanceAs(capturedCurrentTracksWithCommandAvailable.get()); } @Test @@ -1181,6 +1192,7 @@ public void onTracksChanged(Tracks tracks) { availableCommands.get().buildUpon().remove(Player.COMMAND_GET_TRACKS).build()); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(capturedCurrentTracks).hasSize(2); assertThat(capturedCurrentTracks.get(0).getGroups()).hasSize(1); assertThat(capturedCurrentTracks.get(1)).isEqualTo(Tracks.EMPTY); } @@ -2203,7 +2215,7 @@ public void onEvents(Player player, Player.Events events) { } @Test - public void onTimelineChanged_emptyMediaItemAndMediaMetadata_whenCommandUnavailableFromPlayer() + public void onTimelineChanged_playerCommandUnavailable_emptyTimelineMediaItemAndMetadata() throws Exception { int testMediaItemsSize = 2; List testMediaItemList = MediaTestUtils.createMediaItems(testMediaItemsSize); @@ -2217,7 +2229,7 @@ public void onTimelineChanged_emptyMediaItemAndMediaMetadata_whenCommandUnavaila AtomicReference timelineFromGetterRef = new AtomicReference<>(); List onEventsTimelines = new ArrayList<>(); AtomicReference metadataFromGetterRef = new AtomicReference<>(); - AtomicReference currentMediaItemGetterRef = new AtomicReference<>(); + AtomicReference isCurrentMediaItemNullRef = new AtomicReference<>(); List eventsList = new ArrayList<>(); Player.Listener listener = new Player.Listener() { @@ -2226,7 +2238,7 @@ public void onTimelineChanged(Timeline timeline, int reason) { timelineFromParamRef.set(timeline); timelineFromGetterRef.set(controller.getCurrentTimeline()); metadataFromGetterRef.set(controller.getMediaMetadata()); - currentMediaItemGetterRef.set(controller.getCurrentMediaItem()); + isCurrentMediaItemNullRef.set(controller.getCurrentMediaItem() == null); latch.countDown(); } @@ -2244,24 +2256,7 @@ public void onEvents(Player player, Player.Events events) { remoteSession.getMockPlayer().notifyAvailableCommandsChanged(commandsWithoutGetTimeline); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(timelineFromParamRef.get().getWindowCount()).isEqualTo(testMediaItemsSize); - for (int i = 0; i < timelineFromParamRef.get().getWindowCount(); i++) { - assertThat( - timelineFromParamRef - .get() - .getWindow(/* windowIndex= */ i, new Timeline.Window()) - .mediaItem) - .isEqualTo(MediaItem.EMPTY); - } - assertThat(timelineFromGetterRef.get().getWindowCount()).isEqualTo(testMediaItemsSize); - for (int i = 0; i < timelineFromGetterRef.get().getWindowCount(); i++) { - assertThat( - timelineFromGetterRef - .get() - .getWindow(/* windowIndex= */ i, new Timeline.Window()) - .mediaItem) - .isEqualTo(MediaItem.EMPTY); - } + assertThat(timelineFromParamRef.get()).isEqualTo(Timeline.EMPTY); assertThat(onEventsTimelines).hasSize(2); for (int i = 0; i < onEventsTimelines.get(1).getWindowCount(); i++) { assertThat( @@ -2272,15 +2267,16 @@ public void onEvents(Player player, Player.Events events) { .isEqualTo(MediaItem.EMPTY); } assertThat(metadataFromGetterRef.get()).isEqualTo(MediaMetadata.EMPTY); - assertThat(currentMediaItemGetterRef.get()).isEqualTo(MediaItem.EMPTY); + assertThat(isCurrentMediaItemNullRef.get()).isTrue(); assertThat(eventsList).hasSize(2); assertThat(getEventsAsList(eventsList.get(0))) .containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED); - assertThat(getEventsAsList(eventsList.get(1))).contains(Player.EVENT_TIMELINE_CHANGED); + assertThat(getEventsAsList(eventsList.get(1))) + .containsExactly(Player.EVENT_TIMELINE_CHANGED, Player.EVENT_MEDIA_ITEM_TRANSITION); } @Test - public void onTimelineChanged_emptyMediaItemAndMediaMetadata_whenCommandUnavailableFromSession() + public void onTimelineChanged_sessionCommandUnavailable_emptyTimelineMediaItemAndMetadata() throws Exception { int testMediaItemsSize = 2; List testMediaItemList = MediaTestUtils.createMediaItems(testMediaItemsSize); @@ -2293,7 +2289,7 @@ public void onTimelineChanged_emptyMediaItemAndMediaMetadata_whenCommandUnavaila AtomicReference timelineFromParamRef = new AtomicReference<>(); AtomicReference timelineFromGetterRef = new AtomicReference<>(); AtomicReference metadataFromGetterRef = new AtomicReference<>(); - AtomicReference currentMediaItemGetterRef = new AtomicReference<>(); + AtomicReference isCurrentMediaItemNullRef = new AtomicReference<>(); List eventsList = new ArrayList<>(); Player.Listener listener = new Player.Listener() { @@ -2302,7 +2298,7 @@ public void onTimelineChanged(Timeline timeline, int reason) { timelineFromParamRef.set(timeline); timelineFromGetterRef.set(controller.getCurrentTimeline()); metadataFromGetterRef.set(controller.getMediaMetadata()); - currentMediaItemGetterRef.set(controller.getCurrentMediaItem()); + isCurrentMediaItemNullRef.set(controller.getCurrentMediaItem() == null); latch.countDown(); } @@ -2319,30 +2315,14 @@ public void onEvents(Player player, Player.Events events) { remoteSession.setAvailableCommands(SessionCommands.EMPTY, commandsWithoutGetTimeline); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(timelineFromParamRef.get().getWindowCount()).isEqualTo(testMediaItemsSize); - for (int i = 0; i < timelineFromParamRef.get().getWindowCount(); i++) { - assertThat( - timelineFromParamRef - .get() - .getWindow(/* windowIndex= */ i, new Timeline.Window()) - .mediaItem) - .isEqualTo(MediaItem.EMPTY); - } - assertThat(timelineFromGetterRef.get().getWindowCount()).isEqualTo(testMediaItemsSize); - for (int i = 0; i < timelineFromGetterRef.get().getWindowCount(); i++) { - assertThat( - timelineFromGetterRef - .get() - .getWindow(/* windowIndex= */ i, new Timeline.Window()) - .mediaItem) - .isEqualTo(MediaItem.EMPTY); - } + assertThat(timelineFromParamRef.get()).isEqualTo(Timeline.EMPTY); assertThat(metadataFromGetterRef.get()).isEqualTo(MediaMetadata.EMPTY); - assertThat(currentMediaItemGetterRef.get()).isEqualTo(MediaItem.EMPTY); + assertThat(isCurrentMediaItemNullRef.get()).isTrue(); assertThat(eventsList).hasSize(2); assertThat(getEventsAsList(eventsList.get(0))) .containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED); - assertThat(getEventsAsList(eventsList.get(1))).contains(Player.EVENT_TIMELINE_CHANGED); + assertThat(getEventsAsList(eventsList.get(1))) + .containsExactly(Player.EVENT_TIMELINE_CHANGED, Player.EVENT_MEDIA_ITEM_TRANSITION); } /** This also tests {@link MediaController#getAvailableCommands()}. */ diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java index 80dd8073b46..83c5a4e3f8f 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java @@ -20,6 +20,9 @@ import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION; import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; import static androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS; +import static androidx.media3.common.MimeTypes.AUDIO_AAC; +import static androidx.media3.common.MimeTypes.VIDEO_H264; +import static androidx.media3.common.MimeTypes.VIDEO_H265; import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; @@ -36,11 +39,13 @@ import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Pair; import androidx.annotation.Nullable; import androidx.media.AudioAttributesCompat; import androidx.media.utils.MediaConstants; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; +import androidx.media3.common.Format; import androidx.media3.common.HeartRating; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; @@ -49,10 +54,15 @@ import androidx.media3.common.Rating; import androidx.media3.common.StarRating; import androidx.media3.common.ThumbRating; +import androidx.media3.common.Timeline; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.Tracks; +import androidx.media3.session.PlayerInfo.BundlingExclusions; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SdkSuppress; import androidx.test.filters.SmallTest; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; import java.util.Collections; @@ -623,4 +633,134 @@ public void convertToTotalBufferedDurationMs() { state, /* metadataCompat= */ null, /* timeDiffMs= */ C.INDEX_UNSET); assertThat(totalBufferedDurationMs).isEqualTo(testTotalBufferedDurationMs); } + + @Test + public void mergePlayerInfo_timelineAndTracksExcluded_correctMerge() { + Timeline timeline = + new Timeline.RemotableTimeline( + ImmutableList.of(new Timeline.Window()), + ImmutableList.of(new Timeline.Period()), + /* shuffledWindowIndices= */ new int[] {0}); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()), + /* adaptiveSupported= */ false, + new int[] {C.FORMAT_EXCEEDS_CAPABILITIES}, + /* trackSelected= */ new boolean[] {true}), + new Tracks.Group( + new TrackGroup( + new Format.Builder().setSampleMimeType(VIDEO_H264).build(), + new Format.Builder().setSampleMimeType(VIDEO_H265).build()), + /* adaptiveSupported= */ true, + new int[] {C.FORMAT_HANDLED, C.FORMAT_UNSUPPORTED_TYPE}, + /* trackSelected= */ new boolean[] {false, true}))); + PlayerInfo oldPlayerInfo = + PlayerInfo.DEFAULT.copyWithCurrentTracks(tracks).copyWithTimeline(timeline); + PlayerInfo newPlayerInfo = PlayerInfo.DEFAULT; + Player.Commands availableCommands = + Player.Commands.EMPTY + .buildUpon() + .add(Player.COMMAND_GET_TIMELINE) + .add(Player.COMMAND_GET_TRACKS) + .build(); + + Pair mergeResult = + MediaUtils.mergePlayerInfo( + oldPlayerInfo, + BundlingExclusions.NONE, + newPlayerInfo, + new BundlingExclusions(/* isTimelineExcluded= */ true, /* areTracksExcluded= */ true), + availableCommands); + + assertThat(mergeResult.first.timeline).isSameInstanceAs(oldPlayerInfo.timeline); + assertThat(mergeResult.first.currentTracks).isSameInstanceAs(oldPlayerInfo.currentTracks); + assertThat(mergeResult.second.isTimelineExcluded).isFalse(); + assertThat(mergeResult.second.areCurrentTracksExcluded).isFalse(); + } + + @Test + public void mergePlayerInfo_getTimelineCommandNotAvailable_emptyTimeline() { + Timeline timeline = + new Timeline.RemotableTimeline( + ImmutableList.of(new Timeline.Window()), + ImmutableList.of(new Timeline.Period()), + /* shuffledWindowIndices= */ new int[] {0}); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()), + /* adaptiveSupported= */ false, + new int[] {C.FORMAT_EXCEEDS_CAPABILITIES}, + /* trackSelected= */ new boolean[] {true}), + new Tracks.Group( + new TrackGroup( + new Format.Builder().setSampleMimeType(VIDEO_H264).build(), + new Format.Builder().setSampleMimeType(VIDEO_H265).build()), + /* adaptiveSupported= */ true, + new int[] {C.FORMAT_HANDLED, C.FORMAT_UNSUPPORTED_TYPE}, + /* trackSelected= */ new boolean[] {false, true}))); + PlayerInfo oldPlayerInfo = + PlayerInfo.DEFAULT.copyWithCurrentTracks(tracks).copyWithTimeline(timeline); + PlayerInfo newPlayerInfo = PlayerInfo.DEFAULT; + Player.Commands availableCommands = + Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_GET_TRACKS).build(); + + Pair mergeResult = + MediaUtils.mergePlayerInfo( + oldPlayerInfo, + BundlingExclusions.NONE, + newPlayerInfo, + new BundlingExclusions(/* isTimelineExcluded= */ true, /* areTracksExcluded= */ true), + availableCommands); + + assertThat(mergeResult.first.timeline).isSameInstanceAs(Timeline.EMPTY); + assertThat(mergeResult.first.currentTracks).isSameInstanceAs(oldPlayerInfo.currentTracks); + assertThat(mergeResult.second.isTimelineExcluded).isTrue(); + assertThat(mergeResult.second.areCurrentTracksExcluded).isFalse(); + } + + @Test + public void mergePlayerInfo_getTracksCommandNotAvailable_emptyTracks() { + Timeline timeline = + new Timeline.RemotableTimeline( + ImmutableList.of(new Timeline.Window()), + ImmutableList.of(new Timeline.Period()), + /* shuffledWindowIndices= */ new int[] {0}); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()), + /* adaptiveSupported= */ false, + new int[] {C.FORMAT_EXCEEDS_CAPABILITIES}, + /* trackSelected= */ new boolean[] {true}), + new Tracks.Group( + new TrackGroup( + new Format.Builder().setSampleMimeType(VIDEO_H264).build(), + new Format.Builder().setSampleMimeType(VIDEO_H265).build()), + /* adaptiveSupported= */ true, + new int[] {C.FORMAT_HANDLED, C.FORMAT_UNSUPPORTED_TYPE}, + /* trackSelected= */ new boolean[] {false, true}))); + PlayerInfo oldPlayerInfo = + PlayerInfo.DEFAULT.copyWithCurrentTracks(tracks).copyWithTimeline(timeline); + PlayerInfo newPlayerInfo = PlayerInfo.DEFAULT; + Player.Commands availableCommands = + Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_GET_TIMELINE).build(); + + Pair mergeResult = + MediaUtils.mergePlayerInfo( + oldPlayerInfo, + BundlingExclusions.NONE, + newPlayerInfo, + new BundlingExclusions(/* isTimelineExcluded= */ true, /* areTracksExcluded= */ true), + availableCommands); + + assertThat(mergeResult.first.timeline).isSameInstanceAs(oldPlayerInfo.timeline); + assertThat(mergeResult.first.currentTracks).isSameInstanceAs(Tracks.EMPTY); + assertThat(mergeResult.second.isTimelineExcluded).isFalse(); + assertThat(mergeResult.second.areCurrentTracksExcluded).isTrue(); + } } From b495d21f04ee8dfed4c50c1e7767a103a04b3cfb Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 23 Nov 2022 15:07:16 +0000 Subject: [PATCH 023/141] Misc fix in gradle build file Issue: androidx/media#209 #minor-release PiperOrigin-RevId: 490492223 (cherry picked from commit 2424ee77926923fc1bf690e7e623ff9d57b9a200) --- libraries/test_session_current/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/test_session_current/build.gradle b/libraries/test_session_current/build.gradle index 93c1c99c651..f3fd3ef1ec6 100644 --- a/libraries/test_session_current/build.gradle +++ b/libraries/test_session_current/build.gradle @@ -41,7 +41,7 @@ dependencies { implementation project(modulePrefix + 'test-session-common') implementation 'androidx.media:media:' + androidxMediaVersion implementation 'androidx.test:core:' + androidxTestCoreVersion - implementation project(path: ':test-data') + implementation project(modulePrefix + 'test-data') androidTestImplementation project(modulePrefix + 'lib-exoplayer') androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion androidTestImplementation 'androidx.test.ext:truth:' + androidxTestTruthVersion From d58b4fd6a6405df0e44170decf31f074d8885253 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Wed, 23 Nov 2022 17:40:16 +0000 Subject: [PATCH 024/141] Handle the bitmap loading result with applicationHandler Before this change, the bitmap loading result with mainHandler, in which we set the metadata to `MediaSessionCompat`. However, the `MediaSessionCompat` is not thread safe, all calls should be made from the same thread. In the other calls to `MediaSessionCompat`, we ensure that they are on the application thread (which may be or may not be main thread), so we should do the same for `setMetadata` when bitmap arrives. Also removes a comment in `DefaultMediaNotificationProvider` as bitmap request caching is already moved to CacheBitmapLoader. PiperOrigin-RevId: 490524209 (cherry picked from commit 80927260fd46413b7d1efafed72360b10049af2a) --- .../media3/session/DefaultMediaNotificationProvider.java | 2 -- .../androidx/media3/session/MediaSessionLegacyStub.java | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index 008ba329ebc..45de1f5a943 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -244,8 +244,6 @@ public interface NotificationIdProvider { private final String channelId; @StringRes private final int channelNameResourceId; private final NotificationManager notificationManager; - // Cache the last bitmap load request to avoid reloading the bitmap again, particularly useful - // when showing a notification for the same item (e.g. when switching from playing to paused). private final Handler mainHandler; private @MonotonicNonNull OnBitmapLoadedFutureCallback pendingOnBitmapLoadedFutureCallback; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 516dcf10d66..a80301d5098 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -118,7 +118,6 @@ private final MediaPlayPauseKeyHandler mediaPlayPauseKeyHandler; private final MediaSessionCompat sessionCompat; @Nullable private VolumeProviderCompat volumeProviderCompat; - private final Handler mainHandler; private volatile long connectionTimeoutMs; @Nullable private FutureCallback pendingBitmapLoadCallback; @@ -162,7 +161,6 @@ public MediaSessionLegacyStub( @Initialized MediaSessionLegacyStub thisRef = this; sessionCompat.setCallback(thisRef, handler); - mainHandler = new Handler(Looper.getMainLooper()); } /** Starts to receive commands. */ @@ -1205,7 +1203,9 @@ public void onFailure(Throwable t) { } }; Futures.addCallback( - bitmapFuture, pendingBitmapLoadCallback, /* executor= */ mainHandler::post); + bitmapFuture, + pendingBitmapLoadCallback, + /* executor= */ sessionImpl.getApplicationHandler()::post); } } setMetadata( From 101a2498a0074d293d9db6d74e3264c6d425d8be Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 23 Nov 2022 17:56:50 +0000 Subject: [PATCH 025/141] Parse and set `peakBitrate` for Dolby TrueHD(AC-3) and (E-)AC-3 #minor-release PiperOrigin-RevId: 490527831 (cherry picked from commit 76df06a7a364c580dfe07d9f069237cd77c5174c) --- .../main/java/androidx/media3/extractor/Ac3Util.java | 10 +++++++++- .../assets/extractordumps/mp4/sample_ac3.mp4.0.dump | 2 ++ .../assets/extractordumps/mp4/sample_ac3.mp4.1.dump | 2 ++ .../assets/extractordumps/mp4/sample_ac3.mp4.2.dump | 2 ++ .../assets/extractordumps/mp4/sample_ac3.mp4.3.dump | 2 ++ .../mp4/sample_ac3.mp4.unknown_length.dump | 2 ++ .../mp4/sample_ac3_fragmented.mp4.0.dump | 2 ++ .../mp4/sample_ac3_fragmented.mp4.1.dump | 2 ++ .../mp4/sample_ac3_fragmented.mp4.2.dump | 2 ++ .../mp4/sample_ac3_fragmented.mp4.3.dump | 2 ++ .../mp4/sample_ac3_fragmented.mp4.unknown_length.dump | 2 ++ .../assets/extractordumps/mp4/sample_eac3.mp4.0.dump | 1 + .../assets/extractordumps/mp4/sample_eac3.mp4.1.dump | 1 + .../assets/extractordumps/mp4/sample_eac3.mp4.2.dump | 1 + .../assets/extractordumps/mp4/sample_eac3.mp4.3.dump | 1 + .../mp4/sample_eac3.mp4.unknown_length.dump | 1 + .../mp4/sample_eac3_fragmented.mp4.0.dump | 1 + .../mp4/sample_eac3_fragmented.mp4.1.dump | 1 + .../mp4/sample_eac3_fragmented.mp4.2.dump | 1 + .../mp4/sample_eac3_fragmented.mp4.3.dump | 1 + .../mp4/sample_eac3_fragmented.mp4.unknown_length.dump | 1 + .../extractordumps/mp4/sample_eac3joc.mp4.0.dump | 1 + .../extractordumps/mp4/sample_eac3joc.mp4.1.dump | 1 + .../extractordumps/mp4/sample_eac3joc.mp4.2.dump | 1 + .../extractordumps/mp4/sample_eac3joc.mp4.3.dump | 1 + .../mp4/sample_eac3joc.mp4.unknown_length.dump | 1 + .../mp4/sample_eac3joc_fragmented.mp4.0.dump | 1 + .../mp4/sample_eac3joc_fragmented.mp4.1.dump | 1 + .../mp4/sample_eac3joc_fragmented.mp4.2.dump | 1 + .../mp4/sample_eac3joc_fragmented.mp4.3.dump | 1 + .../sample_eac3joc_fragmented.mp4.unknown_length.dump | 1 + 31 files changed, 49 insertions(+), 1 deletion(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java index e4a61f3e0b7..cfbe95a6119 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java @@ -158,6 +158,9 @@ public static Format parseAc3AnnexFFormat( if ((nextByte & 0x04) != 0) { // lfeon channelCount++; } + // bit_rate_code - 5 bits. 2 bits from previous byte and 3 bits from next. + int halfFrmsizecod = ((nextByte & 0x03) << 3) | ((data.readUnsignedByte() & 0xE0) >> 5); + int constantBitrate = BITRATE_BY_HALF_FRMSIZECOD[halfFrmsizecod]; return new Format.Builder() .setId(trackId) .setSampleMimeType(MimeTypes.AUDIO_AC3) @@ -165,6 +168,8 @@ public static Format parseAc3AnnexFFormat( .setSampleRate(sampleRate) .setDrmInitData(drmInitData) .setLanguage(language) + .setAverageBitrate(constantBitrate) + .setPeakBitrate(constantBitrate) .build(); } @@ -180,7 +185,9 @@ public static Format parseAc3AnnexFFormat( */ public static Format parseEAc3AnnexFFormat( ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { - data.skipBytes(2); // data_rate, num_ind_sub + // 13 bits for data_rate, 3 bits for num_ind_sub which are ignored. + int peakBitrate = + ((data.readUnsignedByte() & 0xFF) << 5) | ((data.readUnsignedByte() & 0xF8) >> 3); // Read the first independent substream. int fscod = (data.readUnsignedByte() & 0xC0) >> 6; @@ -216,6 +223,7 @@ public static Format parseEAc3AnnexFFormat( .setSampleRate(sampleRate) .setDrmInitData(drmInitData) .setLanguage(language) + .setPeakBitrate(peakBitrate) .build(); } diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump index c2e51faaef6..71eed666b7a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump index 80f0790cd09..a6fbd97784b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 9216 sample count = 6 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump index a8d1588940d..e02699e2de5 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 4608 sample count = 3 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump index 17bf79c850c..4b7e17e7c92 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 1536 sample count = 1 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump index c2e51faaef6..71eed666b7a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump index 3724592554b..84217c2e015 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump index e9019d4ab15..1edd06253fc 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 10752 sample count = 7 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump index 2b9cb1cd529..01fd6af916d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 6144 sample count = 4 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump index eb313f941d8..c303da0e156 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 1536 sample count = 1 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump index 3724592554b..84217c2e015 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump index 80008645762..aba5268ea29 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump index 49ab3da0aa0..ac03cfd4847 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 144000 sample count = 36 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump index 19bfc7c5fa0..1a61f528ac6 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 72000 sample count = 18 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump index d34514d8a8f..431599a9bef 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 4000 sample count = 1 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump index 80008645762..aba5268ea29 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump index a7f3c63f8da..6da60d472a4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump index a627d006336..646dd35d91a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 148000 sample count = 37 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump index 31013410b61..a7ba576bf58 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 76000 sample count = 19 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump index 13ff558eaac..280d6febc4e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 4000 sample count = 1 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump index a7f3c63f8da..6da60d472a4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump index ecc28b72088..c98e27dc190 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump index d9ed0c417d7..9c9cee29dfb 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 110080 sample count = 43 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump index 741d5199ea6..85c07f6d2d9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 56320 sample count = 22 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump index 98fe8c793d0..56387fb3c78 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 2560 sample count = 1 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump index ecc28b72088..c98e27dc190 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump index c5902f5d19e..c73a6282e8e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump index 8fa0cbf7feb..78b392053ee 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 110080 sample count = 43 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump index 603ca0de80d..25583633429 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 56320 sample count = 22 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump index cd42dac9175..084d2aa030c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 2560 sample count = 1 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump index c5902f5d19e..c73a6282e8e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 From 32f7a8b807b46f4234621e41e53c20ec0322580f Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 23 Nov 2022 21:15:09 +0000 Subject: [PATCH 026/141] Rollback of https://github.com/androidx/media/commit/76df06a7a364c580dfe07d9f069237cd77c5174c *** Original commit *** Parse and set `peakBitrate` for Dolby TrueHD(AC-3) and (E-)AC-3 #minor-release *** PiperOrigin-RevId: 490570517 (cherry picked from commit 427329175e87a7f3173791c59e6c2d4c4ed8dea4) --- .../main/java/androidx/media3/extractor/Ac3Util.java | 10 +--------- .../assets/extractordumps/mp4/sample_ac3.mp4.0.dump | 2 -- .../assets/extractordumps/mp4/sample_ac3.mp4.1.dump | 2 -- .../assets/extractordumps/mp4/sample_ac3.mp4.2.dump | 2 -- .../assets/extractordumps/mp4/sample_ac3.mp4.3.dump | 2 -- .../mp4/sample_ac3.mp4.unknown_length.dump | 2 -- .../mp4/sample_ac3_fragmented.mp4.0.dump | 2 -- .../mp4/sample_ac3_fragmented.mp4.1.dump | 2 -- .../mp4/sample_ac3_fragmented.mp4.2.dump | 2 -- .../mp4/sample_ac3_fragmented.mp4.3.dump | 2 -- .../mp4/sample_ac3_fragmented.mp4.unknown_length.dump | 2 -- .../assets/extractordumps/mp4/sample_eac3.mp4.0.dump | 1 - .../assets/extractordumps/mp4/sample_eac3.mp4.1.dump | 1 - .../assets/extractordumps/mp4/sample_eac3.mp4.2.dump | 1 - .../assets/extractordumps/mp4/sample_eac3.mp4.3.dump | 1 - .../mp4/sample_eac3.mp4.unknown_length.dump | 1 - .../mp4/sample_eac3_fragmented.mp4.0.dump | 1 - .../mp4/sample_eac3_fragmented.mp4.1.dump | 1 - .../mp4/sample_eac3_fragmented.mp4.2.dump | 1 - .../mp4/sample_eac3_fragmented.mp4.3.dump | 1 - .../mp4/sample_eac3_fragmented.mp4.unknown_length.dump | 1 - .../extractordumps/mp4/sample_eac3joc.mp4.0.dump | 1 - .../extractordumps/mp4/sample_eac3joc.mp4.1.dump | 1 - .../extractordumps/mp4/sample_eac3joc.mp4.2.dump | 1 - .../extractordumps/mp4/sample_eac3joc.mp4.3.dump | 1 - .../mp4/sample_eac3joc.mp4.unknown_length.dump | 1 - .../mp4/sample_eac3joc_fragmented.mp4.0.dump | 1 - .../mp4/sample_eac3joc_fragmented.mp4.1.dump | 1 - .../mp4/sample_eac3joc_fragmented.mp4.2.dump | 1 - .../mp4/sample_eac3joc_fragmented.mp4.3.dump | 1 - .../sample_eac3joc_fragmented.mp4.unknown_length.dump | 1 - 31 files changed, 1 insertion(+), 49 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java index cfbe95a6119..e4a61f3e0b7 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java @@ -158,9 +158,6 @@ public static Format parseAc3AnnexFFormat( if ((nextByte & 0x04) != 0) { // lfeon channelCount++; } - // bit_rate_code - 5 bits. 2 bits from previous byte and 3 bits from next. - int halfFrmsizecod = ((nextByte & 0x03) << 3) | ((data.readUnsignedByte() & 0xE0) >> 5); - int constantBitrate = BITRATE_BY_HALF_FRMSIZECOD[halfFrmsizecod]; return new Format.Builder() .setId(trackId) .setSampleMimeType(MimeTypes.AUDIO_AC3) @@ -168,8 +165,6 @@ public static Format parseAc3AnnexFFormat( .setSampleRate(sampleRate) .setDrmInitData(drmInitData) .setLanguage(language) - .setAverageBitrate(constantBitrate) - .setPeakBitrate(constantBitrate) .build(); } @@ -185,9 +180,7 @@ public static Format parseAc3AnnexFFormat( */ public static Format parseEAc3AnnexFFormat( ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { - // 13 bits for data_rate, 3 bits for num_ind_sub which are ignored. - int peakBitrate = - ((data.readUnsignedByte() & 0xFF) << 5) | ((data.readUnsignedByte() & 0xF8) >> 3); + data.skipBytes(2); // data_rate, num_ind_sub // Read the first independent substream. int fscod = (data.readUnsignedByte() & 0xC0) >> 6; @@ -223,7 +216,6 @@ public static Format parseEAc3AnnexFFormat( .setSampleRate(sampleRate) .setDrmInitData(drmInitData) .setLanguage(language) - .setPeakBitrate(peakBitrate) .build(); } diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump index 71eed666b7a..c2e51faaef6 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump @@ -10,8 +10,6 @@ track 0: total output bytes = 13824 sample count = 9 format 0: - averageBitrate = 384 - peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump index a6fbd97784b..80f0790cd09 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump @@ -10,8 +10,6 @@ track 0: total output bytes = 9216 sample count = 6 format 0: - averageBitrate = 384 - peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump index e02699e2de5..a8d1588940d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump @@ -10,8 +10,6 @@ track 0: total output bytes = 4608 sample count = 3 format 0: - averageBitrate = 384 - peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump index 4b7e17e7c92..17bf79c850c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump @@ -10,8 +10,6 @@ track 0: total output bytes = 1536 sample count = 1 format 0: - averageBitrate = 384 - peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump index 71eed666b7a..c2e51faaef6 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump @@ -10,8 +10,6 @@ track 0: total output bytes = 13824 sample count = 9 format 0: - averageBitrate = 384 - peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump index 84217c2e015..3724592554b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump @@ -10,8 +10,6 @@ track 0: total output bytes = 13824 sample count = 9 format 0: - averageBitrate = 384 - peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump index 1edd06253fc..e9019d4ab15 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump @@ -10,8 +10,6 @@ track 0: total output bytes = 10752 sample count = 7 format 0: - averageBitrate = 384 - peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump index 01fd6af916d..2b9cb1cd529 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump @@ -10,8 +10,6 @@ track 0: total output bytes = 6144 sample count = 4 format 0: - averageBitrate = 384 - peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump index c303da0e156..eb313f941d8 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump @@ -10,8 +10,6 @@ track 0: total output bytes = 1536 sample count = 1 format 0: - averageBitrate = 384 - peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump index 84217c2e015..3724592554b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump @@ -10,8 +10,6 @@ track 0: total output bytes = 13824 sample count = 9 format 0: - averageBitrate = 384 - peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump index aba5268ea29..80008645762 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 216000 sample count = 54 format 0: - peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump index ac03cfd4847..49ab3da0aa0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 144000 sample count = 36 format 0: - peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump index 1a61f528ac6..19bfc7c5fa0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 72000 sample count = 18 format 0: - peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump index 431599a9bef..d34514d8a8f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 4000 sample count = 1 format 0: - peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump index aba5268ea29..80008645762 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 216000 sample count = 54 format 0: - peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump index 6da60d472a4..a7f3c63f8da 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 216000 sample count = 54 format 0: - peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump index 646dd35d91a..a627d006336 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 148000 sample count = 37 format 0: - peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump index a7ba576bf58..31013410b61 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 76000 sample count = 19 format 0: - peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump index 280d6febc4e..13ff558eaac 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 4000 sample count = 1 format 0: - peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump index 6da60d472a4..a7f3c63f8da 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 216000 sample count = 54 format 0: - peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump index c98e27dc190..ecc28b72088 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 163840 sample count = 64 format 0: - peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump index 9c9cee29dfb..d9ed0c417d7 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 110080 sample count = 43 format 0: - peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump index 85c07f6d2d9..741d5199ea6 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 56320 sample count = 22 format 0: - peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump index 56387fb3c78..98fe8c793d0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 2560 sample count = 1 format 0: - peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump index c98e27dc190..ecc28b72088 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 163840 sample count = 64 format 0: - peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump index c73a6282e8e..c5902f5d19e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 163840 sample count = 64 format 0: - peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump index 78b392053ee..8fa0cbf7feb 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 110080 sample count = 43 format 0: - peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump index 25583633429..603ca0de80d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 56320 sample count = 22 format 0: - peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump index 084d2aa030c..cd42dac9175 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 2560 sample count = 1 format 0: - peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump index c73a6282e8e..c5902f5d19e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump @@ -10,7 +10,6 @@ track 0: total output bytes = 163840 sample count = 64 format 0: - peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 From fda132f92685a32ffbf7a8f675b83250bdce7ddd Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 24 Nov 2022 12:13:31 +0000 Subject: [PATCH 027/141] Rollback of https://github.com/androidx/media/commit/427329175e87a7f3173791c59e6c2d4c4ed8dea4 *** Original commit *** Rollback of https://github.com/androidx/media/commit/76df06a7a364c580dfe07d9f069237cd77c5174c *** Original commit *** Parse and set `peakBitrate` for Dolby TrueHD(AC-3) and (E-)AC-3 #minor-release *** *** PiperOrigin-RevId: 490707234 (cherry picked from commit 82711630ed1afbe7417aad95244a91135e24c27f) --- .../main/java/androidx/media3/extractor/Ac3Util.java | 10 +++++++++- .../assets/extractordumps/mp4/sample_ac3.mp4.0.dump | 2 ++ .../assets/extractordumps/mp4/sample_ac3.mp4.1.dump | 2 ++ .../assets/extractordumps/mp4/sample_ac3.mp4.2.dump | 2 ++ .../assets/extractordumps/mp4/sample_ac3.mp4.3.dump | 2 ++ .../mp4/sample_ac3.mp4.unknown_length.dump | 2 ++ .../mp4/sample_ac3_fragmented.mp4.0.dump | 2 ++ .../mp4/sample_ac3_fragmented.mp4.1.dump | 2 ++ .../mp4/sample_ac3_fragmented.mp4.2.dump | 2 ++ .../mp4/sample_ac3_fragmented.mp4.3.dump | 2 ++ .../mp4/sample_ac3_fragmented.mp4.unknown_length.dump | 2 ++ .../assets/extractordumps/mp4/sample_eac3.mp4.0.dump | 1 + .../assets/extractordumps/mp4/sample_eac3.mp4.1.dump | 1 + .../assets/extractordumps/mp4/sample_eac3.mp4.2.dump | 1 + .../assets/extractordumps/mp4/sample_eac3.mp4.3.dump | 1 + .../mp4/sample_eac3.mp4.unknown_length.dump | 1 + .../mp4/sample_eac3_fragmented.mp4.0.dump | 1 + .../mp4/sample_eac3_fragmented.mp4.1.dump | 1 + .../mp4/sample_eac3_fragmented.mp4.2.dump | 1 + .../mp4/sample_eac3_fragmented.mp4.3.dump | 1 + .../mp4/sample_eac3_fragmented.mp4.unknown_length.dump | 1 + .../extractordumps/mp4/sample_eac3joc.mp4.0.dump | 1 + .../extractordumps/mp4/sample_eac3joc.mp4.1.dump | 1 + .../extractordumps/mp4/sample_eac3joc.mp4.2.dump | 1 + .../extractordumps/mp4/sample_eac3joc.mp4.3.dump | 1 + .../mp4/sample_eac3joc.mp4.unknown_length.dump | 1 + .../mp4/sample_eac3joc_fragmented.mp4.0.dump | 1 + .../mp4/sample_eac3joc_fragmented.mp4.1.dump | 1 + .../mp4/sample_eac3joc_fragmented.mp4.2.dump | 1 + .../mp4/sample_eac3joc_fragmented.mp4.3.dump | 1 + .../sample_eac3joc_fragmented.mp4.unknown_length.dump | 1 + 31 files changed, 49 insertions(+), 1 deletion(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java index e4a61f3e0b7..cfbe95a6119 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java @@ -158,6 +158,9 @@ public static Format parseAc3AnnexFFormat( if ((nextByte & 0x04) != 0) { // lfeon channelCount++; } + // bit_rate_code - 5 bits. 2 bits from previous byte and 3 bits from next. + int halfFrmsizecod = ((nextByte & 0x03) << 3) | ((data.readUnsignedByte() & 0xE0) >> 5); + int constantBitrate = BITRATE_BY_HALF_FRMSIZECOD[halfFrmsizecod]; return new Format.Builder() .setId(trackId) .setSampleMimeType(MimeTypes.AUDIO_AC3) @@ -165,6 +168,8 @@ public static Format parseAc3AnnexFFormat( .setSampleRate(sampleRate) .setDrmInitData(drmInitData) .setLanguage(language) + .setAverageBitrate(constantBitrate) + .setPeakBitrate(constantBitrate) .build(); } @@ -180,7 +185,9 @@ public static Format parseAc3AnnexFFormat( */ public static Format parseEAc3AnnexFFormat( ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { - data.skipBytes(2); // data_rate, num_ind_sub + // 13 bits for data_rate, 3 bits for num_ind_sub which are ignored. + int peakBitrate = + ((data.readUnsignedByte() & 0xFF) << 5) | ((data.readUnsignedByte() & 0xF8) >> 3); // Read the first independent substream. int fscod = (data.readUnsignedByte() & 0xC0) >> 6; @@ -216,6 +223,7 @@ public static Format parseEAc3AnnexFFormat( .setSampleRate(sampleRate) .setDrmInitData(drmInitData) .setLanguage(language) + .setPeakBitrate(peakBitrate) .build(); } diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump index c2e51faaef6..71eed666b7a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump index 80f0790cd09..a6fbd97784b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 9216 sample count = 6 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump index a8d1588940d..e02699e2de5 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 4608 sample count = 3 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump index 17bf79c850c..4b7e17e7c92 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 1536 sample count = 1 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump index c2e51faaef6..71eed666b7a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump index 3724592554b..84217c2e015 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump index e9019d4ab15..1edd06253fc 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 10752 sample count = 7 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump index 2b9cb1cd529..01fd6af916d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 6144 sample count = 4 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump index eb313f941d8..c303da0e156 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 1536 sample count = 1 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump index 3724592554b..84217c2e015 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump @@ -10,6 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: + averageBitrate = 384 + peakBitrate = 384 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump index 80008645762..aba5268ea29 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump index 49ab3da0aa0..ac03cfd4847 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 144000 sample count = 36 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump index 19bfc7c5fa0..1a61f528ac6 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 72000 sample count = 18 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump index d34514d8a8f..431599a9bef 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 4000 sample count = 1 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump index 80008645762..aba5268ea29 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump index a7f3c63f8da..6da60d472a4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump index a627d006336..646dd35d91a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 148000 sample count = 37 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump index 31013410b61..a7ba576bf58 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 76000 sample count = 19 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump index 13ff558eaac..280d6febc4e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 4000 sample count = 1 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump index a7f3c63f8da..6da60d472a4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 1000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump index ecc28b72088..c98e27dc190 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump index d9ed0c417d7..9c9cee29dfb 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 110080 sample count = 43 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump index 741d5199ea6..85c07f6d2d9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 56320 sample count = 22 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump index 98fe8c793d0..56387fb3c78 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 2560 sample count = 1 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump index ecc28b72088..c98e27dc190 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump index c5902f5d19e..c73a6282e8e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump index 8fa0cbf7feb..78b392053ee 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 110080 sample count = 43 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump index 603ca0de80d..25583633429 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 56320 sample count = 22 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump index cd42dac9175..084d2aa030c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 2560 sample count = 1 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump index c5902f5d19e..c73a6282e8e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump @@ -10,6 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 From 5ebbdc52cb32b54ca48a0a791521681d898d21fa Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 24 Nov 2022 17:15:07 +0000 Subject: [PATCH 028/141] Use `ParsableBitArray` instead of `ParsableByteArray` To avoid complicated bit shifting and masking. Also makes the code more readable. PiperOrigin-RevId: 490749482 (cherry picked from commit 3d31e094a9e802354dce2f3dc5f33062f7624248) --- .../androidx/media3/extractor/Ac3Util.java | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java index cfbe95a6119..78baffd5b56 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java @@ -151,16 +151,21 @@ private SyncFrameInfo( */ public static Format parseAc3AnnexFFormat( ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { - int fscod = (data.readUnsignedByte() & 0xC0) >> 6; + ParsableBitArray dataBitArray = new ParsableBitArray(); + dataBitArray.reset(data); + + int fscod = dataBitArray.readBits(2); int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; - int nextByte = data.readUnsignedByte(); - int channelCount = CHANNEL_COUNT_BY_ACMOD[(nextByte & 0x38) >> 3]; - if ((nextByte & 0x04) != 0) { // lfeon + dataBitArray.skipBits(8); // bsid, bsmod + int channelCount = CHANNEL_COUNT_BY_ACMOD[dataBitArray.readBits(3)]; // acmod + if (dataBitArray.readBits(1) != 0) { // lfeon channelCount++; } - // bit_rate_code - 5 bits. 2 bits from previous byte and 3 bits from next. - int halfFrmsizecod = ((nextByte & 0x03) << 3) | ((data.readUnsignedByte() & 0xE0) >> 5); + int halfFrmsizecod = dataBitArray.readBits(5); // bit_rate_code int constantBitrate = BITRATE_BY_HALF_FRMSIZECOD[halfFrmsizecod]; + // Update data position + dataBitArray.byteAlign(); + data.setPosition(dataBitArray.getBytePosition()); return new Format.Builder() .setId(trackId) .setSampleMimeType(MimeTypes.AUDIO_AC3) @@ -185,37 +190,45 @@ public static Format parseAc3AnnexFFormat( */ public static Format parseEAc3AnnexFFormat( ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { - // 13 bits for data_rate, 3 bits for num_ind_sub which are ignored. - int peakBitrate = - ((data.readUnsignedByte() & 0xFF) << 5) | ((data.readUnsignedByte() & 0xF8) >> 3); + ParsableBitArray dataBitArray = new ParsableBitArray(); + dataBitArray.reset(data); + + int peakBitrate = dataBitArray.readBits(13); // data_rate + dataBitArray.skipBits(3); // num_ind_sub // Read the first independent substream. - int fscod = (data.readUnsignedByte() & 0xC0) >> 6; + int fscod = dataBitArray.readBits(2); int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; - int nextByte = data.readUnsignedByte(); - int channelCount = CHANNEL_COUNT_BY_ACMOD[(nextByte & 0x0E) >> 1]; - if ((nextByte & 0x01) != 0) { // lfeon + dataBitArray.skipBits(10); // bsid, reserved, asvc, bsmod + int channelCount = CHANNEL_COUNT_BY_ACMOD[dataBitArray.readBits(3)]; // acmod + if (dataBitArray.readBits(1) != 0) { // lfeon channelCount++; } // Read the first dependent substream. - nextByte = data.readUnsignedByte(); - int numDepSub = ((nextByte & 0x1E) >> 1); + dataBitArray.skipBits(3); // reserved + int numDepSub = dataBitArray.readBits(4); // num_dep_sub + dataBitArray.skipBits(1); // numDepSub > 0 ? LFE2 : reserved if (numDepSub > 0) { - int lowByteChanLoc = data.readUnsignedByte(); + dataBitArray.skipBytes(6); // other channel configurations // Read Lrs/Rrs pair // TODO: Read other channel configuration - if ((lowByteChanLoc & 0x02) != 0) { + if (dataBitArray.readBits(1) != 0) { channelCount += 2; } + dataBitArray.skipBits(1); // Lc/Rc pair } + String mimeType = MimeTypes.AUDIO_E_AC3; - if (data.bytesLeft() > 0) { - nextByte = data.readUnsignedByte(); - if ((nextByte & 0x01) != 0) { // flag_ec3_extension_type_a + if (dataBitArray.bitsLeft() > 7) { + dataBitArray.skipBits(7); // reserved + if (dataBitArray.readBits(1) != 0) { // flag_ec3_extension_type_a mimeType = MimeTypes.AUDIO_E_AC3_JOC; } } + // Update data position + dataBitArray.byteAlign(); + data.setPosition(dataBitArray.getBytePosition()); return new Format.Builder() .setId(trackId) .setSampleMimeType(mimeType) From 30dce91fc04686fbb7e391ab6fb79dcd2044b25c Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 24 Nov 2022 18:14:44 +0000 Subject: [PATCH 029/141] Convert bitrates to bps before setting it Format expects the values of `averageBitrate` and `peakBitrate` in bps and the value fetched from AC3SpecificBox and EC3SpecificBox is in kbps. PiperOrigin-RevId: 490756581 (cherry picked from commit 4066970ce7292642794f4a3954f8d0fde78dd310) --- .../src/main/java/androidx/media3/extractor/Ac3Util.java | 4 ++-- .../src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump | 4 ++-- .../src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump | 4 ++-- .../src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump | 4 ++-- .../src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump | 4 ++-- .../extractordumps/mp4/sample_ac3.mp4.unknown_length.dump | 4 ++-- .../extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump | 4 ++-- .../extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump | 4 ++-- .../extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump | 4 ++-- .../extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump | 4 ++-- .../mp4/sample_ac3_fragmented.mp4.unknown_length.dump | 4 ++-- .../src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump | 2 +- .../src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump | 2 +- .../src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump | 2 +- .../src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump | 2 +- .../extractordumps/mp4/sample_eac3.mp4.unknown_length.dump | 2 +- .../extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump | 2 +- .../extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump | 2 +- .../extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump | 2 +- .../extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump | 2 +- .../mp4/sample_eac3_fragmented.mp4.unknown_length.dump | 2 +- .../test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump | 2 +- .../test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump | 2 +- .../test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump | 2 +- .../test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump | 2 +- .../extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump | 2 +- .../extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump | 2 +- .../extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump | 2 +- .../extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump | 2 +- .../extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump | 2 +- .../mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump | 2 +- 31 files changed, 42 insertions(+), 42 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java index 78baffd5b56..b9279635d83 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java @@ -162,7 +162,7 @@ public static Format parseAc3AnnexFFormat( channelCount++; } int halfFrmsizecod = dataBitArray.readBits(5); // bit_rate_code - int constantBitrate = BITRATE_BY_HALF_FRMSIZECOD[halfFrmsizecod]; + int constantBitrate = BITRATE_BY_HALF_FRMSIZECOD[halfFrmsizecod] * 1000; // Update data position dataBitArray.byteAlign(); data.setPosition(dataBitArray.getBytePosition()); @@ -193,7 +193,7 @@ public static Format parseEAc3AnnexFFormat( ParsableBitArray dataBitArray = new ParsableBitArray(); dataBitArray.reset(data); - int peakBitrate = dataBitArray.readBits(13); // data_rate + int peakBitrate = dataBitArray.readBits(13) * 1000; // data_rate dataBitArray.skipBits(3); // num_ind_sub // Read the first independent substream. diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump index 71eed666b7a..97bfb758dc9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.0.dump @@ -10,8 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: - averageBitrate = 384 - peakBitrate = 384 + averageBitrate = 384000 + peakBitrate = 384000 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump index a6fbd97784b..9e9b211ed9c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.1.dump @@ -10,8 +10,8 @@ track 0: total output bytes = 9216 sample count = 6 format 0: - averageBitrate = 384 - peakBitrate = 384 + averageBitrate = 384000 + peakBitrate = 384000 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump index e02699e2de5..4e872b3fd50 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.2.dump @@ -10,8 +10,8 @@ track 0: total output bytes = 4608 sample count = 3 format 0: - averageBitrate = 384 - peakBitrate = 384 + averageBitrate = 384000 + peakBitrate = 384000 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump index 4b7e17e7c92..138696f8a7e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.3.dump @@ -10,8 +10,8 @@ track 0: total output bytes = 1536 sample count = 1 format 0: - averageBitrate = 384 - peakBitrate = 384 + averageBitrate = 384000 + peakBitrate = 384000 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump index 71eed666b7a..97bfb758dc9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3.mp4.unknown_length.dump @@ -10,8 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: - averageBitrate = 384 - peakBitrate = 384 + averageBitrate = 384000 + peakBitrate = 384000 id = 1 sampleMimeType = audio/ac3 maxInputSize = 1566 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump index 84217c2e015..448e79e1fd7 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.0.dump @@ -10,8 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: - averageBitrate = 384 - peakBitrate = 384 + averageBitrate = 384000 + peakBitrate = 384000 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump index 1edd06253fc..0f902e441a8 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump @@ -10,8 +10,8 @@ track 0: total output bytes = 10752 sample count = 7 format 0: - averageBitrate = 384 - peakBitrate = 384 + averageBitrate = 384000 + peakBitrate = 384000 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump index 01fd6af916d..d747be40c5b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump @@ -10,8 +10,8 @@ track 0: total output bytes = 6144 sample count = 4 format 0: - averageBitrate = 384 - peakBitrate = 384 + averageBitrate = 384000 + peakBitrate = 384000 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump index c303da0e156..76738dd5b34 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.3.dump @@ -10,8 +10,8 @@ track 0: total output bytes = 1536 sample count = 1 format 0: - averageBitrate = 384 - peakBitrate = 384 + averageBitrate = 384000 + peakBitrate = 384000 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump index 84217c2e015..448e79e1fd7 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.unknown_length.dump @@ -10,8 +10,8 @@ track 0: total output bytes = 13824 sample count = 9 format 0: - averageBitrate = 384 - peakBitrate = 384 + averageBitrate = 384000 + peakBitrate = 384000 id = 1 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump index aba5268ea29..a50ee9fecd9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.0.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: - peakBitrate = 1000 + peakBitrate = 1000000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump index ac03cfd4847..089c940439e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.1.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 144000 sample count = 36 format 0: - peakBitrate = 1000 + peakBitrate = 1000000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump index 1a61f528ac6..5d481314d5d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.2.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 72000 sample count = 18 format 0: - peakBitrate = 1000 + peakBitrate = 1000000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump index 431599a9bef..02425188669 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.3.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 4000 sample count = 1 format 0: - peakBitrate = 1000 + peakBitrate = 1000000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump index aba5268ea29..a50ee9fecd9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3.mp4.unknown_length.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: - peakBitrate = 1000 + peakBitrate = 1000000 id = 1 sampleMimeType = audio/eac3 maxInputSize = 4030 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump index 6da60d472a4..734051bb2d3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.0.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: - peakBitrate = 1000 + peakBitrate = 1000000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump index 646dd35d91a..027e7eb6333 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 148000 sample count = 37 format 0: - peakBitrate = 1000 + peakBitrate = 1000000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump index a7ba576bf58..db94e2636e7 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 76000 sample count = 19 format 0: - peakBitrate = 1000 + peakBitrate = 1000000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump index 280d6febc4e..854d952ad8b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.3.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 4000 sample count = 1 format 0: - peakBitrate = 1000 + peakBitrate = 1000000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump index 6da60d472a4..734051bb2d3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.unknown_length.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: - peakBitrate = 1000 + peakBitrate = 1000000 id = 1 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump index c98e27dc190..45a51b50aea 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.0.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: - peakBitrate = 640 + peakBitrate = 640000 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump index 9c9cee29dfb..4ad3e45f534 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.1.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 110080 sample count = 43 format 0: - peakBitrate = 640 + peakBitrate = 640000 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump index 85c07f6d2d9..0c53717c22f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.2.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 56320 sample count = 22 format 0: - peakBitrate = 640 + peakBitrate = 640000 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump index 56387fb3c78..c8cd33b57b3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.3.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 2560 sample count = 1 format 0: - peakBitrate = 640 + peakBitrate = 640000 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump index c98e27dc190..45a51b50aea 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc.mp4.unknown_length.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: - peakBitrate = 640 + peakBitrate = 640000 id = 1 sampleMimeType = audio/eac3-joc maxInputSize = 2590 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump index c73a6282e8e..87930b0bfb1 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.0.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: - peakBitrate = 640 + peakBitrate = 640000 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump index 78b392053ee..e1aa764cfb7 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.1.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 110080 sample count = 43 format 0: - peakBitrate = 640 + peakBitrate = 640000 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump index 25583633429..c9f083805f5 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.2.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 56320 sample count = 22 format 0: - peakBitrate = 640 + peakBitrate = 640000 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump index 084d2aa030c..a3875f612d4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.3.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 2560 sample count = 1 format 0: - peakBitrate = 640 + peakBitrate = 640000 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump index c73a6282e8e..87930b0bfb1 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.unknown_length.dump @@ -10,7 +10,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: - peakBitrate = 640 + peakBitrate = 640000 id = 1 sampleMimeType = audio/eac3-joc channelCount = 6 From 8767605f5c561f9077573adae69a2fb1fb018965 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 25 Nov 2022 15:36:22 +0000 Subject: [PATCH 030/141] Remove flakiness from DefaultAnalyticsCollectorTest Our FakeClock generally makes sure that playback tests are fully deterministic. However, this fails if the test uses blocking waits with clock.onThreadBlocked and where relevant Handlers are created without using the clock. To fix the flakiness, we can make the following adjustments: - Use TestExoPlayerBuilder instead of legacy ExoPlayerTestRunner to avoid onThreadBlocked calls. This also makes the tests more readable. - Use clock to create Handler for FakeVideoRenderer and FakeAudioRenderer. Ideally, this should be passed through RenderersFactory, but it's too disruptive given this is a public API. - Use clock for MediaSourceList and MediaPeriodQueue update handler. PiperOrigin-RevId: 490907495 (cherry picked from commit 6abc94a8b7180979c520fc581310b87bf297b1bb) --- .../exoplayer/ExoPlayerImplInternal.java | 2 +- .../media3/exoplayer/MediaPeriodQueue.java | 5 +- .../media3/exoplayer/MediaSourceList.java | 167 +++-- .../media3/exoplayer/ExoPlayerTest.java | 21 +- .../exoplayer/MediaPeriodQueueTest.java | 9 +- .../media3/exoplayer/MediaSourceListTest.java | 2 +- .../DefaultAnalyticsCollectorTest.java | 667 +++++++++--------- .../media3/test/utils/FakeAudioRenderer.java | 29 +- .../media3/test/utils/FakeVideoRenderer.java | 51 +- .../test/utils/TestExoPlayerBuilder.java | 18 +- .../robolectric/TestPlayerRunHelper.java | 24 + 11 files changed, 570 insertions(+), 425 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index d92a5e8b877..6c3c64075b7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -281,7 +281,7 @@ public ExoPlayerImplInternal( deliverPendingMessageAtStartPositionRequired = true; - Handler eventHandler = new Handler(applicationLooper); + HandlerWrapper eventHandler = clock.createHandler(applicationLooper, /* callback= */ null); queue = new MediaPeriodQueue(analyticsCollector, eventHandler); mediaSourceList = new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java index f812c11ed50..7649d1bbe80 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java @@ -26,6 +26,7 @@ import androidx.media3.common.Player.RepeatMode; import androidx.media3.common.Timeline; import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; @@ -71,7 +72,7 @@ private final Timeline.Period period; private final Timeline.Window window; private final AnalyticsCollector analyticsCollector; - private final Handler analyticsCollectorHandler; + private final HandlerWrapper analyticsCollectorHandler; private long nextWindowSequenceNumber; private @RepeatMode int repeatMode; @@ -91,7 +92,7 @@ * on. */ public MediaPeriodQueue( - AnalyticsCollector analyticsCollector, Handler analyticsCollectorHandler) { + AnalyticsCollector analyticsCollector, HandlerWrapper analyticsCollectorHandler) { this.analyticsCollector = analyticsCollector; this.analyticsCollectorHandler = analyticsCollectorHandler; period = new Timeline.Period(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaSourceList.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaSourceList.java index 5bc6e1026a3..21cd5ceec47 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaSourceList.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaSourceList.java @@ -15,13 +15,16 @@ */ package androidx.media3.exoplayer; +import static androidx.media3.common.util.Assertions.checkNotNull; import static java.lang.Math.max; import static java.lang.Math.min; import android.os.Handler; +import android.util.Pair; import androidx.annotation.Nullable; import androidx.media3.common.Timeline; import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import androidx.media3.datasource.TransferListener; @@ -48,6 +51,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified @@ -77,11 +81,10 @@ public interface MediaSourceListInfoRefreshListener { private final IdentityHashMap mediaSourceByMediaPeriod; private final Map mediaSourceByUid; private final MediaSourceListInfoRefreshListener mediaSourceListInfoListener; - private final MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher; - private final DrmSessionEventListener.EventDispatcher drmEventDispatcher; private final HashMap childSources; private final Set enabledMediaSourceHolders; - + private final AnalyticsCollector eventListener; + private final HandlerWrapper eventHandler; private ShuffleOrder shuffleOrder; private boolean isPrepared; @@ -101,7 +104,7 @@ public interface MediaSourceListInfoRefreshListener { public MediaSourceList( MediaSourceListInfoRefreshListener listener, AnalyticsCollector analyticsCollector, - Handler analyticsCollectorHandler, + HandlerWrapper analyticsCollectorHandler, PlayerId playerId) { this.playerId = playerId; mediaSourceListInfoListener = listener; @@ -109,12 +112,10 @@ public MediaSourceList( mediaSourceByMediaPeriod = new IdentityHashMap<>(); mediaSourceByUid = new HashMap<>(); mediaSourceHolders = new ArrayList<>(); - mediaSourceEventDispatcher = new MediaSourceEventListener.EventDispatcher(); - drmEventDispatcher = new DrmSessionEventListener.EventDispatcher(); + eventListener = analyticsCollector; + eventHandler = analyticsCollectorHandler; childSources = new HashMap<>(); enabledMediaSourceHolders = new HashSet<>(); - mediaSourceEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector); - drmEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector); } /** @@ -308,7 +309,7 @@ public MediaPeriod createPeriod( Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); MediaSource.MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(getChildPeriodUid(id.periodUid)); - MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid)); + MediaSourceHolder holder = checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid)); enableMediaSource(holder); holder.activeMediaPeriodIds.add(childMediaPeriodId); MediaPeriod mediaPeriod = @@ -324,8 +325,7 @@ public MediaPeriod createPeriod( * @param mediaPeriod The period to release. */ public void releasePeriod(MediaPeriod mediaPeriod) { - MediaSourceHolder holder = - Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); + MediaSourceHolder holder = checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); holder.mediaSource.releasePeriod(mediaPeriod); holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id); if (!mediaSourceByMediaPeriod.isEmpty()) { @@ -450,8 +450,7 @@ private void prepareChildSource(MediaSourceHolder holder) { private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) { // Release if the source has been removed from the playlist and no periods are still active. if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) { - MediaSourceAndListener removedChild = - Assertions.checkNotNull(childSources.remove(mediaSourceHolder)); + MediaSourceAndListener removedChild = checkNotNull(childSources.remove(mediaSourceHolder)); removedChild.mediaSource.releaseSource(removedChild.caller); removedChild.mediaSource.removeEventListener(removedChild.eventListener); removedChild.mediaSource.removeDrmEventListener(removedChild.eventListener); @@ -526,12 +525,8 @@ private final class ForwardingEventListener implements MediaSourceEventListener, DrmSessionEventListener { private final MediaSourceList.MediaSourceHolder id; - private MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher; - private DrmSessionEventListener.EventDispatcher drmEventDispatcher; public ForwardingEventListener(MediaSourceList.MediaSourceHolder id) { - mediaSourceEventDispatcher = MediaSourceList.this.mediaSourceEventDispatcher; - drmEventDispatcher = MediaSourceList.this.drmEventDispatcher; this.id = id; } @@ -543,8 +538,14 @@ public void onLoadStarted( @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventData, MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - mediaSourceEventDispatcher.loadStarted(loadEventData, mediaLoadData); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> + eventListener.onLoadStarted( + eventParameters.first, eventParameters.second, loadEventData, mediaLoadData)); } } @@ -554,8 +555,14 @@ public void onLoadCompleted( @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventData, MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - mediaSourceEventDispatcher.loadCompleted(loadEventData, mediaLoadData); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> + eventListener.onLoadCompleted( + eventParameters.first, eventParameters.second, loadEventData, mediaLoadData)); } } @@ -565,8 +572,14 @@ public void onLoadCanceled( @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventData, MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - mediaSourceEventDispatcher.loadCanceled(loadEventData, mediaLoadData); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> + eventListener.onLoadCanceled( + eventParameters.first, eventParameters.second, loadEventData, mediaLoadData)); } } @@ -578,8 +591,19 @@ public void onLoadError( MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - mediaSourceEventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> + eventListener.onLoadError( + eventParameters.first, + eventParameters.second, + loadEventData, + mediaLoadData, + error, + wasCanceled)); } } @@ -588,8 +612,14 @@ public void onUpstreamDiscarded( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - mediaSourceEventDispatcher.upstreamDiscarded(mediaLoadData); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> + eventListener.onUpstreamDiscarded( + eventParameters.first, checkNotNull(eventParameters.second), mediaLoadData)); } } @@ -598,8 +628,14 @@ public void onDownstreamFormatChanged( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - mediaSourceEventDispatcher.downstreamFormatChanged(mediaLoadData); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> + eventListener.onDownstreamFormatChanged( + eventParameters.first, eventParameters.second, mediaLoadData)); } } @@ -610,75 +646,94 @@ public void onDrmSessionAcquired( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, @DrmSession.State int state) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - drmEventDispatcher.drmSessionAcquired(state); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> + eventListener.onDrmSessionAcquired( + eventParameters.first, eventParameters.second, state)); } } @Override public void onDrmKeysLoaded( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - drmEventDispatcher.drmKeysLoaded(); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> eventListener.onDrmKeysLoaded(eventParameters.first, eventParameters.second)); } } @Override public void onDrmSessionManagerError( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, Exception error) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - drmEventDispatcher.drmSessionManagerError(error); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> + eventListener.onDrmSessionManagerError( + eventParameters.first, eventParameters.second, error)); } } @Override public void onDrmKeysRestored( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - drmEventDispatcher.drmKeysRestored(); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> eventListener.onDrmKeysRestored(eventParameters.first, eventParameters.second)); } } @Override public void onDrmKeysRemoved( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - drmEventDispatcher.drmKeysRemoved(); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> eventListener.onDrmKeysRemoved(eventParameters.first, eventParameters.second)); } } @Override public void onDrmSessionReleased( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { - if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - drmEventDispatcher.drmSessionReleased(); + @Nullable + Pair eventParameters = + getEventParameters(windowIndex, mediaPeriodId); + if (eventParameters != null) { + eventHandler.post( + () -> + eventListener.onDrmSessionReleased(eventParameters.first, eventParameters.second)); } } - /** Updates the event dispatcher and returns whether the event should be dispatched. */ - private boolean maybeUpdateEventDispatcher( + /** Updates the event parameters and returns whether the event should be dispatched. */ + @Nullable + private Pair getEventParameters( int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) { @Nullable MediaSource.MediaPeriodId mediaPeriodId = null; if (childMediaPeriodId != null) { mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId); if (mediaPeriodId == null) { // Media period not found. Ignore event. - return false; + return null; } } int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex); - if (mediaSourceEventDispatcher.windowIndex != windowIndex - || !Util.areEqual(mediaSourceEventDispatcher.mediaPeriodId, mediaPeriodId)) { - mediaSourceEventDispatcher = - MediaSourceList.this.mediaSourceEventDispatcher.withParameters( - windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L); - } - if (drmEventDispatcher.windowIndex != windowIndex - || !Util.areEqual(drmEventDispatcher.mediaPeriodId, mediaPeriodId)) { - drmEventDispatcher = - MediaSourceList.this.drmEventDispatcher.withParameters(windowIndex, mediaPeriodId); - } - return true; + return Pair.create(windowIndex, mediaPeriodId); } } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 31a0726ef76..14f92f873d8 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -112,6 +112,7 @@ import androidx.media3.common.Tracks; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Clock; +import androidx.media3.common.util.SystemClock; import androidx.media3.common.util.Util; import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.analytics.AnalyticsListener; @@ -11897,7 +11898,11 @@ public void newServerSideInsertedAdAtPlaybackPosition_keepsRenderersEnabled() th new TestExoPlayerBuilder(context) .setRenderersFactory( (handler, videoListener, audioListener, textOutput, metadataOutput) -> { - videoRenderer.set(new FakeVideoRenderer(handler, videoListener)); + videoRenderer.set( + new FakeVideoRenderer( + SystemClock.DEFAULT.createHandler( + handler.getLooper(), /* callback= */ null), + videoListener)); return new Renderer[] {videoRenderer.get()}; }) .build(); @@ -12034,7 +12039,12 @@ public void release_triggersAllPendingEventsInAnalyticsListeners() throws Except new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) .setRenderersFactory( (handler, videoListener, audioListener, textOutput, metadataOutput) -> - new Renderer[] {new FakeVideoRenderer(handler, videoListener)}) + new Renderer[] { + new FakeVideoRenderer( + SystemClock.DEFAULT.createHandler( + handler.getLooper(), /* callback= */ null), + videoListener) + }) .build(); AnalyticsListener listener = mock(AnalyticsListener.class); player.addAnalyticsListener(listener); @@ -12059,7 +12069,12 @@ public void releaseAfterRendererEvents_triggersPendingVideoEventsInListener() th new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) .setRenderersFactory( (handler, videoListener, audioListener, textOutput, metadataOutput) -> - new Renderer[] {new FakeVideoRenderer(handler, videoListener)}) + new Renderer[] { + new FakeVideoRenderer( + SystemClock.DEFAULT.createHandler( + handler.getLooper(), /* callback= */ null), + videoListener) + }) .build(); Player.Listener listener = mock(Player.Listener.class); player.addListener(listener); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java index ca3bd02eebe..2ab4681030e 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java @@ -25,7 +25,6 @@ import static org.robolectric.Shadows.shadowOf; import android.net.Uri; -import android.os.Handler; import android.os.Looper; import android.util.Pair; import androidx.media3.common.AdPlaybackState; @@ -36,6 +35,7 @@ import androidx.media3.common.Timeline; import androidx.media3.common.Tracks; import androidx.media3.common.util.Clock; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector; import androidx.media3.exoplayer.analytics.PlayerId; @@ -97,13 +97,14 @@ public void setUp() { analyticsCollector.setPlayer( new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(), Looper.getMainLooper()); - mediaPeriodQueue = - new MediaPeriodQueue(analyticsCollector, new Handler(Looper.getMainLooper())); + HandlerWrapper handler = + Clock.DEFAULT.createHandler(Looper.getMainLooper(), /* callback= */ null); + mediaPeriodQueue = new MediaPeriodQueue(analyticsCollector, handler); mediaSourceList = new MediaSourceList( mock(MediaSourceList.MediaSourceListInfoRefreshListener.class), analyticsCollector, - new Handler(Looper.getMainLooper()), + handler, PlayerId.UNSET); rendererCapabilities = new RendererCapabilities[0]; trackSelector = mock(TrackSelector.class); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java index edb874091c0..ea3c6ce33d0 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java @@ -67,7 +67,7 @@ public void setUp() { new MediaSourceList( mock(MediaSourceList.MediaSourceListInfoRefreshListener.class), analyticsCollector, - Util.createHandlerForCurrentOrMainLooper(), + Clock.DEFAULT.createHandler(Util.getCurrentOrMainLooper(), /* callback= */ null), PlayerId.UNSET); } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java index c25ddebdcf2..ceefe172c7e 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java @@ -49,6 +49,12 @@ import static androidx.media3.exoplayer.analytics.AnalyticsListener.EVENT_VIDEO_SIZE_CHANGED; import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample; +import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilPosition; +import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilError; +import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilIsLoading; +import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled; +import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState; +import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilTimelineChanged; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -63,6 +69,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.robolectric.shadows.ShadowLooper.idleMainLooper; +import static org.robolectric.shadows.ShadowLooper.runMainLooperToNextTask; import android.graphics.SurfaceTexture; import android.os.Looper; @@ -85,6 +93,7 @@ import androidx.media3.common.VideoSize; import androidx.media3.common.util.Clock; import androidx.media3.common.util.ConditionVariable; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.DecoderCounters; import androidx.media3.exoplayer.ExoPlaybackException; @@ -102,8 +111,6 @@ import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; -import androidx.media3.test.utils.ActionSchedule; -import androidx.media3.test.utils.ActionSchedule.PlayerRunnable; import androidx.media3.test.utils.ExoPlayerTestRunner; import androidx.media3.test.utils.FakeAudioRenderer; import androidx.media3.test.utils.FakeClock; @@ -132,14 +139,11 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; -import org.robolectric.shadows.ShadowLooper; /** Integration test for {@link DefaultAnalyticsCollector}. */ @RunWith(AndroidJUnit4.class) public final class DefaultAnalyticsCollectorTest { - private static final String TAG = "DefaultAnalyticsCollectorTest"; - // Deprecated event constants. private static final long EVENT_PLAYER_STATE_CHANGED = 1L << 63; private static final long EVENT_SEEK_STARTED = 1L << 62; @@ -167,7 +171,6 @@ public final class DefaultAnalyticsCollectorTest { private static final Format VIDEO_FORMAT_DRM_1 = ExoPlayerTestRunner.VIDEO_FORMAT.buildUpon().setDrmInitData(DRM_DATA_1).build(); - private static final int TIMEOUT_MS = 10_000; private static final Timeline SINGLE_PERIOD_TIMELINE = new FakeTimeline(); private static final EventWindowAndPeriodId WINDOW_0 = new EventWindowAndPeriodId(/* windowIndex= */ 0, /* mediaPeriodId= */ null); @@ -217,7 +220,14 @@ public void emptyTimeline() throws Exception { FakeMediaSource mediaSource = new FakeMediaSource( Timeline.EMPTY, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT); - TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.play(); + player.setMediaSource(mediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_ENDED); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( @@ -236,7 +246,14 @@ public void singlePeriod() throws Exception { SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT); - TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.play(); + player.setMediaSource(mediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_ENDED); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) @@ -247,7 +264,7 @@ public void singlePeriod() throws Exception { period0 /* ENDED */) .inOrder(); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */) .inOrder(); assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly(period0 /* started */, period0 /* stopped */) @@ -297,7 +314,14 @@ public void automaticPeriodTransition() throws Exception { SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT)); - TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.play(); + player.setMediaSource(mediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_ENDED); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) @@ -378,7 +402,14 @@ public void periodTransitionWithRendererChange() throws Exception { new ConcatenatingMediaSource( new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT), new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.AUDIO_FORMAT)); - TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.play(); + player.setMediaSource(mediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_ENDED); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) @@ -449,23 +480,23 @@ public void seekToOtherPeriod() throws Exception { ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT), new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.AUDIO_FORMAT)); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .pause() - // Wait until second period has fully loaded to assert loading events without flakiness. - .waitForIsLoading(true) - .waitForIsLoading(false) - .seek(/* mediaItemIndex= */ 1, /* positionMs= */ 0) - .play() - .build(); - TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.setMediaSource(mediaSource); + player.prepare(); + // Wait until second period has fully loaded to assert loading events. + runUntilIsLoading(player, /* expectedIsLoading= */ true); + runUntilIsLoading(player, /* expectedIsLoading= */ false); + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( - WINDOW_0 /* setPlayWhenReady=true */, WINDOW_0 /* BUFFERING */, - WINDOW_0 /* setPlayWhenReady=false */, period0 /* READY */, period1 /* BUFFERING */, period1 /* setPlayWhenReady=true */, @@ -542,23 +573,24 @@ public void seekBackAfterReadingAhead() throws Exception { SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT)); - long periodDurationMs = + long windowDurationMs = SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .pause() - .waitForPlaybackState(Player.STATE_READY) - .playUntilPosition(/* mediaItemIndex= */ 0, periodDurationMs) - .seekAndWait(/* positionMs= */ 0) - .play() - .build(); - TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.setMediaSource(mediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_READY); + playUntilPosition(player, /* mediaItemIndex= */ 0, windowDurationMs - 100); + player.seekTo(/* positionMs= */ 0); + runUntilPlaybackState(player, Player.STATE_READY); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( - WINDOW_0 /* setPlayWhenReady=true */, - WINDOW_0 /* setPlayWhenReady=false */, WINDOW_0 /* BUFFERING */, period0 /* READY */, period0 /* setPlayWhenReady=true */, @@ -653,17 +685,19 @@ public void prepareNewSource() throws Exception { new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT); MediaSource mediaSource2 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .pause() - .waitForPlaybackState(Player.STATE_READY) - .setMediaSources(/* resetPosition= */ false, mediaSource2) - .waitForTimelineChanged() - // Wait until loading started to prevent flakiness caused by loading finishing too fast. - .waitForIsLoading(true) - .play() - .build(); - TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.setMediaSource(mediaSource1); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_READY); + player.setMediaSource(mediaSource2, /* resetPosition= */ false); + runUntilTimelineChanged(player); + // Wait until loading started to assert loading events. + runUntilIsLoading(player, /* expectedIsLoading= */ true); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); // Populate all event ids with last timeline (after second prepare). populateEventIds(listener.lastReportedTimeline); @@ -676,9 +710,7 @@ public void prepareNewSource() throws Exception { /* windowSequenceNumber= */ 0)); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( - WINDOW_0 /* setPlayWhenReady=true */, WINDOW_0 /* BUFFERING */, - WINDOW_0 /* setPlayWhenReady=false */, period0Seq0 /* READY */, WINDOW_0 /* BUFFERING */, period0Seq1 /* setPlayWhenReady=true */, @@ -688,9 +720,9 @@ public void prepareNewSource() throws Exception { assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly( WINDOW_0 /* PLAYLIST_CHANGE */, - period0Seq0 /* SOURCE_UPDATE */, + WINDOW_0 /* SOURCE_UPDATE */, WINDOW_0 /* PLAYLIST_CHANGE */, - period0Seq1 /* SOURCE_UPDATE */); + WINDOW_0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) .containsExactly(WINDOW_0 /* REMOVE */); assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) @@ -753,28 +785,31 @@ public void prepareNewSource() throws Exception { public void reprepareAfterError() throws Exception { MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .pause() - .waitForPlaybackState(Player.STATE_READY) - .throwPlaybackException( - ExoPlaybackException.createForSource( - new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED)) - .waitForPlaybackState(Player.STATE_IDLE) - .seek(/* positionMs= */ 0) - .prepare() - // Wait until loading started to assert loading events without flakiness. - .waitForIsLoading(true) - .play() - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.setMediaSource(mediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_READY); + player + .createMessage( + (message, payload) -> { + throw ExoPlaybackException.createForSource( + new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED); + }) + .send(); + runUntilError(player); + player.seekTo(/* positionMs= */ 0); + player.prepare(); + // Wait until loading started to assert loading events. + runUntilIsLoading(player, /* expectedIsLoading= */ true); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( - WINDOW_0 /* setPlayWhenReady=true */, - WINDOW_0 /* setPlayWhenReady=false */, WINDOW_0 /* BUFFERING */, period0Seq0 /* READY */, period0Seq0 /* IDLE */, @@ -784,7 +819,7 @@ public void reprepareAfterError() throws Exception { period0Seq0 /* ENDED */) .inOrder(); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* prepared */, period0Seq0 /* prepared */); + .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0Seq0); @@ -835,36 +870,33 @@ public void dynamicTimelineChange() throws Exception { new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT); final ConcatenatingMediaSource concatenatedMediaSource = new ConcatenatingMediaSource(childMediaSource, childMediaSource); - long periodDurationMs = + long windowDurationMs = SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .pause() - .waitForPlaybackState(Player.STATE_READY) - // Ensure second period is already being read from. - .playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ periodDurationMs) - .executeRunnable( - () -> - concatenatedMediaSource.moveMediaSource( - /* currentIndex= */ 0, /* newIndex= */ 1)) - .waitForTimelineChanged() - .waitForPlaybackState(Player.STATE_READY) - .play() - .build(); - TestAnalyticsListener listener = runAnalyticsTest(concatenatedMediaSource, actionSchedule); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.setMediaSource(concatenatedMediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_READY); + // Ensure second period is already being read from. + playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ windowDurationMs - 100); + concatenatedMediaSource.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 1); + runUntilTimelineChanged(player); + runUntilPlaybackState(player, Player.STATE_READY); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( - WINDOW_0 /* setPlayWhenReady=true */, - WINDOW_0 /* setPlayWhenReady=false */, WINDOW_0 /* BUFFERING */, window0Period1Seq0 /* READY */, window0Period1Seq0 /* setPlayWhenReady=true */, window0Period1Seq0 /* setPlayWhenReady=false */, - period1Seq0 /* setPlayWhenReady=true */, period1Seq0 /* BUFFERING */, period1Seq0 /* READY */, + period1Seq0 /* setPlayWhenReady=true */, period1Seq0 /* ENDED */) .inOrder(); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) @@ -926,20 +958,22 @@ public void dynamicTimelineChange() throws Exception { public void playlistOperations() throws Exception { MediaSource fakeMediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .pause() - .waitForPlaybackState(Player.STATE_READY) - .addMediaSources(fakeMediaSource) - // Wait until second period has fully loaded to assert loading events without flakiness. - .waitForIsLoading(true) - .waitForIsLoading(false) - .removeMediaItem(/* index= */ 0) - .waitForPlaybackState(Player.STATE_BUFFERING) - .waitForPlaybackState(Player.STATE_READY) - .play() - .build(); - TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.setMediaSource(fakeMediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_READY); + player.addMediaSource(fakeMediaSource); + // Wait until second period has fully loaded to assert loading events. + runUntilIsLoading(player, /* expectedIsLoading= */ true); + runUntilIsLoading(player, /* expectedIsLoading= */ false); + player.removeMediaItem(/* index= */ 0); + runUntilPlaybackState(player, Player.STATE_BUFFERING); + runUntilPlaybackState(player, Player.STATE_READY); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); // Populate event ids with second to last timeline that still contained both periods. populateEventIds(listener.reportedTimelines.get(listener.reportedTimelines.size() - 2)); @@ -953,8 +987,6 @@ public void playlistOperations() throws Exception { /* windowSequenceNumber= */ 1)); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( - WINDOW_0 /* setPlayWhenReady=true */, - WINDOW_0 /* setPlayWhenReady=false */, WINDOW_0 /* BUFFERING */, period0Seq0 /* READY */, period0Seq1 /* BUFFERING */, @@ -965,7 +997,7 @@ public void playlistOperations() throws Exception { assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly( WINDOW_0 /* PLAYLIST_CHANGED */, - period0Seq0 /* SOURCE_UPDATE (first item) */, + WINDOW_0 /* SOURCE_UPDATE (first item) */, period0Seq0 /* PLAYLIST_CHANGED (add) */, period0Seq0 /* SOURCE_UPDATE (second item) */, period0Seq1 /* PLAYLIST_CHANGED (remove) */) @@ -1063,60 +1095,53 @@ public void adPlayback() throws Exception { } }, ExoPlayerTestRunner.VIDEO_FORMAT); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(ExoPlayer player) { - player.addListener( - new Player.Listener() { - @Override - public void onPositionDiscontinuity( - Player.PositionInfo oldPosition, - Player.PositionInfo newPosition, - @Player.DiscontinuityReason int reason) { - if (!player.isPlayingAd() - && reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { - // Finished playing ad. Marked as played. - adPlaybackState.set( - adPlaybackState - .get() - .withPlayedAd( - /* adGroupIndex= */ playedAdCount.getAndIncrement(), - /* adIndexInAdGroup= */ 0)); - fakeMediaSource.setNewSourceInfo( - new FakeTimeline( - new TimelineWindowDefinition( - /* periodCount= */ 1, - /* id= */ 0, - /* isSeekable= */ true, - /* isDynamic= */ false, - contentDurationsUs, - adPlaybackState.get())), - /* sendManifestLoadEvents= */ false); - } - } - }); - } - }) - .pause() - // Ensure everything is preloaded. - .waitForIsLoading(true) - .waitForIsLoading(false) - .waitForPlaybackState(Player.STATE_READY) - // Wait in each content part to ensure previously triggered events get a chance to be - // delivered. This prevents flakiness caused by playback progressing too fast. - .playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 3_000) - .waitForPendingPlayerCommands() - .playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 8_000) - .waitForPendingPlayerCommands() - .play() - .waitForPlaybackState(Player.STATE_ENDED) - // Wait for final timeline change that marks post-roll played. - .waitForTimelineChanged() - .build(); - TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule); + ExoPlayer player = setupPlayer(); + player.addListener( + new Player.Listener() { + @Override + public void onPositionDiscontinuity( + Player.PositionInfo oldPosition, + Player.PositionInfo newPosition, + @Player.DiscontinuityReason int reason) { + if (!player.isPlayingAd() && reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { + // Finished playing ad. Marked as played. + adPlaybackState.set( + adPlaybackState + .get() + .withPlayedAd( + /* adGroupIndex= */ playedAdCount.getAndIncrement(), + /* adIndexInAdGroup= */ 0)); + fakeMediaSource.setNewSourceInfo( + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + contentDurationsUs, + adPlaybackState.get())), + /* sendManifestLoadEvents= */ false); + } + } + }); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.setMediaSource(fakeMediaSource); + player.prepare(); + // Ensure everything is preloaded. + runUntilIsLoading(player, /* expectedIsLoading= */ true); + runUntilIsLoading(player, /* expectedIsLoading= */ false); + runUntilPlaybackState(player, Player.STATE_READY); + // Wait in each content part to ensure previously triggered events get a chance to be delivered. + playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 3_000); + runUntilPendingCommandsAreFullyHandled(player); + playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 8_000); + runUntilPendingCommandsAreFullyHandled(player); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); + // Wait for final timeline change that marks post-roll played. + runUntilTimelineChanged(player); Object periodUid = listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0); EventWindowAndPeriodId prerollAd = @@ -1158,8 +1183,6 @@ public void onPositionDiscontinuity( periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ C.INDEX_UNSET)); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( - WINDOW_0 /* setPlayWhenReady=true */, - WINDOW_0 /* setPlayWhenReady=false */, WINDOW_0 /* BUFFERING */, prerollAd /* READY */, prerollAd /* setPlayWhenReady=true */, @@ -1172,7 +1195,7 @@ public void onPositionDiscontinuity( assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly( WINDOW_0 /* PLAYLIST_CHANGED */, - prerollAd /* SOURCE_UPDATE (initial) */, + WINDOW_0 /* SOURCE_UPDATE (initial) */, contentAfterPreroll /* SOURCE_UPDATE (played preroll) */, contentAfterMidroll /* SOURCE_UPDATE (played midroll) */, contentAfterPostroll /* SOURCE_UPDATE (played postroll) */) @@ -1322,20 +1345,21 @@ public void seekAfterMidroll() throws Exception { } }, ExoPlayerTestRunner.VIDEO_FORMAT); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .pause() - // Ensure everything is preloaded. - .waitForIsLoading(true) - .waitForIsLoading(false) - // Seek behind the midroll. - .seek(6 * C.MICROS_PER_SECOND) - // Wait until loading started again to assert loading events without flakiness. - .waitForIsLoading(true) - .play() - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.setMediaSource(fakeMediaSource); + player.prepare(); + // Ensure everything is preloaded. + runUntilIsLoading(player, /* expectedIsLoading= */ true); + runUntilIsLoading(player, /* expectedIsLoading= */ false); + // Seek behind the midroll. + player.seekTo(/* positionMs= */ 6_000); + // Wait until loading started again to assert loading events. + runUntilIsLoading(player, /* expectedIsLoading= */ true); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); Object periodUid = listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0); EventWindowAndPeriodId midrollAd = @@ -1357,8 +1381,6 @@ public void seekAfterMidroll() throws Exception { periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ C.INDEX_UNSET)); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( - WINDOW_0 /* setPlayWhenReady=true */, - WINDOW_0 /* setPlayWhenReady=false */, WINDOW_0 /* BUFFERING */, contentBeforeMidroll /* READY */, contentAfterMidroll /* BUFFERING */, @@ -1367,7 +1389,7 @@ public void seekAfterMidroll() throws Exception { contentAfterMidroll /* ENDED */) .inOrder(); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, contentBeforeMidroll /* SOURCE_UPDATE */); + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) .containsExactly( contentAfterMidroll /* seek */, @@ -1435,21 +1457,17 @@ public void seekAfterMidroll() throws Exception { @Test public void notifyExternalEvents() throws Exception { MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE); - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .pause() - .waitForPlaybackState(Player.STATE_READY) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(ExoPlayer player) { - player.getAnalyticsCollector().notifySeekStarted(); - } - }) - .seek(/* positionMs= */ 0) - .play() - .build(); - TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.setMediaSource(mediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_READY); + player.getAnalyticsCollector().notifySeekStarted(); + player.seekTo(/* positionMs= */ 0); + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); @@ -1460,7 +1478,14 @@ public void run(ExoPlayer player) { public void drmEvents_singlePeriod() throws Exception { MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, drmSessionManager, VIDEO_FORMAT_DRM_1); - TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.play(); + player.setMediaSource(mediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_ENDED); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty(); @@ -1488,18 +1513,21 @@ public void drmEvents_periodsWithSameDrmData_keysReusedButLoadEventReportedTwice SINGLE_PERIOD_TIMELINE, blockingDrmSessionManager, VIDEO_FORMAT_DRM_1), new FakeMediaSource( SINGLE_PERIOD_TIMELINE, blockingDrmSessionManager, VIDEO_FORMAT_DRM_1)); - TestAnalyticsListener listener = - runAnalyticsTest( - mediaSource, - // Wait for the media to be fully buffered before unblocking the DRM key request. This - // ensures both periods report the same load event (because period1's DRM session is - // already preacquired by the time the key load completes). - new ActionSchedule.Builder(TAG) - .waitForIsLoading(false) - .waitForIsLoading(true) - .waitForIsLoading(false) - .executeRunnable(mediaDrmCallback.keyCondition::open) - .build()); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.play(); + player.setMediaSource(mediaSource); + player.prepare(); + // Wait for the media to be fully buffered before unblocking the DRM key request. This + // ensures both periods report the same load event (because period1's DRM session is + // already preacquired by the time the key load completes). + runUntilIsLoading(player, /* expectedIsLoading= */ false); + runUntilIsLoading(player, /* expectedIsLoading= */ true); + runUntilIsLoading(player, /* expectedIsLoading= */ false); + mediaDrmCallback.keyCondition.open(); + runUntilPlaybackState(player, Player.STATE_ENDED); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty(); @@ -1525,7 +1553,14 @@ public void drmEvents_periodWithDifferentDrmData_keysLoadedAgain() throws Except SINGLE_PERIOD_TIMELINE, drmSessionManager, VIDEO_FORMAT_DRM_1.buildUpon().setDrmInitData(DRM_DATA_2).build())); - TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.play(); + player.setMediaSource(mediaSource); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_ENDED); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty(); @@ -1552,13 +1587,16 @@ public void drmEvents_errorHandling() throws Exception { .build(mediaDrmCallback); MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, failingDrmSessionManager, VIDEO_FORMAT_DRM_1); - TestAnalyticsListener listener = - runAnalyticsTest( - mediaSource, - new ActionSchedule.Builder(TAG) - .waitForIsLoading(false) - .executeRunnable(mediaDrmCallback.keyCondition::open) - .build()); + ExoPlayer player = setupPlayer(); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); + + player.play(); + player.setMediaSource(mediaSource); + player.prepare(); + runUntilIsLoading(player, /* expectedIsLoading= */ false); + mediaDrmCallback.keyCondition.open(); + runUntilError(player); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).containsExactly(period0); @@ -1588,12 +1626,14 @@ protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) } } }; + ExoPlayer player = setupPlayer(renderersFactory); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); - TestAnalyticsListener listener = - runAnalyticsTest( - new ConcatenatingMediaSource(source0, source1), - /* actionSchedule= */ null, - renderersFactory); + player.play(); + player.setMediaSource(new ConcatenatingMediaSource(source0, source1)); + player.prepare(); + runUntilError(player); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1); @@ -1622,12 +1662,14 @@ public void render(long positionUs, long realtimeUs) throws ExoPlaybackException } } }; + ExoPlayer player = setupPlayer(renderersFactory); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); - TestAnalyticsListener listener = - runAnalyticsTest( - new ConcatenatingMediaSource(source0, source1), - /* actionSchedule= */ null, - renderersFactory); + player.play(); + player.setMediaSource(new ConcatenatingMediaSource(source0, source1)); + player.prepare(); + runUntilError(player); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1); @@ -1660,12 +1702,14 @@ protected void onStreamChanged( } } }; + ExoPlayer player = setupPlayer(renderersFactory); + TestAnalyticsListener listener = new TestAnalyticsListener(); + player.addAnalyticsListener(listener); - TestAnalyticsListener listener = - runAnalyticsTest( - new ConcatenatingMediaSource(source, source), - /* actionSchedule= */ null, - renderersFactory); + player.play(); + player.setMediaSource(new ConcatenatingMediaSource(source, source)); + player.prepare(); + runUntilError(player); populateEventIds(listener.lastReportedTimeline); assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1); @@ -1673,11 +1717,7 @@ protected void onStreamChanged( @Test public void onEvents_isReportedWithCorrectEventTimes() throws Exception { - ExoPlayer player = - new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); - Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0)); - player.setVideoSurface(surface); - + ExoPlayer player = setupPlayer(); AnalyticsListener listener = mock(AnalyticsListener.class); Format[] formats = new Format[] { @@ -1690,20 +1730,18 @@ public void onEvents_isReportedWithCorrectEventTimes() throws Exception { player.setMediaSource(new FakeMediaSource(new FakeTimeline(), formats)); player.seekTo(2_000); player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f)); - ShadowLooper.runMainLooperToNextTask(); - + runMainLooperToNextTask(); // Move to another item and fail with a third one to trigger events with different EventTimes. player.prepare(); - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY); + runUntilPlaybackState(player, Player.STATE_READY); player.addMediaSource(new FakeMediaSource(new FakeTimeline(), formats)); player.play(); TestPlayerRunHelper.runUntilPositionDiscontinuity( player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); player.setMediaItem(MediaItem.fromUri("http://this-will-throw-an-exception.mp4")); - TestPlayerRunHelper.runUntilError(player); - ShadowLooper.runMainLooperToNextTask(); + runUntilError(player); + runMainLooperToNextTask(); player.release(); - surface.release(); // Verify that expected individual callbacks have been called and capture EventTimes. ArgumentCaptor individualTimelineChangedEventTimes = @@ -1928,48 +1966,6 @@ public void onEvents_isReportedWithCorrectEventTimes() throws Exception { .inOrder(); } - private void populateEventIds(Timeline timeline) { - period0 = - new EventWindowAndPeriodId( - /* windowIndex= */ 0, - new MediaPeriodId( - timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); - period0Seq0 = period0; - period0Seq1 = - new EventWindowAndPeriodId( - /* windowIndex= */ 0, - new MediaPeriodId( - timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1)); - window1Period0Seq1 = - new EventWindowAndPeriodId( - /* windowIndex= */ 1, - new MediaPeriodId( - timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1)); - if (timeline.getPeriodCount() > 1) { - period1 = - new EventWindowAndPeriodId( - /* windowIndex= */ 1, - new MediaPeriodId( - timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1)); - period1Seq1 = period1; - period1Seq0 = - new EventWindowAndPeriodId( - /* windowIndex= */ 1, - new MediaPeriodId( - timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0)); - period1Seq2 = - new EventWindowAndPeriodId( - /* windowIndex= */ 1, - new MediaPeriodId( - timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 2)); - window0Period1Seq0 = - new EventWindowAndPeriodId( - /* windowIndex= */ 0, - new MediaPeriodId( - timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0)); - } - } - @Test public void recursiveListenerInvocation_arrivesInCorrectOrder() { AnalyticsCollector analyticsCollector = new DefaultAnalyticsCollector(Clock.DEFAULT); @@ -2027,13 +2023,12 @@ public void onVideoDisabled(EventTime eventTime, DecoderCounters decoderCounters exoPlayer.setMediaSource( new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT)); exoPlayer.prepare(); - TestPlayerRunHelper.runUntilPlaybackState(exoPlayer, Player.STATE_READY); - + runUntilPlaybackState(exoPlayer, Player.STATE_READY); // Release and add delay on releasing thread to verify timestamps of events. exoPlayer.release(); long releaseTimeMs = fakeClock.currentTimeMillis(); fakeClock.advanceTime(1); - ShadowLooper.idleMainLooper(); + idleMainLooper(); // Verify video disable events and release events arrived in order. ArgumentCaptor videoDisabledEventTime = @@ -2059,49 +2054,79 @@ public void onVideoDisabled(EventTime eventTime, DecoderCounters decoderCounters assertThat(releasedEventTime.getValue().realtimeMs).isGreaterThan(videoDisableTimeMs); } - private static TestAnalyticsListener runAnalyticsTest(MediaSource mediaSource) throws Exception { - return runAnalyticsTest(mediaSource, /* actionSchedule= */ null); + private void populateEventIds(Timeline timeline) { + period0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); + period0Seq0 = period0; + period0Seq1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1)); + window1Period0Seq1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1)); + if (timeline.getPeriodCount() > 1) { + period1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1)); + period1Seq1 = period1; + period1Seq0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0)); + period1Seq2 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 2)); + window0Period1Seq0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0)); + } } - private static TestAnalyticsListener runAnalyticsTest( - MediaSource mediaSource, @Nullable ActionSchedule actionSchedule) throws Exception { - RenderersFactory renderersFactory = - (eventHandler, + private static ExoPlayer setupPlayer() { + Clock clock = new FakeClock(/* isAutoAdvancing= */ true); + return setupPlayer( + /* renderersFactory= */ (eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, - metadataRendererOutput) -> - new Renderer[] { - new FakeVideoRenderer(eventHandler, videoRendererEventListener), - new FakeAudioRenderer(eventHandler, audioRendererEventListener) - }; - return runAnalyticsTest(mediaSource, actionSchedule, renderersFactory); + metadataRendererOutput) -> { + HandlerWrapper clockAwareHandler = + clock.createHandler(eventHandler.getLooper(), /* callback= */ null); + return new Renderer[] { + new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener), + new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener) + }; + }, + clock); } - private static TestAnalyticsListener runAnalyticsTest( - MediaSource mediaSource, - @Nullable ActionSchedule actionSchedule, - RenderersFactory renderersFactory) - throws Exception { + private static ExoPlayer setupPlayer(RenderersFactory renderersFactory) { + return setupPlayer(renderersFactory, new FakeClock(/* isAutoAdvancing= */ true)); + } + + private static ExoPlayer setupPlayer(RenderersFactory renderersFactory, Clock clock) { Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0)); - TestAnalyticsListener listener = new TestAnalyticsListener(); - try { - new ExoPlayerTestRunner.Builder(ApplicationProvider.getApplicationContext()) - .setMediaSources(mediaSource) - .setRenderersFactory(renderersFactory) - .setVideoSurface(surface) - .setAnalyticsListener(listener) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - } catch (ExoPlaybackException e) { - // Ignore ExoPlaybackException as these may be expected. - } finally { - surface.release(); - } - return listener; + ExoPlayer player = + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) + .setClock(clock) + .setRenderersFactory(renderersFactory) + .build(); + player.setVideoSurface(surface); + return player; } private static final class EventWindowAndPeriodId { diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAudioRenderer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAudioRenderer.java index 7a476d16b5d..dc219d9b989 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAudioRenderer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAudioRenderer.java @@ -16,10 +16,10 @@ package androidx.media3.test.utils; -import android.os.Handler; import android.os.SystemClock; import androidx.media3.common.C; import androidx.media3.common.Format; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.DecoderCounters; import androidx.media3.exoplayer.ExoPlaybackException; @@ -29,13 +29,15 @@ @UnstableApi public class FakeAudioRenderer extends FakeRenderer { - private final AudioRendererEventListener.EventDispatcher eventDispatcher; + private final HandlerWrapper handler; + private final AudioRendererEventListener eventListener; private final DecoderCounters decoderCounters; private boolean notifiedPositionAdvancing; - public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) { + public FakeAudioRenderer(HandlerWrapper handler, AudioRendererEventListener eventListener) { super(C.TRACK_TYPE_AUDIO); - eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener); + this.handler = handler; + this.eventListener = eventListener; decoderCounters = new DecoderCounters(); } @@ -43,30 +45,33 @@ public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListen protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) throws ExoPlaybackException { super.onEnabled(joining, mayRenderStartOfStream); - eventDispatcher.enabled(decoderCounters); + handler.post(() -> eventListener.onAudioEnabled(decoderCounters)); notifiedPositionAdvancing = false; } @Override protected void onDisabled() { super.onDisabled(); - eventDispatcher.disabled(decoderCounters); + handler.post(() -> eventListener.onAudioDisabled(decoderCounters)); } @Override protected void onFormatChanged(Format format) { - eventDispatcher.inputFormatChanged(format, /* decoderReuseEvaluation= */ null); - eventDispatcher.decoderInitialized( - /* decoderName= */ "fake.audio.decoder", - /* initializedTimestampMs= */ SystemClock.elapsedRealtime(), - /* initializationDurationMs= */ 0); + handler.post( + () -> eventListener.onAudioInputFormatChanged(format, /* decoderReuseEvaluation= */ null)); + handler.post( + () -> + eventListener.onAudioDecoderInitialized( + /* decoderName= */ "fake.audio.decoder", + /* initializedTimestampMs= */ SystemClock.elapsedRealtime(), + /* initializationDurationMs= */ 0)); } @Override protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) { boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs); if (shouldProcess && !notifiedPositionAdvancing) { - eventDispatcher.positionAdvancing(System.currentTimeMillis()); + handler.post(() -> eventListener.onAudioPositionAdvancing(System.currentTimeMillis())); notifiedPositionAdvancing = true; } return shouldProcess; diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeVideoRenderer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeVideoRenderer.java index 9c9a4bb1b7e..45de4ecad12 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeVideoRenderer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeVideoRenderer.java @@ -16,13 +16,13 @@ package androidx.media3.test.utils; -import android.os.Handler; import android.os.SystemClock; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.VideoSize; import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.DecoderCounters; import androidx.media3.exoplayer.ExoPlaybackException; @@ -34,7 +34,8 @@ @UnstableApi public class FakeVideoRenderer extends FakeRenderer { - private final VideoRendererEventListener.EventDispatcher eventDispatcher; + private final HandlerWrapper handler; + private final VideoRendererEventListener eventListener; private final DecoderCounters decoderCounters; private @MonotonicNonNull Format format; @Nullable private Object output; @@ -43,9 +44,10 @@ public class FakeVideoRenderer extends FakeRenderer { private boolean mayRenderFirstFrameAfterEnableIfNotStarted; private boolean renderedFirstFrameAfterEnable; - public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) { + public FakeVideoRenderer(HandlerWrapper handler, VideoRendererEventListener eventListener) { super(C.TRACK_TYPE_VIDEO); - eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener); + this.handler = handler; + this.eventListener = eventListener; decoderCounters = new DecoderCounters(); } @@ -53,7 +55,7 @@ public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListen protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) throws ExoPlaybackException { super.onEnabled(joining, mayRenderStartOfStream); - eventDispatcher.enabled(decoderCounters); + handler.post(() -> eventListener.onVideoEnabled(decoderCounters)); mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream; renderedFirstFrameAfterEnable = false; } @@ -69,15 +71,17 @@ protected void onStreamChanged(Format[] formats, long startPositionUs, long offs @Override protected void onStopped() { super.onStopped(); - eventDispatcher.droppedFrames(/* droppedFrameCount= */ 0, /* elapsedMs= */ 0); - eventDispatcher.reportVideoFrameProcessingOffset( - /* totalProcessingOffsetUs= */ 400000, /* frameCount= */ 10); + handler.post(() -> eventListener.onDroppedFrames(/* count= */ 0, /* elapsedMs= */ 0)); + handler.post( + () -> + eventListener.onVideoFrameProcessingOffset( + /* totalProcessingOffsetUs= */ 400000, /* frameCount= */ 10)); } @Override protected void onDisabled() { super.onDisabled(); - eventDispatcher.disabled(decoderCounters); + handler.post(() -> eventListener.onVideoDisabled(decoderCounters)); } @Override @@ -88,11 +92,14 @@ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlayb @Override protected void onFormatChanged(Format format) { - eventDispatcher.inputFormatChanged(format, /* decoderReuseEvaluation= */ null); - eventDispatcher.decoderInitialized( - /* decoderName= */ "fake.video.decoder", - /* initializedTimestampMs= */ SystemClock.elapsedRealtime(), - /* initializationDurationMs= */ 0); + handler.post( + () -> eventListener.onVideoInputFormatChanged(format, /* decoderReuseEvaluation= */ null)); + handler.post( + () -> + eventListener.onVideoDecoderInitialized( + /* decoderName= */ "fake.video.decoder", + /* initializedTimestampMs= */ SystemClock.elapsedRealtime(), + /* initializationDurationMs= */ 0)); this.format = format; } @@ -133,10 +140,18 @@ protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs @Nullable Object output = this.output; if (shouldProcess && !renderedFirstFrameAfterReset && output != null) { @MonotonicNonNull Format format = Assertions.checkNotNull(this.format); - eventDispatcher.videoSizeChanged( - new VideoSize( - format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio)); - eventDispatcher.renderedFirstFrame(output); + handler.post( + () -> + eventListener.onVideoSizeChanged( + new VideoSize( + format.width, + format.height, + format.rotationDegrees, + format.pixelWidthHeightRatio))); + handler.post( + () -> + eventListener.onRenderedFirstFrame( + output, /* renderTimeMs= */ SystemClock.elapsedRealtime())); renderedFirstFrameAfterReset = true; renderedFirstFrameAfterEnable = true; } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java index f5e02695bf9..afc58e3ef8c 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java @@ -23,6 +23,7 @@ import androidx.media3.common.C; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Clock; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.ExoPlayer; @@ -299,13 +300,16 @@ public ExoPlayer build() { videoRendererEventListener, audioRendererEventListener, textRendererOutput, - metadataRendererOutput) -> - renderers != null - ? renderers - : new Renderer[] { - new FakeVideoRenderer(eventHandler, videoRendererEventListener), - new FakeAudioRenderer(eventHandler, audioRendererEventListener) - }; + metadataRendererOutput) -> { + HandlerWrapper clockAwareHandler = + clock.createHandler(eventHandler.getLooper(), /* callback= */ null); + return renderers != null + ? renderers + : new Renderer[] { + new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener), + new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener) + }; + }; } ExoPlayer.Builder builder = diff --git a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java index 54d62208ef8..173b6d26835 100644 --- a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java +++ b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java @@ -91,6 +91,30 @@ public static void runUntilPlayWhenReady(Player player, boolean expectedPlayWhen } } + /** + * Runs tasks of the main {@link Looper} until {@link Player#isLoading()} matches the expected + * value or a playback error occurs. + * + *

    If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}. + * + * @param player The {@link Player}. + * @param expectedIsLoading The expected value for {@link Player#isLoading()}. + * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. + */ + public static void runUntilIsLoading(Player player, boolean expectedIsLoading) + throws TimeoutException { + verifyMainTestThread(player); + if (player instanceof ExoPlayer) { + verifyPlaybackThreadIsAlive((ExoPlayer) player); + } + runMainLooperUntil( + () -> player.isLoading() == expectedIsLoading || player.getPlayerError() != null); + if (player.getPlayerError() != null) { + throw new IllegalStateException(player.getPlayerError()); + } + } + /** * Runs tasks of the main {@link Looper} until {@link Player#getCurrentTimeline()} matches the * expected timeline or a playback error occurs. From d5815c5ab0b110cd9f34530a7a3f0e0a2f43130a Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 25 Nov 2022 17:55:12 +0000 Subject: [PATCH 031/141] Clean up javadoc on `Metadata.Entry.populateMediaMetadata` Remove self-links, and remove section that is documenting internal ordering behaviour of [`SimpleBasePlayer.getCombinedMediaMetadata`](https://github.com/google/ExoPlayer/blob/bb270c62cf2f7a1570fe22f87bb348a2d5e94dcf/library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java#L1770) rather than anything specifically about this method. #minor-release PiperOrigin-RevId: 490923719 (cherry picked from commit a6703285d0d1bedd946a8477cb68c46b1a097b09) --- .../src/main/java/androidx/media3/common/Metadata.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Metadata.java b/libraries/common/src/main/java/androidx/media3/common/Metadata.java index 6a558425518..201d9b1296c 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Metadata.java +++ b/libraries/common/src/main/java/androidx/media3/common/Metadata.java @@ -50,11 +50,8 @@ default byte[] getWrappedMetadataBytes() { } /** - * Updates the {@link MediaMetadata.Builder} with the type specific values stored in this Entry. - * - *

    The order of the {@link Entry} objects in the {@link Metadata} matters. If two {@link - * Entry} entries attempt to populate the same {@link MediaMetadata} field, then the last one in - * the list is used. + * Updates the {@link MediaMetadata.Builder} with the type-specific values stored in this {@code + * Entry}. * * @param builder The builder to be updated. */ From f9c6fb4e90b6190c0653a491dd7aacc766246bc6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 28 Nov 2022 09:25:18 +0000 Subject: [PATCH 032/141] Ensure messages sent on a dead thread don't block FakeClock execution FakeClock keeps an internal list of messages to be executed to ensure deterministic serialization. The next message from the list is triggered by a separate helper message sent to the real Handler. However, if the target HandlerThread is no longer alive (e.g. when it quit itself during the message execution), this helper message is never executed and the entire message execution chain is stuck forever. This can be solved by checking the return values of Hander.post or Handler.sendMessage, which are false if the message won't be delivered. If the messages are not delivered, we can unblock the chain by marking the message as complete and triggering the next one. PiperOrigin-RevId: 491275031 (cherry picked from commit 8fcc06309323847b47ed8ab225cd861335448d36) --- .../androidx/media3/test/utils/FakeClock.java | 17 +++--- .../media3/test/utils/FakeClockTest.java | 54 +++++++++++++++++-- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeClock.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeClock.java index bbd558208a2..db37f664055 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeClock.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeClock.java @@ -244,16 +244,19 @@ private synchronized void maybeTriggerMessage() { } handlerMessages.remove(messageIndex); waitingForMessage = true; + boolean messageSent; + Handler realHandler = message.handler.handler; if (message.runnable != null) { - message.handler.handler.post(message.runnable); + messageSent = realHandler.post(message.runnable); } else { - message - .handler - .handler - .obtainMessage(message.what, message.arg1, message.arg2, message.obj) - .sendToTarget(); + messageSent = + realHandler.sendMessage( + realHandler.obtainMessage(message.what, message.arg1, message.arg2, message.obj)); + } + messageSent &= message.handler.internalHandler.post(this::onMessageHandled); + if (!messageSent) { + onMessageHandled(); } - message.handler.internalHandler.post(this::onMessageHandled); } private synchronized void onMessageHandled() { diff --git a/libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeClockTest.java b/libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeClockTest.java index 393ce98408b..51dd19bab56 100644 --- a/libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeClockTest.java +++ b/libraries/test_utils/src/test/java/androidx/media3/test/utils/FakeClockTest.java @@ -29,6 +29,7 @@ import com.google.common.collect.Iterables; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.shadows.ShadowLooper; @@ -40,6 +41,7 @@ public final class FakeClockTest { @Test public void currentTimeMillis_withoutBootTime() { FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 10); + assertThat(fakeClock.currentTimeMillis()).isEqualTo(10); } @@ -48,6 +50,7 @@ public void currentTimeMillis_withBootTime() { FakeClock fakeClock = new FakeClock( /* bootTimeMs= */ 150, /* initialTimeMs= */ 200, /* isAutoAdvancing= */ false); + assertThat(fakeClock.currentTimeMillis()).isEqualTo(350); } @@ -55,17 +58,24 @@ public void currentTimeMillis_withBootTime() { public void currentTimeMillis_afterAdvanceTime_currentTimeHasAdvanced() { FakeClock fakeClock = new FakeClock(/* bootTimeMs= */ 100, /* initialTimeMs= */ 50, /* isAutoAdvancing= */ false); + fakeClock.advanceTime(/* timeDiffMs */ 250); + assertThat(fakeClock.currentTimeMillis()).isEqualTo(400); } @Test public void elapsedRealtime_afterAdvanceTime_timeHasAdvanced() { FakeClock fakeClock = new FakeClock(2000); + assertThat(fakeClock.elapsedRealtime()).isEqualTo(2000); + fakeClock.advanceTime(500); + assertThat(fakeClock.elapsedRealtime()).isEqualTo(2500); + fakeClock.advanceTime(0); + assertThat(fakeClock.elapsedRealtime()).isEqualTo(2500); } @@ -86,6 +96,7 @@ public void createHandler_obtainMessageSendToTarget_triggersMessage() { .sendToTarget(); ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); + handlerThread.quitSafely(); assertThat(callback.messages) .containsExactly( @@ -126,6 +137,7 @@ public void createHandler_sendEmptyMessage_triggersMessageAtCorrectTime() { fakeClock.advanceTime(50); shadowOf(handler.getLooper()).idle(); + handlerThread.quitSafely(); assertThat(callback.messages).hasSize(4); assertThat(Iterables.getLast(callback.messages)) @@ -146,6 +158,7 @@ public void createHandler_sendMessageAtFrontOfQueue_sendsMessageFirst() { handler.obtainMessage(/* what= */ 4).sendToTarget(); ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); + handlerThread.quitSafely(); assertThat(callback.messages) .containsExactly( @@ -192,6 +205,8 @@ public void createHandler_postDelayed_triggersMessagesUpToCurrentTime() { fakeClock.advanceTime(1000); shadowOf(handler.getLooper()).idle(); assertTestRunnableStates(new boolean[] {true, true, true, true, true}, testRunnables); + + handlerThread.quitSafely(); } @Test @@ -203,7 +218,6 @@ public void createHandler_removeMessages_removesMessages() { HandlerWrapper handler = fakeClock.createHandler(handlerThread.getLooper(), callback); TestCallback otherCallback = new TestCallback(); HandlerWrapper otherHandler = fakeClock.createHandler(handlerThread.getLooper(), otherCallback); - TestRunnable testRunnable1 = new TestRunnable(); TestRunnable testRunnable2 = new TestRunnable(); Object messageToken = new Object(); @@ -216,10 +230,10 @@ public void createHandler_removeMessages_removesMessages() { handler.removeMessages(/* what= */ 2); handler.removeCallbacksAndMessages(messageToken); - fakeClock.advanceTime(50); ShadowLooper.idleMainLooper(); shadowOf(handlerThread.getLooper()).idle(); + handlerThread.quitSafely(); assertThat(callback.messages) .containsExactly( @@ -242,7 +256,6 @@ public void createHandler_removeAllMessages_removesAllMessages() { HandlerWrapper handler = fakeClock.createHandler(handlerThread.getLooper(), callback); TestCallback otherCallback = new TestCallback(); HandlerWrapper otherHandler = fakeClock.createHandler(handlerThread.getLooper(), otherCallback); - TestRunnable testRunnable1 = new TestRunnable(); TestRunnable testRunnable2 = new TestRunnable(); Object messageToken = new Object(); @@ -254,15 +267,14 @@ public void createHandler_removeAllMessages_removesAllMessages() { otherHandler.sendEmptyMessage(/* what= */ 1); handler.removeCallbacksAndMessages(/* token= */ null); - fakeClock.advanceTime(50); ShadowLooper.idleMainLooper(); shadowOf(handlerThread.getLooper()).idle(); + handlerThread.quitSafely(); assertThat(callback.messages).isEmpty(); assertThat(testRunnable1.hasRun).isFalse(); assertThat(testRunnable2.hasRun).isFalse(); - // Assert that message on other handler wasn't removed. assertThat(otherCallback.messages) .containsExactly( @@ -295,6 +307,7 @@ public void createHandler_withIsAutoAdvancing_advancesTimeToNextMessages() { }); ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); + handlerThread.quitSafely(); assertThat(clockTimes).containsExactly(0L, 20L, 50L, 70L, 100L).inOrder(); } @@ -333,6 +346,8 @@ public void createHandler_multiThreadCommunication_deliversMessagesDeterministic }); ShadowLooper.idleMainLooper(); messagesFinished.block(); + handlerThread1.quitSafely(); + handlerThread2.quitSafely(); assertThat(executionOrder).containsExactly(1, 2, 3, 4, 5, 6, 7, 8).inOrder(); } @@ -368,10 +383,39 @@ public void createHandler_blockingThreadWithOnBusyWaiting_canBeUnblockedByOtherT ShadowLooper.idleMainLooper(); shadowOf(handler1.getLooper()).idle(); shadowOf(handler2.getLooper()).idle(); + handlerThread1.quitSafely(); + handlerThread2.quitSafely(); assertThat(executionOrder).containsExactly(1, 2, 3, 4).inOrder(); } + @Test + public void createHandler_messageOnDeadThread_doesNotBlockExecution() { + HandlerThread handlerThread1 = new HandlerThread("FakeClockTest"); + handlerThread1.start(); + HandlerThread handlerThread2 = new HandlerThread("FakeClockTest"); + handlerThread2.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0); + HandlerWrapper handler1 = + fakeClock.createHandler(handlerThread1.getLooper(), /* callback= */ null); + HandlerWrapper handler2 = + fakeClock.createHandler(handlerThread2.getLooper(), /* callback= */ null); + + ConditionVariable messagesFinished = new ConditionVariable(); + AtomicBoolean messageOnDeadThreadExecuted = new AtomicBoolean(); + handler1.post( + () -> { + handlerThread1.quitSafely(); + handler1.post(() -> messageOnDeadThreadExecuted.set(true)); + handler2.post(messagesFinished::open); + }); + ShadowLooper.idleMainLooper(); + messagesFinished.block(); + handlerThread2.quitSafely(); + + assertThat(messageOnDeadThreadExecuted.get()).isFalse(); + } + private static void assertTestRunnableStates(boolean[] states, TestRunnable[] testRunnables) { for (int i = 0; i < testRunnables.length; i++) { assertThat(testRunnables[i].hasRun).isEqualTo(states[i]); From 887179f50ae6164ffd527aaae96fdc02ae6dc496 Mon Sep 17 00:00:00 2001 From: Rohit Singh Date: Tue, 29 Nov 2022 18:35:59 +0000 Subject: [PATCH 033/141] Merge pull request #10799 from OxygenCobalt:id3v2-multi-value PiperOrigin-RevId: 491289028 (cherry picked from commit b81d5f304e2f5fc55577e31c31ff6df5ce7d0ef5) --- RELEASENOTES.md | 3 + .../media3/exoplayer/ExoPlayerTest.java | 4 +- .../metadata/MetadataRendererTest.java | 2 +- .../ImaServerSideAdInsertionMediaSource.java | 2 +- .../extractor/metadata/id3/Id3Decoder.java | 53 ++++++++---- .../metadata/id3/TextInformationFrame.java | 81 +++++++++++++------ .../media3/extractor/mp3/Mp3Extractor.java | 2 +- .../media3/extractor/mp4/MetadataUtil.java | 12 ++- .../metadata/id3/ChapterFrameTest.java | 3 +- .../metadata/id3/ChapterTocFrameTest.java | 3 +- .../metadata/id3/Id3DecoderTest.java | 32 ++++++-- .../id3/TextInformationFrameTest.java | 77 +++++++++++++++--- .../flac/bear_with_id3_enabled_flac.0.dump | 2 +- .../flac/bear_with_id3_enabled_flac.1.dump | 2 +- .../flac/bear_with_id3_enabled_flac.2.dump | 2 +- .../flac/bear_with_id3_enabled_flac.3.dump | 2 +- ..._with_id3_enabled_flac.unknown_length.dump | 2 +- .../flac/bear_with_id3_enabled_raw.0.dump | 2 +- .../flac/bear_with_id3_enabled_raw.1.dump | 2 +- .../flac/bear_with_id3_enabled_raw.2.dump | 2 +- .../flac/bear_with_id3_enabled_raw.3.dump | 2 +- ...r_with_id3_enabled_raw.unknown_length.dump | 2 +- .../mp3/bear-id3-enabled.0.dump | 2 +- .../mp3/bear-id3-enabled.1.dump | 2 +- .../mp3/bear-id3-enabled.2.dump | 2 +- .../mp3/bear-id3-enabled.3.dump | 2 +- .../mp3/bear-id3-enabled.unknown_length.dump | 2 +- .../mp3/bear-vbr-xing-header.mp3.0.dump | 2 +- .../mp3/bear-vbr-xing-header.mp3.1.dump | 2 +- .../mp3/bear-vbr-xing-header.mp3.2.dump | 2 +- .../mp3/bear-vbr-xing-header.mp3.3.dump | 2 +- ...ar-vbr-xing-header.mp3.unknown_length.dump | 2 +- .../extractordumps/mp4/sample.mp4.0.dump | 2 +- .../extractordumps/mp4/sample.mp4.1.dump | 2 +- .../extractordumps/mp4/sample.mp4.2.dump | 2 +- .../extractordumps/mp4/sample.mp4.3.dump | 2 +- .../mp4/sample.mp4.unknown_length.dump | 2 +- .../mp4/sample_mdat_too_long.mp4.0.dump | 2 +- .../mp4/sample_mdat_too_long.mp4.1.dump | 2 +- .../mp4/sample_mdat_too_long.mp4.2.dump | 2 +- .../mp4/sample_mdat_too_long.mp4.3.dump | 2 +- ...mple_mdat_too_long.mp4.unknown_length.dump | 2 +- .../extractordumps/mp4/sample_opus.mp4.0.dump | 2 +- .../extractordumps/mp4/sample_opus.mp4.1.dump | 2 +- .../extractordumps/mp4/sample_opus.mp4.2.dump | 2 +- .../extractordumps/mp4/sample_opus.mp4.3.dump | 2 +- .../mp4/sample_opus.mp4.unknown_length.dump | 2 +- .../sample_with_colr_mdcv_and_clli.mp4.0.dump | 2 +- .../sample_with_colr_mdcv_and_clli.mp4.1.dump | 2 +- .../sample_with_colr_mdcv_and_clli.mp4.2.dump | 2 +- .../sample_with_colr_mdcv_and_clli.mp4.3.dump | 2 +- ...colr_mdcv_and_clli.mp4.unknown_length.dump | 2 +- .../transformerdumps/mp4/sample.mp4.dump | 2 +- .../mp4/sample.mp4.novideo.dump | 2 +- ...sing_timestamps_320w_240h.mp4.clipped.dump | 2 +- 55 files changed, 248 insertions(+), 112 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 854f943d895..24a1db83590 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,9 @@ * Session: * Add helper method to convert platform session token to Media3 `SessionToken` ([#171](https://github.com/androidx/media/issues/171)). +* Metadata: + * Parse multiple null-separated values from ID3 frames, as permitted by + ID3 v2.4. ### 1.0.0-beta03 (2022-11-22) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 14f92f873d8..eef9c43b4bf 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -10408,7 +10408,9 @@ public void onEvents_correspondToListenerCalls() throws Exception { new Metadata( new BinaryFrame(/* id= */ "", /* data= */ new byte[0]), new TextInformationFrame( - /* id= */ "TT2", /* description= */ null, /* value= */ "title"))) + /* id= */ "TT2", + /* description= */ null, + /* values= */ ImmutableList.of("title")))) .build(); // Set multiple values together. diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/metadata/MetadataRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/metadata/MetadataRendererTest.java index 8409d3f4d94..9e248def394 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/metadata/MetadataRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/metadata/MetadataRendererTest.java @@ -108,7 +108,7 @@ public void decodeMetadata_handlesId3WrappedInEmsg() throws Exception { assertThat(metadata).hasSize(1); assertThat(metadata.get(0).length()).isEqualTo(1); TextInformationFrame expectedId3Frame = - new TextInformationFrame("TXXX", "Test description", "Test value"); + new TextInformationFrame("TXXX", "Test description", ImmutableList.of("Test value")); assertThat(metadata.get(0).get(0)).isEqualTo(expectedId3Frame); } diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index e5467d7a549..150c852a91e 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -813,7 +813,7 @@ public void onMetadata(Metadata metadata) { if (entry instanceof TextInformationFrame) { TextInformationFrame textFrame = (TextInformationFrame) entry; if ("TXXX".equals(textFrame.id)) { - streamPlayer.triggerUserTextReceived(textFrame.value); + streamPlayer.triggerUserTextReceived(textFrame.values.get(0)); } } else if (entry instanceof EventMessage) { EventMessage eventMessage = (EventMessage) entry; diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java index 4dacad9b75a..19854db16bf 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java @@ -26,6 +26,7 @@ import androidx.media3.extractor.metadata.MetadataInputBuffer; import androidx.media3.extractor.metadata.SimpleMetadataDecoder; import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -457,14 +458,13 @@ private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, i byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); - int descriptionEndIndex = indexOfEos(data, 0, encoding); + int descriptionEndIndex = indexOfTerminator(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); - int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); - int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); - String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset); - - return new TextInformationFrame("TXXX", description, value); + ImmutableList values = + decodeTextInformationFrameValues( + data, encoding, descriptionEndIndex + delimiterLength(encoding)); + return new TextInformationFrame("TXXX", description, values); } @Nullable @@ -476,15 +476,34 @@ private static TextInformationFrame decodeTextInformationFrame( } int encoding = id3Data.readUnsignedByte(); - String charset = getCharsetName(encoding); byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); - int valueEndIndex = indexOfEos(data, 0, encoding); - String value = new String(data, 0, valueEndIndex, charset); + ImmutableList values = decodeTextInformationFrameValues(data, encoding, 0); + return new TextInformationFrame(id, null, values); + } + + private static ImmutableList decodeTextInformationFrameValues( + byte[] data, final int encoding, final int index) throws UnsupportedEncodingException { + if (index >= data.length) { + return ImmutableList.of(""); + } + + ImmutableList.Builder values = ImmutableList.builder(); + String charset = getCharsetName(encoding); + int valueStartIndex = index; + int valueEndIndex = indexOfTerminator(data, valueStartIndex, encoding); + while (valueStartIndex < valueEndIndex) { + String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); + values.add(value); + + valueStartIndex = valueEndIndex + delimiterLength(encoding); + valueEndIndex = indexOfTerminator(data, valueStartIndex, encoding); + } - return new TextInformationFrame(id, null, value); + ImmutableList result = values.build(); + return result.isEmpty() ? ImmutableList.of("") : result; } @Nullable @@ -501,7 +520,7 @@ private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frame byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); - int descriptionEndIndex = indexOfEos(data, 0, encoding); + int descriptionEndIndex = indexOfTerminator(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); int urlStartIndex = descriptionEndIndex + delimiterLength(encoding); @@ -548,11 +567,11 @@ private static GeobFrame decodeGeobFrame(ParsableByteArray id3Data, int frameSiz String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"); int filenameStartIndex = mimeTypeEndIndex + 1; - int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding); + int filenameEndIndex = indexOfTerminator(data, filenameStartIndex, encoding); String filename = decodeStringIfValid(data, filenameStartIndex, filenameEndIndex, charset); int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding); - int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding); + int descriptionEndIndex = indexOfTerminator(data, descriptionStartIndex, encoding); String description = decodeStringIfValid(data, descriptionStartIndex, descriptionEndIndex, charset); @@ -590,7 +609,7 @@ private static ApicFrame decodeApicFrame( int pictureType = data[mimeTypeEndIndex + 1] & 0xFF; int descriptionStartIndex = mimeTypeEndIndex + 2; - int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding); + int descriptionEndIndex = indexOfTerminator(data, descriptionStartIndex, encoding); String description = new String( data, descriptionStartIndex, descriptionEndIndex - descriptionStartIndex, charset); @@ -619,11 +638,11 @@ private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int fr data = new byte[frameSize - 4]; id3Data.readBytes(data, 0, frameSize - 4); - int descriptionEndIndex = indexOfEos(data, 0, encoding); + int descriptionEndIndex = indexOfTerminator(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); int textStartIndex = descriptionEndIndex + delimiterLength(encoding); - int textEndIndex = indexOfEos(data, textStartIndex, encoding); + int textEndIndex = indexOfTerminator(data, textStartIndex, encoding); String text = decodeStringIfValid(data, textStartIndex, textEndIndex, charset); return new CommentFrame(language, description, text); @@ -800,7 +819,7 @@ private static String getFrameId( : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); } - private static int indexOfEos(byte[] data, int fromIndex, int encoding) { + private static int indexOfTerminator(byte[] data, int fromIndex, int encoding) { int terminationPos = indexOfZeroByte(data, fromIndex); // For single byte encoding charsets, we're done. diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/TextInformationFrame.java b/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/TextInformationFrame.java index c33ef14e312..04d3d17463e 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/TextInformationFrame.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/TextInformationFrame.java @@ -15,7 +15,8 @@ */ package androidx.media3.extractor.metadata.id3; -import static androidx.media3.common.util.Util.castNonNull; +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; import android.os.Parcel; import android.os.Parcelable; @@ -23,6 +24,8 @@ import androidx.media3.common.MediaMetadata; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.InlineMe; import java.util.ArrayList; import java.util.List; @@ -31,42 +34,69 @@ public final class TextInformationFrame extends Id3Frame { @Nullable public final String description; - public final String value; - public TextInformationFrame(String id, @Nullable String description, String value) { + /** + * @deprecated Use the first element of {@link #values} instead. + */ + @Deprecated public final String value; + + /** The text values of this frame. Will always have at least one element. */ + public final ImmutableList values; + + public TextInformationFrame(String id, @Nullable String description, List values) { super(id); + checkArgument(!values.isEmpty()); + this.description = description; - this.value = value; + this.values = ImmutableList.copyOf(values); + this.value = this.values.get(0); + } + + /** + * @deprecated Use {@code TextInformationFrame(String id, String description, String[] values} + * instead + */ + @Deprecated + @InlineMe( + replacement = "this(id, description, ImmutableList.of(value))", + imports = "com.google.common.collect.ImmutableList") + public TextInformationFrame(String id, @Nullable String description, String value) { + this(id, description, ImmutableList.of(value)); } - /* package */ TextInformationFrame(Parcel in) { - super(castNonNull(in.readString())); - description = in.readString(); - value = castNonNull(in.readString()); + private TextInformationFrame(Parcel in) { + this( + checkNotNull(in.readString()), + in.readString(), + ImmutableList.copyOf(checkNotNull(in.createStringArray()))); } + /** + * Uses the first element in {@link #values} to set the relevant field in {@link MediaMetadata} + * (as determined by {@link #id}). + */ @Override public void populateMediaMetadata(MediaMetadata.Builder builder) { switch (id) { case "TT2": case "TIT2": - builder.setTitle(value); + builder.setTitle(values.get(0)); break; case "TP1": case "TPE1": - builder.setArtist(value); + builder.setArtist(values.get(0)); break; case "TP2": case "TPE2": - builder.setAlbumArtist(value); + builder.setAlbumArtist(values.get(0)); break; case "TAL": case "TALB": - builder.setAlbumTitle(value); + builder.setAlbumTitle(values.get(0)); break; case "TRK": case "TRCK": - String[] trackNumbers = Util.split(value, "/"); + String[] trackNumbers = Util.split(values.get(0), "/"); try { int trackNumber = Integer.parseInt(trackNumbers[0]); @Nullable @@ -80,7 +110,7 @@ public void populateMediaMetadata(MediaMetadata.Builder builder) { case "TYE": case "TYER": try { - builder.setRecordingYear(Integer.parseInt(value)); + builder.setRecordingYear(Integer.parseInt(values.get(0))); } catch (NumberFormatException e) { // Do nothing, invalid input. } @@ -88,15 +118,16 @@ public void populateMediaMetadata(MediaMetadata.Builder builder) { case "TDA": case "TDAT": try { - int month = Integer.parseInt(value.substring(2, 4)); - int day = Integer.parseInt(value.substring(0, 2)); + String date = values.get(0); + int month = Integer.parseInt(date.substring(2, 4)); + int day = Integer.parseInt(date.substring(0, 2)); builder.setRecordingMonth(month).setRecordingDay(day); } catch (NumberFormatException | StringIndexOutOfBoundsException e) { // Do nothing, invalid input. } break; case "TDRC": - List recordingDate = parseId3v2point4TimestampFrameForDate(value); + List recordingDate = parseId3v2point4TimestampFrameForDate(values.get(0)); switch (recordingDate.size()) { case 3: builder.setRecordingDay(recordingDate.get(2)); @@ -114,7 +145,7 @@ public void populateMediaMetadata(MediaMetadata.Builder builder) { } break; case "TDRL": - List releaseDate = parseId3v2point4TimestampFrameForDate(value); + List releaseDate = parseId3v2point4TimestampFrameForDate(values.get(0)); switch (releaseDate.size()) { case 3: builder.setReleaseDay(releaseDate.get(2)); @@ -133,15 +164,15 @@ public void populateMediaMetadata(MediaMetadata.Builder builder) { break; case "TCM": case "TCOM": - builder.setComposer(value); + builder.setComposer(values.get(0)); break; case "TP3": case "TPE3": - builder.setConductor(value); + builder.setConductor(values.get(0)); break; case "TXT": case "TEXT": - builder.setWriter(value); + builder.setWriter(values.get(0)); break; default: break; @@ -159,7 +190,7 @@ public boolean equals(@Nullable Object obj) { TextInformationFrame other = (TextInformationFrame) obj; return Util.areEqual(id, other.id) && Util.areEqual(description, other.description) - && Util.areEqual(value, other.value); + && values.equals(other.values); } @Override @@ -167,13 +198,13 @@ public int hashCode() { int result = 17; result = 31 * result + id.hashCode(); result = 31 * result + (description != null ? description.hashCode() : 0); - result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + values.hashCode(); return result; } @Override public String toString() { - return id + ": description=" + description + ": value=" + value; + return id + ": description=" + description + ": values=" + values; } // Parcelable implementation. @@ -182,7 +213,7 @@ public String toString() { public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); dest.writeString(description); - dest.writeString(value); + dest.writeStringArray(values.toArray(new String[0])); } public static final Parcelable.Creator CREATOR = diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Extractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Extractor.java index 6279da9e631..3c90957f646 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Extractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Extractor.java @@ -594,7 +594,7 @@ private static long getId3TlenUs(@Nullable Metadata metadata) { Metadata.Entry entry = metadata.get(i); if (entry instanceof TextInformationFrame && ((TextInformationFrame) entry).id.equals("TLEN")) { - return Util.msToUs(Long.parseLong(((TextInformationFrame) entry).value)); + return Util.msToUs(Long.parseLong(((TextInformationFrame) entry).values.get(0))); } } } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/MetadataUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/MetadataUtil.java index 4c111af2411..023d573e29b 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/MetadataUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/MetadataUtil.java @@ -31,6 +31,7 @@ import androidx.media3.extractor.metadata.id3.InternalFrame; import androidx.media3.extractor.metadata.id3.TextInformationFrame; import androidx.media3.extractor.metadata.mp4.MdtaMetadataEntry; +import com.google.common.collect.ImmutableList; import org.checkerframework.checker.nullness.compatqual.NullableType; /** Utilities for handling metadata in MP4. */ @@ -452,7 +453,7 @@ private static TextInformationFrame parseTextAttribute( if (atomType == Atom.TYPE_data) { data.skipBytes(8); // version (1), flags (3), empty (4) String value = data.readNullTerminatedString(atomSize - 16); - return new TextInformationFrame(id, /* description= */ null, value); + return new TextInformationFrame(id, /* description= */ null, ImmutableList.of(value)); } Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type)); return null; @@ -484,7 +485,8 @@ private static Id3Frame parseUint8Attribute( } if (value >= 0) { return isTextInformationFrame - ? new TextInformationFrame(id, /* description= */ null, Integer.toString(value)) + ? new TextInformationFrame( + id, /* description= */ null, ImmutableList.of(Integer.toString(value))) : new CommentFrame(C.LANGUAGE_UNDETERMINED, id, Integer.toString(value)); } Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); @@ -505,7 +507,8 @@ private static TextInformationFrame parseIndexAndCountAttribute( if (count > 0) { value += "/" + count; } - return new TextInformationFrame(attributeName, /* description= */ null, value); + return new TextInformationFrame( + attributeName, /* description= */ null, ImmutableList.of(value)); } } Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); @@ -521,7 +524,8 @@ private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArra ? STANDARD_GENRES[genreCode - 1] : null; if (genreString != null) { - return new TextInformationFrame("TCON", /* description= */ null, genreString); + return new TextInformationFrame( + "TCON", /* description= */ null, ImmutableList.of(genreString)); } Log.w(TAG, "Failed to parse standard genre code"); return null; diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/ChapterFrameTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/ChapterFrameTest.java index d2042f1798c..38c164d14e5 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/ChapterFrameTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/ChapterFrameTest.java @@ -19,6 +19,7 @@ import android.os.Parcel; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; @@ -30,7 +31,7 @@ public final class ChapterFrameTest { public void parcelable() { Id3Frame[] subFrames = new Id3Frame[] { - new TextInformationFrame("TIT2", null, "title"), + new TextInformationFrame("TIT2", null, ImmutableList.of("title")), new UrlLinkFrame("WXXX", "description", "url") }; ChapterFrame chapterFrameToParcel = new ChapterFrame("id", 0, 1, 2, 3, subFrames); diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/ChapterTocFrameTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/ChapterTocFrameTest.java index 222df1785df..b786a4f23fc 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/ChapterTocFrameTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/ChapterTocFrameTest.java @@ -19,6 +19,7 @@ import android.os.Parcel; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,7 +32,7 @@ public void parcelable() { String[] children = new String[] {"child0", "child1"}; Id3Frame[] subFrames = new Id3Frame[] { - new TextInformationFrame("TIT2", null, "title"), + new TextInformationFrame("TIT2", null, ImmutableList.of("title")), new UrlLinkFrame("WXXX", "description", "url") }; ChapterTocFrame chapterTocFrameToParcel = diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/Id3DecoderTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/Id3DecoderTest.java index 55e81ab93c5..ce884844df8 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/Id3DecoderTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/Id3DecoderTest.java @@ -52,7 +52,7 @@ public void decodeTxxxFrame() { TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.description).isEmpty(); - assertThat(textInformationFrame.value).isEqualTo("mdialog_VINDICO1527664_start"); + assertThat(textInformationFrame.values.get(0)).isEqualTo("mdialog_VINDICO1527664_start"); // Test UTF-16. rawId3 = @@ -67,7 +67,21 @@ public void decodeTxxxFrame() { textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.description).isEqualTo("Hello World"); - assertThat(textInformationFrame.value).isEmpty(); + assertThat(textInformationFrame.values).containsExactly(""); + + // Test multiple values. + rawId3 = + buildSingleFrameTag( + "TXXX", + new byte[] { + 1, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, + 100, 0, 0, 0, 70, 0, 111, 0, 111, 0, 0, 0, 66, 0, 97, 0, 114, 0, 0 + }); + metadata = decoder.decode(rawId3, rawId3.length); + assertThat(metadata.length()).isEqualTo(1); + textInformationFrame = (TextInformationFrame) metadata.get(0); + assertThat(textInformationFrame.description).isEqualTo("Hello World"); + assertThat(textInformationFrame.values).containsExactly("Foo", "Bar").inOrder(); // Test empty. rawId3 = buildSingleFrameTag("TXXX", new byte[0]); @@ -81,7 +95,7 @@ public void decodeTxxxFrame() { textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.description).isEmpty(); - assertThat(textInformationFrame.value).isEmpty(); + assertThat(textInformationFrame.values).containsExactly(""); } @Test @@ -95,7 +109,15 @@ public void decodeTextInformationFrame() { TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TIT2"); assertThat(textInformationFrame.description).isNull(); - assertThat(textInformationFrame.value).isEqualTo("Hello World"); + assertThat(textInformationFrame.values.size()).isEqualTo(1); + assertThat(textInformationFrame.values.get(0)).isEqualTo("Hello World"); + + // Test multiple values. + rawId3 = buildSingleFrameTag("TIT2", new byte[] {3, 70, 111, 111, 0, 66, 97, 114, 0}); + metadata = decoder.decode(rawId3, rawId3.length); + assertThat(metadata.length()).isEqualTo(1); + textInformationFrame = (TextInformationFrame) metadata.get(0); + assertThat(textInformationFrame.values).containsExactly("Foo", "Bar").inOrder(); // Test empty. rawId3 = buildSingleFrameTag("TIT2", new byte[0]); @@ -109,7 +131,7 @@ public void decodeTextInformationFrame() { textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TIT2"); assertThat(textInformationFrame.description).isNull(); - assertThat(textInformationFrame.value).isEmpty(); + assertThat(textInformationFrame.values).containsExactly(""); } @Test diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/TextInformationFrameTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/TextInformationFrameTest.java index bafb57e3cfb..ce9123c308d 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/TextInformationFrameTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/TextInformationFrameTest.java @@ -16,6 +16,7 @@ package androidx.media3.extractor.metadata.id3; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import android.os.Parcel; import androidx.media3.common.MediaMetadata; @@ -32,7 +33,8 @@ public class TextInformationFrameTest { @Test public void parcelable() { - TextInformationFrame textInformationFrameToParcel = new TextInformationFrame("", "", ""); + TextInformationFrame textInformationFrameToParcel = + new TextInformationFrame("", "", ImmutableList.of("")); Parcel parcel = Parcel.obtain(); textInformationFrameToParcel.writeToParcel(parcel, 0); @@ -62,28 +64,42 @@ public void populateMediaMetadata_setsBuilderValues() { List entries = ImmutableList.of( - new TextInformationFrame(/* id= */ "TT2", /* description= */ null, /* value= */ title), - new TextInformationFrame(/* id= */ "TP1", /* description= */ null, /* value= */ artist), new TextInformationFrame( - /* id= */ "TAL", /* description= */ null, /* value= */ albumTitle), + /* id= */ "TT2", /* description= */ null, /* values= */ ImmutableList.of(title)), new TextInformationFrame( - /* id= */ "TP2", /* description= */ null, /* value= */ albumArtist), + /* id= */ "TP1", /* description= */ null, /* values= */ ImmutableList.of(artist)), new TextInformationFrame( - /* id= */ "TRK", /* description= */ null, /* value= */ trackNumberInfo), + /* id= */ "TAL", + /* description= */ null, + /* values= */ ImmutableList.of(albumTitle)), + new TextInformationFrame( + /* id= */ "TP2", + /* description= */ null, + /* values= */ ImmutableList.of(albumArtist)), new TextInformationFrame( - /* id= */ "TYE", /* description= */ null, /* value= */ recordingYear), + /* id= */ "TRK", + /* description= */ null, + /* values= */ ImmutableList.of(trackNumberInfo)), + new TextInformationFrame( + /* id= */ "TYE", + /* description= */ null, + /* values= */ ImmutableList.of(recordingYear)), new TextInformationFrame( /* id= */ "TDA", /* description= */ null, - /* value= */ recordingDay + recordingMonth), + /* values= */ ImmutableList.of(recordingDay + recordingMonth)), new TextInformationFrame( - /* id= */ "TDRL", /* description= */ null, /* value= */ releaseDate), + /* id= */ "TDRL", + /* description= */ null, + /* values= */ ImmutableList.of(releaseDate)), new TextInformationFrame( - /* id= */ "TCM", /* description= */ null, /* value= */ composer), + /* id= */ "TCM", /* description= */ null, /* values= */ ImmutableList.of(composer)), new TextInformationFrame( - /* id= */ "TP3", /* description= */ null, /* value= */ conductor), + /* id= */ "TP3", + /* description= */ null, + /* values= */ ImmutableList.of(conductor)), new TextInformationFrame( - /* id= */ "TXT", /* description= */ null, /* value= */ writer)); + /* id= */ "TXT", /* description= */ null, /* values= */ ImmutableList.of(writer))); MediaMetadata.Builder builder = MediaMetadata.EMPTY.buildUpon(); for (Metadata.Entry entry : entries) { @@ -108,4 +124,41 @@ public void populateMediaMetadata_setsBuilderValues() { assertThat(mediaMetadata.conductor.toString()).isEqualTo(conductor); assertThat(mediaMetadata.writer.toString()).isEqualTo(writer); } + + @Test + public void emptyValuesListThrowsException() { + assertThrows( + IllegalArgumentException.class, + () -> new TextInformationFrame("TXXX", "description", ImmutableList.of())); + } + + @Test + @SuppressWarnings("deprecation") // Testing deprecated field + public void deprecatedValueStillPopulated() { + TextInformationFrame frame = + new TextInformationFrame("TXXX", "description", ImmutableList.of("value")); + + assertThat(frame.value).isEqualTo("value"); + assertThat(frame.values).containsExactly("value"); + } + + @Test + @SuppressWarnings({"deprecation", "InlineMeInliner"}) // Testing deprecated constructor + public void deprecatedConstructorPopulatesValuesList() { + TextInformationFrame frame = new TextInformationFrame("TXXX", "description", "value"); + + assertThat(frame.value).isEqualTo("value"); + assertThat(frame.values).containsExactly("value"); + } + + @Test + @SuppressWarnings({"deprecation", "InlineMeInliner"}) // Testing deprecated constructor + public void deprecatedConstructorCreatesEqualInstance() { + TextInformationFrame frame1 = new TextInformationFrame("TXXX", "description", "value"); + TextInformationFrame frame2 = + new TextInformationFrame("TXXX", "description", ImmutableList.of("value")); + + assertThat(frame1).isEqualTo(frame2); + assertThat(frame1.hashCode()).isEqualTo(frame2.hashCode()); + } } diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.0.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.0.dump index d2a8d6a442b..b6a9c0947dd 100644 --- a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.0.dump @@ -14,7 +14,7 @@ track 0: maxInputSize = 5776 channelCount = 2 sampleRate = 48000 - metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + metadata = entries=[TXXX: description=ID: values=[105519843], TIT2: description=null: values=[那么爱你为什么], TPE1: description=null: values=[阿强], TALB: description=null: values=[华丽的外衣], TXXX: description=ID: values=[105519843], APIC: mimeType=image/jpeg, description=] initializationData: data = length 42, hash 83F6895 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.1.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.1.dump index 250d1add953..725c496cec0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.1.dump @@ -14,7 +14,7 @@ track 0: maxInputSize = 5776 channelCount = 2 sampleRate = 48000 - metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + metadata = entries=[TXXX: description=ID: values=[105519843], TIT2: description=null: values=[那么爱你为什么], TPE1: description=null: values=[阿强], TALB: description=null: values=[华丽的外衣], TXXX: description=ID: values=[105519843], APIC: mimeType=image/jpeg, description=] initializationData: data = length 42, hash 83F6895 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.2.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.2.dump index e5057cff25d..c310e1ffdc8 100644 --- a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.2.dump @@ -14,7 +14,7 @@ track 0: maxInputSize = 5776 channelCount = 2 sampleRate = 48000 - metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + metadata = entries=[TXXX: description=ID: values=[105519843], TIT2: description=null: values=[那么爱你为什么], TPE1: description=null: values=[阿强], TALB: description=null: values=[华丽的外衣], TXXX: description=ID: values=[105519843], APIC: mimeType=image/jpeg, description=] initializationData: data = length 42, hash 83F6895 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.3.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.3.dump index afaead1d88c..1423a1df78f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.3.dump @@ -14,7 +14,7 @@ track 0: maxInputSize = 5776 channelCount = 2 sampleRate = 48000 - metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + metadata = entries=[TXXX: description=ID: values=[105519843], TIT2: description=null: values=[那么爱你为什么], TPE1: description=null: values=[阿强], TALB: description=null: values=[华丽的外衣], TXXX: description=ID: values=[105519843], APIC: mimeType=image/jpeg, description=] initializationData: data = length 42, hash 83F6895 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.unknown_length.dump index d2a8d6a442b..b6a9c0947dd 100644 --- a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_flac.unknown_length.dump @@ -14,7 +14,7 @@ track 0: maxInputSize = 5776 channelCount = 2 sampleRate = 48000 - metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + metadata = entries=[TXXX: description=ID: values=[105519843], TIT2: description=null: values=[那么爱你为什么], TPE1: description=null: values=[阿强], TALB: description=null: values=[华丽的外衣], TXXX: description=ID: values=[105519843], APIC: mimeType=image/jpeg, description=] initializationData: data = length 42, hash 83F6895 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.0.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.0.dump index ca9f1a74a1f..68f3fe89a66 100644 --- a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.0.dump @@ -17,7 +17,7 @@ track 0: channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + metadata = entries=[TXXX: description=ID: values=[105519843], TIT2: description=null: values=[那么爱你为什么], TPE1: description=null: values=[阿强], TALB: description=null: values=[华丽的外衣], TXXX: description=ID: values=[105519843], APIC: mimeType=image/jpeg, description=] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.1.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.1.dump index 36314d9433f..364d99e3cc4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.1.dump @@ -17,7 +17,7 @@ track 0: channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + metadata = entries=[TXXX: description=ID: values=[105519843], TIT2: description=null: values=[那么爱你为什么], TPE1: description=null: values=[阿强], TALB: description=null: values=[华丽的外衣], TXXX: description=ID: values=[105519843], APIC: mimeType=image/jpeg, description=] sample 0: time = 853333 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.2.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.2.dump index 0e8cc733411..ce1cee5dd20 100644 --- a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.2.dump @@ -17,7 +17,7 @@ track 0: channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + metadata = entries=[TXXX: description=ID: values=[105519843], TIT2: description=null: values=[那么爱你为什么], TPE1: description=null: values=[阿强], TALB: description=null: values=[华丽的外衣], TXXX: description=ID: values=[105519843], APIC: mimeType=image/jpeg, description=] sample 0: time = 1792000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.3.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.3.dump index 8ef6f9cb33b..0d23e7d7dbc 100644 --- a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.3.dump @@ -17,7 +17,7 @@ track 0: channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + metadata = entries=[TXXX: description=ID: values=[105519843], TIT2: description=null: values=[那么爱你为什么], TPE1: description=null: values=[阿强], TALB: description=null: values=[华丽的外衣], TXXX: description=ID: values=[105519843], APIC: mimeType=image/jpeg, description=] sample 0: time = 2645333 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.unknown_length.dump index ca9f1a74a1f..68f3fe89a66 100644 --- a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_id3_enabled_raw.unknown_length.dump @@ -17,7 +17,7 @@ track 0: channelCount = 2 sampleRate = 48000 pcmEncoding = 2 - metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=] + metadata = entries=[TXXX: description=ID: values=[105519843], TIT2: description=null: values=[那么爱你为什么], TPE1: description=null: values=[阿强], TALB: description=null: values=[华丽的外衣], TXXX: description=ID: values=[105519843], APIC: mimeType=image/jpeg, description=] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.0.dump index c252057e475..84b9b78db40 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.0.dump @@ -16,7 +16,7 @@ track 0: sampleRate = 48000 encoderDelay = 576 encoderPadding = 576 - metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] + metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.1.dump index 76fcbc0f8ed..3c0e31eb8e0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.1.dump @@ -16,7 +16,7 @@ track 0: sampleRate = 48000 encoderDelay = 576 encoderPadding = 576 - metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] + metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] sample 0: time = 943000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.2.dump index 4f9b29dc55f..1e877253dc4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.2.dump @@ -16,7 +16,7 @@ track 0: sampleRate = 48000 encoderDelay = 576 encoderPadding = 576 - metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] + metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] sample 0: time = 1879000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.3.dump index 220965634f7..702eefce2e9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.3.dump @@ -16,5 +16,5 @@ track 0: sampleRate = 48000 encoderDelay = 576 encoderPadding = 576 - metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] + metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.unknown_length.dump index c252057e475..84b9b78db40 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-id3-enabled.unknown_length.dump @@ -16,7 +16,7 @@ track 0: sampleRate = 48000 encoderDelay = 576 encoderPadding = 576 - metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] + metadata = entries=[TIT2: description=null: values=[Test title], TPE1: description=null: values=[Test Artist], TALB: description=null: values=[Test Album], TXXX: description=Test description: values=[Test user info], COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: values=[Lavf58.29.100], MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.0.dump index 20a69e34a8a..352641de04c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.0.dump @@ -16,7 +16,7 @@ track 0: sampleRate = 48000 encoderDelay = 576 encoderPadding = 576 - metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.1.dump index ef3785beb3a..811fd0aaaa9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.1.dump @@ -16,7 +16,7 @@ track 0: sampleRate = 48000 encoderDelay = 576 encoderPadding = 576 - metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]] sample 0: time = 958041 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.2.dump index 12697f6d126..7e2745f2804 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.2.dump @@ -16,7 +16,7 @@ track 0: sampleRate = 48000 encoderDelay = 576 encoderPadding = 576 - metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]] sample 0: time = 1886772 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.3.dump index 2ab479e6337..11102a27ce0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.3.dump @@ -16,5 +16,5 @@ track 0: sampleRate = 48000 encoderDelay = 576 encoderPadding = 576 - metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]] tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.unknown_length.dump index 20a69e34a8a..352641de04c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp3/bear-vbr-xing-header.mp3.unknown_length.dump @@ -16,7 +16,7 @@ track 0: sampleRate = 48000 encoderDelay = 576 encoderPadding = 576 - metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump index 804961f6904..a2e644c9806 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump @@ -152,7 +152,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump index a9745483647..621c343df17 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump @@ -152,7 +152,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump index 19fd0f36d2c..f5585c7bf69 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump @@ -152,7 +152,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump index 80ca2a76c7e..d94ad775dbd 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump @@ -152,7 +152,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump index 804961f6904..a2e644c9806 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump @@ -152,7 +152,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump index 321bc3a832c..1bcbd8e43f3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump @@ -152,7 +152,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump index 4d8fe681cec..2cb5ff29f51 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump @@ -152,7 +152,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump index a3e5cd60d03..bfe2e5b1b09 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump @@ -152,7 +152,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump index a498d93b5e1..f90a082a27a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump @@ -152,7 +152,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump index 321bc3a832c..1bcbd8e43f3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump @@ -152,7 +152,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.0.dump index f9c15394027..fb6ca4f7d21 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.0.dump @@ -16,7 +16,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]] initializationData: data = length 19, hash 86852AE2 data = length 8, hash 72CBCBF5 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.1.dump index d0c0ab1586b..86625de6e0c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.1.dump @@ -16,7 +16,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]] initializationData: data = length 19, hash 86852AE2 data = length 8, hash 72CBCBF5 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.2.dump index cc688289e29..f63cc0a5004 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.2.dump @@ -16,7 +16,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]] initializationData: data = length 19, hash 86852AE2 data = length 8, hash 72CBCBF5 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.3.dump index 42e14dbd2d6..ec53664fc57 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.3.dump @@ -16,7 +16,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]] initializationData: data = length 19, hash 86852AE2 data = length 8, hash 72CBCBF5 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.unknown_length.dump index f9c15394027..fb6ca4f7d21 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus.mp4.unknown_length.dump @@ -16,7 +16,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[TSSE: description=null: value=Lavf58.29.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100]] initializationData: data = length 19, hash 86852AE2 data = length 8, hash 72CBCBF5 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump index 8c1813ef83c..54ba703ee5e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump @@ -274,7 +274,7 @@ track 1: channelCount = 2 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.76.100]] initializationData: data = length 16, hash CAA21BBF sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump index 5011cfa353e..4ae939fe5ab 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump @@ -274,7 +274,7 @@ track 1: channelCount = 2 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.76.100]] initializationData: data = length 16, hash CAA21BBF sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump index ad7c5fbe40f..ff45deced5f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump @@ -274,7 +274,7 @@ track 1: channelCount = 2 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.76.100]] initializationData: data = length 16, hash CAA21BBF sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump index 9e8fbd75843..4a58490278b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump @@ -274,7 +274,7 @@ track 1: channelCount = 2 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.76.100]] initializationData: data = length 16, hash CAA21BBF sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump index 8c1813ef83c..54ba703ee5e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump @@ -274,7 +274,7 @@ track 1: channelCount = 2 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.76.100]] initializationData: data = length 16, hash CAA21BBF sample 0: diff --git a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump index be627cc4d46..2ee74557d54 100644 --- a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump +++ b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump @@ -7,7 +7,7 @@ format 0: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 format 1: diff --git a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump index 5ec2d5f904a..b08fed34425 100644 --- a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump +++ b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump @@ -7,7 +7,7 @@ format 0: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[TSSE: description=null: value=Lavf56.1.0] + metadata = entries=[TSSE: description=null: values=[Lavf56.1.0]] initializationData: data = length 2, hash 5F7 sample: diff --git a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample_with_increasing_timestamps_320w_240h.mp4.clipped.dump b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample_with_increasing_timestamps_320w_240h.mp4.clipped.dump index 90f6bb0017d..f37630c1cdc 100644 --- a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample_with_increasing_timestamps_320w_240h.mp4.clipped.dump +++ b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample_with_increasing_timestamps_320w_240h.mp4.clipped.dump @@ -8,7 +8,7 @@ format 0: channelCount = 2 sampleRate = 48000 language = en - metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + metadata = entries=[TSSE: description=null: values=[Lavf58.76.100]] initializationData: data = length 2, hash 560 format 1: From 26f5e9b83c01d1f6043bb2939e97cdafda8b0aa6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 28 Nov 2022 11:33:36 +0000 Subject: [PATCH 034/141] Split up `Id3DecoderTest` methods It's clearer if each test method follows the Arrange/Act/Assert pattern PiperOrigin-RevId: 491299379 (cherry picked from commit fc5d17832f90f36eb30ee0058204d110e27adcc9) --- .../metadata/id3/Id3DecoderTest.java | 198 ++++++++++++------ 1 file changed, 139 insertions(+), 59 deletions(-) diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/Id3DecoderTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/Id3DecoderTest.java index ce884844df8..8b9ce528404 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/Id3DecoderTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/metadata/id3/Id3DecoderTest.java @@ -37,8 +37,7 @@ public final class Id3DecoderTest { private static final int ID3_TEXT_ENCODING_UTF_8 = 3; @Test - public void decodeTxxxFrame() { - // Test UTF-8. + public void decodeTxxxFrame_utf8() { byte[] rawId3 = buildSingleFrameTag( "TXXX", @@ -47,52 +46,74 @@ public void decodeTxxxFrame() { 55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0 }); Id3Decoder decoder = new Id3Decoder(); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertThat(metadata.length()).isEqualTo(1); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.description).isEmpty(); assertThat(textInformationFrame.values.get(0)).isEqualTo("mdialog_VINDICO1527664_start"); + } - // Test UTF-16. - rawId3 = + @Test + public void decodeTxxxFrame_utf16() { + byte[] rawId3 = buildSingleFrameTag( "TXXX", new byte[] { 1, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0, 0 }); - metadata = decoder.decode(rawId3, rawId3.length); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertThat(metadata.length()).isEqualTo(1); - textInformationFrame = (TextInformationFrame) metadata.get(0); + TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.description).isEqualTo("Hello World"); assertThat(textInformationFrame.values).containsExactly(""); + } - // Test multiple values. - rawId3 = + @Test + public void decodeTxxxFrame_multipleValues() { + byte[] rawId3 = buildSingleFrameTag( "TXXX", new byte[] { 1, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0, 0, 0, 70, 0, 111, 0, 111, 0, 0, 0, 66, 0, 97, 0, 114, 0, 0 }); - metadata = decoder.decode(rawId3, rawId3.length); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertThat(metadata.length()).isEqualTo(1); - textInformationFrame = (TextInformationFrame) metadata.get(0); + TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.description).isEqualTo("Hello World"); assertThat(textInformationFrame.values).containsExactly("Foo", "Bar").inOrder(); + } + + @Test + public void decodeTxxxFrame_empty() { + byte[] rawId3 = buildSingleFrameTag("TXXX", new byte[0]); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); - // Test empty. - rawId3 = buildSingleFrameTag("TXXX", new byte[0]); - metadata = decoder.decode(rawId3, rawId3.length); assertThat(metadata.length()).isEqualTo(0); + } + + @Test + public void decodeTxxxFrame_encodingByteOnly() { + byte[] rawId3 = buildSingleFrameTag("TXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8}); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); - // Test encoding byte only. - rawId3 = buildSingleFrameTag("TXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8}); - metadata = decoder.decode(rawId3, rawId3.length); assertThat(metadata.length()).isEqualTo(1); - textInformationFrame = (TextInformationFrame) metadata.get(0); + TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TXXX"); assertThat(textInformationFrame.description).isEmpty(); assertThat(textInformationFrame.values).containsExactly(""); @@ -104,31 +125,49 @@ public void decodeTextInformationFrame() { buildSingleFrameTag( "TIT2", new byte[] {3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}); Id3Decoder decoder = new Id3Decoder(); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertThat(metadata.length()).isEqualTo(1); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TIT2"); assertThat(textInformationFrame.description).isNull(); assertThat(textInformationFrame.values.size()).isEqualTo(1); assertThat(textInformationFrame.values.get(0)).isEqualTo("Hello World"); + } + @Test + public void decodeTextInformationFrame_multipleValues() { // Test multiple values. - rawId3 = buildSingleFrameTag("TIT2", new byte[] {3, 70, 111, 111, 0, 66, 97, 114, 0}); - metadata = decoder.decode(rawId3, rawId3.length); + byte[] rawId3 = buildSingleFrameTag("TIT2", new byte[] {3, 70, 111, 111, 0, 66, 97, 114, 0}); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertThat(metadata.length()).isEqualTo(1); - textInformationFrame = (TextInformationFrame) metadata.get(0); + TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.values).containsExactly("Foo", "Bar").inOrder(); + } + + @Test + public void decodeTextInformationFrame_empty() { + byte[] rawId3 = buildSingleFrameTag("TIT2", new byte[0]); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); - // Test empty. - rawId3 = buildSingleFrameTag("TIT2", new byte[0]); - metadata = decoder.decode(rawId3, rawId3.length); assertThat(metadata.length()).isEqualTo(0); + } + + @Test + public void decodeTextInformationFrame_encodingByteOnly() { + byte[] rawId3 = buildSingleFrameTag("TIT2", new byte[] {ID3_TEXT_ENCODING_UTF_8}); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); - // Test encoding byte only. - rawId3 = buildSingleFrameTag("TIT2", new byte[] {ID3_TEXT_ENCODING_UTF_8}); - metadata = decoder.decode(rawId3, rawId3.length); assertThat(metadata.length()).isEqualTo(1); - textInformationFrame = (TextInformationFrame) metadata.get(0); + TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); assertThat(textInformationFrame.id).isEqualTo("TIT2"); assertThat(textInformationFrame.description).isNull(); assertThat(textInformationFrame.values).containsExactly(""); @@ -172,23 +211,35 @@ public void decodeWxxxFrame() { 102 }); Id3Decoder decoder = new Id3Decoder(); + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertThat(metadata.length()).isEqualTo(1); UrlLinkFrame urlLinkFrame = (UrlLinkFrame) metadata.get(0); assertThat(urlLinkFrame.id).isEqualTo("WXXX"); assertThat(urlLinkFrame.description).isEqualTo("test"); assertThat(urlLinkFrame.url).isEqualTo("https://test.com/abc?def"); + } + + @Test + public void decodeWxxxFrame_empty() { + byte[] rawId3 = buildSingleFrameTag("WXXX", new byte[0]); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); - // Test empty. - rawId3 = buildSingleFrameTag("WXXX", new byte[0]); - metadata = decoder.decode(rawId3, rawId3.length); assertThat(metadata.length()).isEqualTo(0); + } + + @Test + public void decodeWxxxFrame_encodingByteOnly() { + byte[] rawId3 = buildSingleFrameTag("WXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8}); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); - // Test encoding byte only. - rawId3 = buildSingleFrameTag("WXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8}); - metadata = decoder.decode(rawId3, rawId3.length); assertThat(metadata.length()).isEqualTo(1); - urlLinkFrame = (UrlLinkFrame) metadata.get(0); + UrlLinkFrame urlLinkFrame = (UrlLinkFrame) metadata.get(0); assertThat(urlLinkFrame.id).isEqualTo("WXXX"); assertThat(urlLinkFrame.description).isEmpty(); assertThat(urlLinkFrame.url).isEmpty(); @@ -210,12 +261,17 @@ public void decodeUrlLinkFrame() { assertThat(urlLinkFrame.id).isEqualTo("WCOM"); assertThat(urlLinkFrame.description).isNull(); assertThat(urlLinkFrame.url).isEqualTo("https://test.com/abc?def"); + } + + @Test + public void decodeUrlLinkFrame_empty() { + byte[] rawId3 = buildSingleFrameTag("WCOM", new byte[0]); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); - // Test empty. - rawId3 = buildSingleFrameTag("WCOM", new byte[0]); - metadata = decoder.decode(rawId3, rawId3.length); assertThat(metadata.length()).isEqualTo(1); - urlLinkFrame = (UrlLinkFrame) metadata.get(0); + UrlLinkFrame urlLinkFrame = (UrlLinkFrame) metadata.get(0); assertThat(urlLinkFrame.id).isEqualTo("WCOM"); assertThat(urlLinkFrame.description).isNull(); assertThat(urlLinkFrame.url).isEmpty(); @@ -230,12 +286,17 @@ public void decodePrivFrame() { PrivFrame privFrame = (PrivFrame) metadata.get(0); assertThat(privFrame.owner).isEqualTo("test"); assertThat(privFrame.privateData).isEqualTo(new byte[] {1, 2, 3, 4}); + } + + @Test + public void decodePrivFrame_empty() { + byte[] rawId3 = buildSingleFrameTag("PRIV", new byte[0]); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); - // Test empty. - rawId3 = buildSingleFrameTag("PRIV", new byte[0]); - metadata = decoder.decode(rawId3, rawId3.length); assertThat(metadata.length()).isEqualTo(1); - privFrame = (PrivFrame) metadata.get(0); + PrivFrame privFrame = (PrivFrame) metadata.get(0); assertThat(privFrame.owner).isEmpty(); assertThat(privFrame.privateData).isEqualTo(new byte[0]); } @@ -258,9 +319,11 @@ public void decodeApicFrame() { assertThat(apicFrame.description).isEqualTo("Hello World"); assertThat(apicFrame.pictureData).hasLength(10); assertThat(apicFrame.pictureData).isEqualTo(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}); + } - // Test with UTF-16 description at even offset. - rawId3 = + @Test + public void decodeApicFrame_utf16DescriptionEvenOffset() { + byte[] rawId3 = buildSingleFrameTag( "APIC", new byte[] { @@ -268,28 +331,34 @@ public void decodeApicFrame() { 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }); - decoder = new Id3Decoder(); - metadata = decoder.decode(rawId3, rawId3.length); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertThat(metadata.length()).isEqualTo(1); - apicFrame = (ApicFrame) metadata.get(0); + ApicFrame apicFrame = (ApicFrame) metadata.get(0); assertThat(apicFrame.mimeType).isEqualTo("image/jpeg"); assertThat(apicFrame.pictureType).isEqualTo(16); assertThat(apicFrame.description).isEqualTo("Hello World"); assertThat(apicFrame.pictureData).hasLength(10); assertThat(apicFrame.pictureData).isEqualTo(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}); + } - // Test with UTF-16 description at odd offset. - rawId3 = + @Test + public void decodeApicFrame_utf16DescriptionOddOffset() { + byte[] rawId3 = buildSingleFrameTag( "APIC", new byte[] { 1, 105, 109, 97, 103, 101, 47, 112, 110, 103, 0, 16, 0, 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 87, 0, 111, 0, 114, 0, 108, 0, 100, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }); - decoder = new Id3Decoder(); - metadata = decoder.decode(rawId3, rawId3.length); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); + assertThat(metadata.length()).isEqualTo(1); - apicFrame = (ApicFrame) metadata.get(0); + ApicFrame apicFrame = (ApicFrame) metadata.get(0); assertThat(apicFrame.mimeType).isEqualTo("image/png"); assertThat(apicFrame.pictureType).isEqualTo(16); assertThat(apicFrame.description).isEqualTo("Hello World"); @@ -332,17 +401,28 @@ public void decodeCommentFrame() { assertThat(commentFrame.language).isEqualTo("eng"); assertThat(commentFrame.description).isEqualTo("description"); assertThat(commentFrame.text).isEqualTo("text"); + } + + @Test + public void decodeCommentFrame_empty() { + byte[] rawId3 = buildSingleFrameTag("COMM", new byte[0]); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); - // Test empty. - rawId3 = buildSingleFrameTag("COMM", new byte[0]); - metadata = decoder.decode(rawId3, rawId3.length); assertThat(metadata.length()).isEqualTo(0); + } + + @Test + public void decodeCommentFrame_languageOnly() { + byte[] rawId3 = + buildSingleFrameTag("COMM", new byte[] {ID3_TEXT_ENCODING_UTF_8, 101, 110, 103}); + Id3Decoder decoder = new Id3Decoder(); + + Metadata metadata = decoder.decode(rawId3, rawId3.length); - // Test language only. - rawId3 = buildSingleFrameTag("COMM", new byte[] {ID3_TEXT_ENCODING_UTF_8, 101, 110, 103}); - metadata = decoder.decode(rawId3, rawId3.length); assertThat(metadata.length()).isEqualTo(1); - commentFrame = (CommentFrame) metadata.get(0); + CommentFrame commentFrame = (CommentFrame) metadata.get(0); assertThat(commentFrame.language).isEqualTo("eng"); assertThat(commentFrame.description).isEmpty(); assertThat(commentFrame.text).isEmpty(); From 289f0cf00b7480d99ffe6db576b6f92b6cea8ed7 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 28 Nov 2022 14:15:03 +0000 Subject: [PATCH 035/141] Remove impossible `UnsupportedEncodingException` from `Id3Decoder` The list of charsets is already hard-coded, and using `Charset` types ensures they will all be present at run-time, hence we will never encounter an 'unsupported' charset. PiperOrigin-RevId: 491324466 (cherry picked from commit 5292e408a6fd000c1a125519e22a7c18460eed59) --- .../extractor/metadata/id3/Id3Decoder.java | 104 ++++++++---------- 1 file changed, 46 insertions(+), 58 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java index 19854db16bf..0a6f8bb3b44 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java @@ -26,9 +26,10 @@ import androidx.media3.extractor.metadata.MetadataInputBuffer; import androidx.media3.extractor.metadata.SimpleMetadataDecoder; import com.google.common.base.Ascii; +import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -436,30 +437,25 @@ private static Id3Frame decodeFrame( + frameSize); } return frame; - } catch (UnsupportedEncodingException e) { - Log.w(TAG, "Unsupported character encoding"); - return null; } finally { id3Data.setPosition(nextFramePosition); } } @Nullable - private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) - throws UnsupportedEncodingException { + private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) { if (frameSize < 1) { // Frame is malformed. return null; } int encoding = id3Data.readUnsignedByte(); - String charset = getCharsetName(encoding); byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); int descriptionEndIndex = indexOfTerminator(data, 0, encoding); - String description = new String(data, 0, descriptionEndIndex, charset); + String description = new String(data, 0, descriptionEndIndex, getCharset(encoding)); ImmutableList values = decodeTextInformationFrameValues( @@ -469,7 +465,7 @@ private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, i @Nullable private static TextInformationFrame decodeTextInformationFrame( - ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException { + ParsableByteArray id3Data, int frameSize, String id) { if (frameSize < 1) { // Frame is malformed. return null; @@ -485,17 +481,17 @@ private static TextInformationFrame decodeTextInformationFrame( } private static ImmutableList decodeTextInformationFrameValues( - byte[] data, final int encoding, final int index) throws UnsupportedEncodingException { + byte[] data, final int encoding, final int index) { if (index >= data.length) { return ImmutableList.of(""); } ImmutableList.Builder values = ImmutableList.builder(); - String charset = getCharsetName(encoding); int valueStartIndex = index; int valueEndIndex = indexOfTerminator(data, valueStartIndex, encoding); while (valueStartIndex < valueEndIndex) { - String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); + String value = + new String(data, valueStartIndex, valueEndIndex - valueStartIndex, getCharset(encoding)); values.add(value); valueStartIndex = valueEndIndex + delimiterLength(encoding); @@ -507,47 +503,44 @@ private static ImmutableList decodeTextInformationFrameValues( } @Nullable - private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) - throws UnsupportedEncodingException { + private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) { if (frameSize < 1) { // Frame is malformed. return null; } int encoding = id3Data.readUnsignedByte(); - String charset = getCharsetName(encoding); byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); int descriptionEndIndex = indexOfTerminator(data, 0, encoding); - String description = new String(data, 0, descriptionEndIndex, charset); + String description = new String(data, 0, descriptionEndIndex, getCharset(encoding)); int urlStartIndex = descriptionEndIndex + delimiterLength(encoding); int urlEndIndex = indexOfZeroByte(data, urlStartIndex); - String url = decodeStringIfValid(data, urlStartIndex, urlEndIndex, "ISO-8859-1"); + String url = decodeStringIfValid(data, urlStartIndex, urlEndIndex, Charsets.ISO_8859_1); return new UrlLinkFrame("WXXX", description, url); } private static UrlLinkFrame decodeUrlLinkFrame( - ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException { + ParsableByteArray id3Data, int frameSize, String id) { byte[] data = new byte[frameSize]; id3Data.readBytes(data, 0, frameSize); int urlEndIndex = indexOfZeroByte(data, 0); - String url = new String(data, 0, urlEndIndex, "ISO-8859-1"); + String url = new String(data, 0, urlEndIndex, Charsets.ISO_8859_1); return new UrlLinkFrame(id, null, url); } - private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize) - throws UnsupportedEncodingException { + private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize) { byte[] data = new byte[frameSize]; id3Data.readBytes(data, 0, frameSize); int ownerEndIndex = indexOfZeroByte(data, 0); - String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1"); + String owner = new String(data, 0, ownerEndIndex, Charsets.ISO_8859_1); int privateDataStartIndex = ownerEndIndex + 1; byte[] privateData = copyOfRangeIfValid(data, privateDataStartIndex, data.length); @@ -555,16 +548,15 @@ private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSiz return new PrivFrame(owner, privateData); } - private static GeobFrame decodeGeobFrame(ParsableByteArray id3Data, int frameSize) - throws UnsupportedEncodingException { + private static GeobFrame decodeGeobFrame(ParsableByteArray id3Data, int frameSize) { int encoding = id3Data.readUnsignedByte(); - String charset = getCharsetName(encoding); + Charset charset = getCharset(encoding); byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); int mimeTypeEndIndex = indexOfZeroByte(data, 0); - String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"); + String mimeType = new String(data, 0, mimeTypeEndIndex, Charsets.ISO_8859_1); int filenameStartIndex = mimeTypeEndIndex + 1; int filenameEndIndex = indexOfTerminator(data, filenameStartIndex, encoding); @@ -582,10 +574,9 @@ private static GeobFrame decodeGeobFrame(ParsableByteArray id3Data, int frameSiz } private static ApicFrame decodeApicFrame( - ParsableByteArray id3Data, int frameSize, int majorVersion) - throws UnsupportedEncodingException { + ParsableByteArray id3Data, int frameSize, int majorVersion) { int encoding = id3Data.readUnsignedByte(); - String charset = getCharsetName(encoding); + Charset charset = getCharset(encoding); byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); @@ -594,13 +585,13 @@ private static ApicFrame decodeApicFrame( int mimeTypeEndIndex; if (majorVersion == 2) { mimeTypeEndIndex = 2; - mimeType = "image/" + Ascii.toLowerCase(new String(data, 0, 3, "ISO-8859-1")); + mimeType = "image/" + Ascii.toLowerCase(new String(data, 0, 3, Charsets.ISO_8859_1)); if ("image/jpg".equals(mimeType)) { mimeType = "image/jpeg"; } } else { mimeTypeEndIndex = indexOfZeroByte(data, 0); - mimeType = Ascii.toLowerCase(new String(data, 0, mimeTypeEndIndex, "ISO-8859-1")); + mimeType = Ascii.toLowerCase(new String(data, 0, mimeTypeEndIndex, Charsets.ISO_8859_1)); if (mimeType.indexOf('/') == -1) { mimeType = "image/" + mimeType; } @@ -621,15 +612,14 @@ private static ApicFrame decodeApicFrame( } @Nullable - private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) - throws UnsupportedEncodingException { + private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) { if (frameSize < 4) { // Frame is malformed. return null; } int encoding = id3Data.readUnsignedByte(); - String charset = getCharsetName(encoding); + Charset charset = getCharset(encoding); byte[] data = new byte[3]; id3Data.readBytes(data, 0, 3); @@ -654,13 +644,15 @@ private static ChapterFrame decodeChapterFrame( int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, - @Nullable FramePredicate framePredicate) - throws UnsupportedEncodingException { + @Nullable FramePredicate framePredicate) { int framePosition = id3Data.getPosition(); int chapterIdEndIndex = indexOfZeroByte(id3Data.getData(), framePosition); String chapterId = new String( - id3Data.getData(), framePosition, chapterIdEndIndex - framePosition, "ISO-8859-1"); + id3Data.getData(), + framePosition, + chapterIdEndIndex - framePosition, + Charsets.ISO_8859_1); id3Data.setPosition(chapterIdEndIndex + 1); int startTime = id3Data.readInt(); @@ -695,13 +687,15 @@ private static ChapterTocFrame decodeChapterTOCFrame( int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, - @Nullable FramePredicate framePredicate) - throws UnsupportedEncodingException { + @Nullable FramePredicate framePredicate) { int framePosition = id3Data.getPosition(); int elementIdEndIndex = indexOfZeroByte(id3Data.getData(), framePosition); String elementId = new String( - id3Data.getData(), framePosition, elementIdEndIndex - framePosition, "ISO-8859-1"); + id3Data.getData(), + framePosition, + elementIdEndIndex - framePosition, + Charsets.ISO_8859_1); id3Data.setPosition(elementIdEndIndex + 1); int ctocFlags = id3Data.readUnsignedByte(); @@ -713,7 +707,8 @@ private static ChapterTocFrame decodeChapterTOCFrame( for (int i = 0; i < childCount; i++) { int startIndex = id3Data.getPosition(); int endIndex = indexOfZeroByte(id3Data.getData(), startIndex); - children[i] = new String(id3Data.getData(), startIndex, endIndex - startIndex, "ISO-8859-1"); + children[i] = + new String(id3Data.getData(), startIndex, endIndex - startIndex, Charsets.ISO_8859_1); id3Data.setPosition(endIndex + 1); } @@ -792,23 +787,18 @@ private static int removeUnsynchronization(ParsableByteArray data, int length) { return length; } - /** - * Maps encoding byte from ID3v2 frame to a Charset. - * - * @param encodingByte The value of encoding byte from ID3v2 frame. - * @return Charset name. - */ - private static String getCharsetName(int encodingByte) { + /** Maps encoding byte from ID3v2 frame to a {@link Charset}. */ + private static Charset getCharset(int encodingByte) { switch (encodingByte) { case ID3_TEXT_ENCODING_UTF_16: - return "UTF-16"; + return Charsets.UTF_16; case ID3_TEXT_ENCODING_UTF_16BE: - return "UTF-16BE"; + return Charsets.UTF_16BE; case ID3_TEXT_ENCODING_UTF_8: - return "UTF-8"; + return Charsets.UTF_8; case ID3_TEXT_ENCODING_ISO_8859_1: default: - return "ISO-8859-1"; + return Charsets.ISO_8859_1; } } @@ -871,21 +861,19 @@ private static byte[] copyOfRangeIfValid(byte[] data, int from, int to) { /** * Returns a string obtained by decoding the specified range of {@code data} using the specified - * {@code charsetName}. An empty string is returned if the range is invalid. + * {@code charset}. An empty string is returned if the range is invalid. * * @param data The array from which to decode the string. * @param from The start of the range. * @param to The end of the range (exclusive). - * @param charsetName The name of the Charset to use. + * @param charset The {@link Charset} to use. * @return The decoded string, or an empty string if the range is invalid. - * @throws UnsupportedEncodingException If the Charset is not supported. */ - private static String decodeStringIfValid(byte[] data, int from, int to, String charsetName) - throws UnsupportedEncodingException { + private static String decodeStringIfValid(byte[] data, int from, int to, Charset charset) { if (to <= from || to > data.length) { return ""; } - return new String(data, from, to - from, charsetName); + return new String(data, from, to - from, charset); } private static final class Id3Header { From 3bf99706dcd89568687521ae1b360390a83481ee Mon Sep 17 00:00:00 2001 From: Rohit Singh Date: Tue, 29 Nov 2022 18:41:54 +0000 Subject: [PATCH 036/141] Merge pull request #10776 from dongvanhung:feature/add_support_clear_download_manager_helpers PiperOrigin-RevId: 491336828 (cherry picked from commit 3581ccde29f0b70b113e38456ff07167267b0ad9) --- RELEASENOTES.md | 2 ++ .../media3/exoplayer/offline/DownloadService.java | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 24a1db83590..22c5a30d277 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ ([#10604](https://github.com/google/ExoPlayer/issues/10604)). * Add `ExoPlayer.Builder.setPlaybackLooper` that sets a pre-existing playback thread for a new ExoPlayer instance. + * Allow download manager helpers to be cleared + ([#10776](https://github.com/google/ExoPlayer/issues/10776)). * Session: * Add helper method to convert platform session token to Media3 `SessionToken` ([#171](https://github.com/androidx/media/issues/171)). diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java index e7fdf2dd468..f2df8effff0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java @@ -574,6 +574,17 @@ public static void startForeground(Context context, ClassCalling this method is normally only required if an app supports downloading content for + * multiple users for which different download directories should be used. + */ + public static void clearDownloadManagerHelpers() { + downloadManagerHelpers.clear(); + } + @Override public void onCreate() { if (channelId != null) { From 1d082ee9a7bf060f8d986b4612208ef8873ef73c Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 29 Nov 2022 10:59:22 +0000 Subject: [PATCH 037/141] Bump cast sdk version and remove workaround for live duration The fix for b/171657375 (internal) has been shipped with 21.1.0 already (see https://developers.google.com/cast/docs/release-notes#august-8,-2022). PiperOrigin-RevId: 491583727 (cherry picked from commit 835d3c89f2099ca66c5b5f7af686eace1ac17eb8) --- RELEASENOTES.md | 2 ++ libraries/cast/build.gradle | 2 +- .../src/main/java/androidx/media3/cast/CastUtils.java | 8 +------- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 22c5a30d277..5eeaf2f8d23 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -19,6 +19,8 @@ * Metadata: * Parse multiple null-separated values from ID3 frames, as permitted by ID3 v2.4. +* Cast extension + * Bump Cast SDK version to 21.2.0. ### 1.0.0-beta03 (2022-11-22) diff --git a/libraries/cast/build.gradle b/libraries/cast/build.gradle index 32dbee1e1e2..87f7e6f20ce 100644 --- a/libraries/cast/build.gradle +++ b/libraries/cast/build.gradle @@ -14,7 +14,7 @@ apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" dependencies { - api 'com.google.android.gms:play-services-cast-framework:21.0.1' + api 'com.google.android.gms:play-services-cast-framework:21.2.0' implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation project(modulePrefix + 'lib-common') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastUtils.java b/libraries/cast/src/main/java/androidx/media3/cast/CastUtils.java index a7a24818436..5e0b045cd86 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastUtils.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastUtils.java @@ -26,10 +26,6 @@ /** Utility methods for Cast integration. */ /* package */ final class CastUtils { - /** The duration returned by {@link MediaInfo#getStreamDuration()} for live streams. */ - // TODO: Remove once [Internal ref: b/171657375] is fixed. - private static final long LIVE_STREAM_DURATION = -1000; - /** * Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if * unknown or not applicable. @@ -42,9 +38,7 @@ public static long getStreamDurationUs(@Nullable MediaInfo mediaInfo) { return C.TIME_UNSET; } long durationMs = mediaInfo.getStreamDuration(); - return durationMs != MediaInfo.UNKNOWN_DURATION && durationMs != LIVE_STREAM_DURATION - ? Util.msToUs(durationMs) - : C.TIME_UNSET; + return durationMs != MediaInfo.UNKNOWN_DURATION ? Util.msToUs(durationMs) : C.TIME_UNSET; } /** From e85e4979115c02b9755f7697e97440c8a1d3b25c Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Nov 2022 14:01:35 +0000 Subject: [PATCH 038/141] Add configuration to support OPUS offload To support OPUS offload, we need to provide a few configuration values that are currently not set due to the lack of devices supporting OPUS offload. PiperOrigin-RevId: 491613716 (cherry picked from commit 568fa1e1fa479fd1659abf1d83d71e01227ab9cf) --- .../main/java/androidx/media3/common/C.java | 9 +- .../androidx/media3/common/MimeTypes.java | 2 + .../exoplayer/audio/DefaultAudioSink.java | 3 + .../DefaultAudioTrackBufferSizeProvider.java | 3 + .../androidx/media3/extractor/OpusUtil.java | 59 ++ .../media3/extractor/ogg/OpusReader.java | 38 +- .../media3/extractor/OpusUtilTest.java | 575 +++++++++++++++++- 7 files changed, 648 insertions(+), 41 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/C.java b/libraries/common/src/main/java/androidx/media3/common/C.java index 968253e4b33..ac44ebb6036 100644 --- a/libraries/common/src/main/java/androidx/media3/common/C.java +++ b/libraries/common/src/main/java/androidx/media3/common/C.java @@ -196,7 +196,7 @@ private C() {} * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, - * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. + * {@link #ENCODING_DTS_HD}, {@link #ENCODING_DOLBY_TRUEHD} or {@link #ENCODING_OPUS}. */ @UnstableApi @Documented @@ -224,7 +224,8 @@ private C() {} ENCODING_AC4, ENCODING_DTS, ENCODING_DTS_HD, - ENCODING_DOLBY_TRUEHD + ENCODING_DOLBY_TRUEHD, + ENCODING_OPUS, }) public @interface Encoding {} @@ -325,6 +326,10 @@ private C() {} * @see AudioFormat#ENCODING_DOLBY_TRUEHD */ @UnstableApi public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD; + /** + * @see AudioFormat#ENCODING_OPUS + */ + @UnstableApi public static final int ENCODING_OPUS = AudioFormat.ENCODING_OPUS; /** Represents the behavior affecting whether spatialization will be used. */ @Documented diff --git a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java index 859773b2a66..1bcab6a3eef 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java +++ b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java @@ -587,6 +587,8 @@ public static String getMimeTypeFromMp4ObjectType(int objectType) { return C.ENCODING_DTS_HD; case MimeTypes.AUDIO_TRUEHD: return C.ENCODING_DOLBY_TRUEHD; + case MimeTypes.AUDIO_OPUS: + return C.ENCODING_OPUS; default: return C.ENCODING_INVALID; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 605f5f0d443..4adcffdaf81 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -60,6 +60,7 @@ 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.InlineMe; import com.google.errorprone.annotations.InlineMeValidationDisabled; @@ -1787,6 +1788,8 @@ private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffe ? 0 : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); + case C.ENCODING_OPUS: + return OpusUtil.parsePacketAudioSampleCount(buffer); case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_16BIT_BIG_ENDIAN: case C.ENCODING_PCM_24BIT: diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java index 62c72a57227..317f06d05c1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java @@ -32,6 +32,7 @@ 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; /** Provide the buffer size to use when creating an {@link AudioTrack}. */ @@ -255,6 +256,8 @@ protected static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encodin 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: diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java index 81a1adedd14..a1ecff461e6 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/OpusUtil.java @@ -29,6 +29,9 @@ public class OpusUtil { /** Opus streams are always 48000 Hz. */ public static final int SAMPLE_RATE = 48_000; + /** Maximum achievable Opus bitrate. */ + public static final int MAX_BYTES_PER_SECOND = 510 * 1000 / 8; // See RFC 6716. Section 2.1.1 + private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840; private static final int FULL_CODEC_INITIALIZATION_DATA_BUFFER_COUNT = 3; @@ -63,6 +66,62 @@ public static List buildInitializationData(byte[] header) { return initializationData; } + /** + * Returns the number of audio samples in the given audio packet. + * + *

    The buffer's position is not modified. + * + * @param buffer The audio packet. + * @return Returns the number of audio samples in the packet. + */ + public static int parsePacketAudioSampleCount(ByteBuffer buffer) { + long packetDurationUs = + getPacketDurationUs(buffer.get(0), buffer.limit() > 1 ? buffer.get(1) : 0); + return (int) (packetDurationUs * SAMPLE_RATE / C.MICROS_PER_SECOND); + } + + /** + * Returns the duration of the given audio packet. + * + * @param buffer The audio packet. + * @return Returns the duration of the given audio packet, in microseconds. + */ + public static long getPacketDurationUs(byte[] buffer) { + return getPacketDurationUs(buffer[0], buffer.length > 1 ? buffer[1] : 0); + } + + private static long getPacketDurationUs(byte packetByte0, byte packetByte1) { + // See RFC6716, Sections 3.1 and 3.2. + int toc = packetByte0 & 0xFF; + int frames; + switch (toc & 0x3) { + case 0: + frames = 1; + break; + case 1: + case 2: + frames = 2; + break; + default: + frames = packetByte1 & 0x3F; + break; + } + + int config = toc >> 3; + int length = config & 0x3; + int frameDurationUs; + if (config >= 16) { + frameDurationUs = 2500 << length; + } else if (config >= 12) { + frameDurationUs = 10000 << (length & 0x1); + } else if (length == 3) { + frameDurationUs = 60000; + } else { + frameDurationUs = 10000 << length; + } + return (long) frames * frameDurationUs; + } + private static int getPreSkipSamples(byte[] header) { return ((header[11] & 0xFF) << 8) | (header[10] & 0xFF); } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ogg/OpusReader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ogg/OpusReader.java index 95996f6a804..00e12a7b56f 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ogg/OpusReader.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ogg/OpusReader.java @@ -54,7 +54,7 @@ protected void reset(boolean headerData) { @Override protected long preparePayload(ParsableByteArray packet) { - return convertTimeToGranule(getPacketDurationUs(packet.getData())); + return convertTimeToGranule(OpusUtil.getPacketDurationUs(packet.getData())); } @Override @@ -121,42 +121,6 @@ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData } } - /** - * Returns the duration of the given audio packet. - * - * @param packet Contains audio data. - * @return Returns the duration of the given audio packet. - */ - private long getPacketDurationUs(byte[] packet) { - int toc = packet[0] & 0xFF; - int frames; - switch (toc & 0x3) { - case 0: - frames = 1; - break; - case 1: - case 2: - frames = 2; - break; - default: - frames = packet[1] & 0x3F; - break; - } - - int config = toc >> 3; - int length = config & 0x3; - if (config >= 16) { - length = 2500 << length; - } else if (config >= 12) { - length = 10000 << (length & 0x1); - } else if (length == 3) { - length = 60000; - } else { - length = 10000 << length; - } - return (long) frames * length; - } - /** * Returns true if the given {@link ParsableByteArray} starts with {@code expectedPrefix}. Does * not change the {@link ParsableByteArray#getPosition() position} of {@code packet}. diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/OpusUtilTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/OpusUtilTest.java index 1bc6b3431c3..48c74327c27 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/OpusUtilTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/OpusUtilTest.java @@ -15,6 +15,7 @@ */ package androidx.media3.extractor; +import static androidx.media3.common.util.Util.getBytesFromHexString; import static com.google.common.truth.Truth.assertThat; import androidx.media3.common.C; @@ -41,8 +42,9 @@ public final class OpusUtilTest { buildNativeOrderByteArray(sampleCountToNanoseconds(DEFAULT_SEEK_PRE_ROLL_SAMPLES)); @Test - public void buildInitializationData() { + public void buildInitializationData_returnsExpectedHeaderWithPreSkipAndPreRoll() { List initializationData = OpusUtil.buildInitializationData(HEADER); + assertThat(initializationData).hasSize(3); assertThat(initializationData.get(0)).isEqualTo(HEADER); assertThat(initializationData.get(1)).isEqualTo(HEADER_PRE_SKIP_BYTES); @@ -50,11 +52,576 @@ public void buildInitializationData() { } @Test - public void getChannelCount() { + public void getChannelCount_returnsChannelCount() { int channelCount = OpusUtil.getChannelCount(HEADER); + assertThat(channelCount).isEqualTo(2); } + @Test + public void getPacketDurationUs_code0_returnsExpectedDuration() { + long config0DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("04")); + long config1DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("0C")); + long config2DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("14")); + long config3DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("1C")); + long config4DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("24")); + long config5DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("2C")); + long config6DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("34")); + long config7DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("3C")); + long config8DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("44")); + long config9DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("4C")); + long config10DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("54")); + long config11DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("5C")); + long config12DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("64")); + long config13DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("6C")); + long config14DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("74")); + long config15DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("7C")); + long config16DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("84")); + long config17DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("8C")); + long config18DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("94")); + long config19DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("9C")); + long config20DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("A4")); + long config21DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("AC")); + long config22DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("B4")); + long config23DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("BC")); + long config24DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("C4")); + long config25DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("CC")); + long config26DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("D4")); + long config27DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("DC")); + long config28DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("E4")); + long config29DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("EC")); + long config30DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("F4")); + long config31DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("FC")); + + assertThat(config0DurationUs).isEqualTo(10_000); + assertThat(config1DurationUs).isEqualTo(20_000); + assertThat(config2DurationUs).isEqualTo(40_000); + assertThat(config3DurationUs).isEqualTo(60_000); + assertThat(config4DurationUs).isEqualTo(10_000); + assertThat(config5DurationUs).isEqualTo(20_000); + assertThat(config6DurationUs).isEqualTo(40_000); + assertThat(config7DurationUs).isEqualTo(60_000); + assertThat(config8DurationUs).isEqualTo(10_000); + assertThat(config9DurationUs).isEqualTo(20_000); + assertThat(config10DurationUs).isEqualTo(40_000); + assertThat(config11DurationUs).isEqualTo(60_000); + assertThat(config12DurationUs).isEqualTo(10_000); + assertThat(config13DurationUs).isEqualTo(20_000); + assertThat(config14DurationUs).isEqualTo(10_000); + assertThat(config15DurationUs).isEqualTo(20_000); + assertThat(config16DurationUs).isEqualTo(2_500); + assertThat(config17DurationUs).isEqualTo(5_000); + assertThat(config18DurationUs).isEqualTo(10_000); + assertThat(config19DurationUs).isEqualTo(20_000); + assertThat(config20DurationUs).isEqualTo(2_500); + assertThat(config21DurationUs).isEqualTo(5_000); + assertThat(config22DurationUs).isEqualTo(10_000); + assertThat(config23DurationUs).isEqualTo(20_000); + assertThat(config24DurationUs).isEqualTo(2_500); + assertThat(config25DurationUs).isEqualTo(5_000); + assertThat(config26DurationUs).isEqualTo(10_000); + assertThat(config27DurationUs).isEqualTo(20_000); + assertThat(config28DurationUs).isEqualTo(2_500); + assertThat(config29DurationUs).isEqualTo(5_000); + assertThat(config30DurationUs).isEqualTo(10_000); + assertThat(config31DurationUs).isEqualTo(20_000); + } + + @Test + public void getPacketDurationUs_code1_returnsExpectedDuration() { + long config0DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("05")); + long config1DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("0D")); + long config2DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("15")); + long config3DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("1D")); + long config4DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("25")); + long config5DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("2D")); + long config6DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("35")); + long config7DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("3D")); + long config8DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("45")); + long config9DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("4D")); + long config10DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("55")); + long config11DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("5D")); + long config12DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("65")); + long config13DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("6D")); + long config14DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("75")); + long config15DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("7D")); + long config16DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("85")); + long config17DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("8D")); + long config18DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("95")); + long config19DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("9D")); + long config20DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("A5")); + long config21DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("AD")); + long config22DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("B5")); + long config23DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("BD")); + long config24DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("C5")); + long config25DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("CD")); + long config26DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("D5")); + long config27DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("DD")); + long config28DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("E5")); + long config29DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("ED")); + long config30DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("F5")); + long config31DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("FD")); + + assertThat(config0DurationUs).isEqualTo(20_000); + assertThat(config1DurationUs).isEqualTo(40_000); + assertThat(config2DurationUs).isEqualTo(80_000); + assertThat(config3DurationUs).isEqualTo(120_000); + assertThat(config4DurationUs).isEqualTo(20_000); + assertThat(config5DurationUs).isEqualTo(40_000); + assertThat(config6DurationUs).isEqualTo(80_000); + assertThat(config7DurationUs).isEqualTo(120_000); + assertThat(config8DurationUs).isEqualTo(20_000); + assertThat(config9DurationUs).isEqualTo(40_000); + assertThat(config10DurationUs).isEqualTo(80_000); + assertThat(config11DurationUs).isEqualTo(120_000); + assertThat(config12DurationUs).isEqualTo(20_000); + assertThat(config13DurationUs).isEqualTo(40_000); + assertThat(config14DurationUs).isEqualTo(20_000); + assertThat(config15DurationUs).isEqualTo(40_000); + assertThat(config16DurationUs).isEqualTo(5_000); + assertThat(config17DurationUs).isEqualTo(10_000); + assertThat(config18DurationUs).isEqualTo(20_000); + assertThat(config19DurationUs).isEqualTo(40_000); + assertThat(config20DurationUs).isEqualTo(5_000); + assertThat(config21DurationUs).isEqualTo(10_000); + assertThat(config22DurationUs).isEqualTo(20_000); + assertThat(config23DurationUs).isEqualTo(40_000); + assertThat(config24DurationUs).isEqualTo(5_000); + assertThat(config25DurationUs).isEqualTo(10_000); + assertThat(config26DurationUs).isEqualTo(20_000); + assertThat(config27DurationUs).isEqualTo(40_000); + assertThat(config28DurationUs).isEqualTo(5_000); + assertThat(config29DurationUs).isEqualTo(10_000); + assertThat(config30DurationUs).isEqualTo(20_000); + assertThat(config31DurationUs).isEqualTo(40_000); + } + + @Test + public void getPacketDurationUs_code2_returnsExpectedDuration() { + long config0DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("06")); + long config1DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("0E")); + long config2DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("16")); + long config3DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("1E")); + long config4DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("26")); + long config5DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("2E")); + long config6DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("36")); + long config7DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("3E")); + long config8DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("46")); + long config9DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("4E")); + long config10DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("56")); + long config11DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("5E")); + long config12DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("66")); + long config13DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("6E")); + long config14DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("76")); + long config15DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("7E")); + long config16DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("86")); + long config17DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("8E")); + long config18DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("96")); + long config19DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("9E")); + long config20DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("A6")); + long config21DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("AE")); + long config22DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("B6")); + long config23DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("BE")); + long config24DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("C6")); + long config25DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("CE")); + long config26DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("D6")); + long config27DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("DE")); + long config28DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("E6")); + long config29DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("EE")); + long config30DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("F6")); + long config31DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("FE")); + + assertThat(config0DurationUs).isEqualTo(20_000); + assertThat(config1DurationUs).isEqualTo(40_000); + assertThat(config2DurationUs).isEqualTo(80_000); + assertThat(config3DurationUs).isEqualTo(120_000); + assertThat(config4DurationUs).isEqualTo(20_000); + assertThat(config5DurationUs).isEqualTo(40_000); + assertThat(config6DurationUs).isEqualTo(80_000); + assertThat(config7DurationUs).isEqualTo(120_000); + assertThat(config8DurationUs).isEqualTo(20_000); + assertThat(config9DurationUs).isEqualTo(40_000); + assertThat(config10DurationUs).isEqualTo(80_000); + assertThat(config11DurationUs).isEqualTo(120_000); + assertThat(config12DurationUs).isEqualTo(20_000); + assertThat(config13DurationUs).isEqualTo(40_000); + assertThat(config14DurationUs).isEqualTo(20_000); + assertThat(config15DurationUs).isEqualTo(40_000); + assertThat(config16DurationUs).isEqualTo(5_000); + assertThat(config17DurationUs).isEqualTo(10_000); + assertThat(config18DurationUs).isEqualTo(20_000); + assertThat(config19DurationUs).isEqualTo(40_000); + assertThat(config20DurationUs).isEqualTo(5_000); + assertThat(config21DurationUs).isEqualTo(10_000); + assertThat(config22DurationUs).isEqualTo(20_000); + assertThat(config23DurationUs).isEqualTo(40_000); + assertThat(config24DurationUs).isEqualTo(5_000); + assertThat(config25DurationUs).isEqualTo(10_000); + assertThat(config26DurationUs).isEqualTo(20_000); + assertThat(config27DurationUs).isEqualTo(40_000); + assertThat(config28DurationUs).isEqualTo(5_000); + assertThat(config29DurationUs).isEqualTo(10_000); + assertThat(config30DurationUs).isEqualTo(20_000); + assertThat(config31DurationUs).isEqualTo(40_000); + } + + @Test + public void getPacketDurationUs_code3_returnsExpectedDuration() { + // max possible frame count to reach 120ms duration + long config0DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("078C")); + long config1DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("0F86")); + long config2DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("1783")); + long config3DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("1F82")); + // frame count of 2 + long config4DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("2782")); + long config5DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("2F82")); + long config6DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("3782")); + long config7DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("3F82")); + long config8DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("4782")); + long config9DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("4F82")); + long config10DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("5782")); + long config11DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("5F82")); + // max possible frame count to reach 120ms duration + long config12DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("678C")); + long config13DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("6F86")); + // frame count of 2 + long config14DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("7782")); + long config15DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("7F82")); + // max possible frame count to reach 120ms duration + long config16DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("87B0")); + long config17DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("8F98")); + long config18DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("978C")); + long config19DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("9F86")); + // frame count of 2 + long config20DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("A782")); + long config21DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("AF82")); + long config22DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("B782")); + long config23DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("BF82")); + long config24DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("C782")); + long config25DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("CF82")); + long config26DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("D782")); + long config27DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("DF82")); + long config28DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("E782")); + long config29DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("EF82")); + long config30DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("F782")); + long config31DurationUs = OpusUtil.getPacketDurationUs(getBytesFromHexString("FF82")); + + assertThat(config0DurationUs).isEqualTo(120_000); + assertThat(config1DurationUs).isEqualTo(120_000); + assertThat(config2DurationUs).isEqualTo(120_000); + assertThat(config3DurationUs).isEqualTo(120_000); + assertThat(config4DurationUs).isEqualTo(20_000); + assertThat(config5DurationUs).isEqualTo(40_000); + assertThat(config6DurationUs).isEqualTo(80_000); + assertThat(config7DurationUs).isEqualTo(120_000); + assertThat(config8DurationUs).isEqualTo(20_000); + assertThat(config9DurationUs).isEqualTo(40_000); + assertThat(config10DurationUs).isEqualTo(80_000); + assertThat(config11DurationUs).isEqualTo(120_000); + assertThat(config12DurationUs).isEqualTo(120_000); + assertThat(config13DurationUs).isEqualTo(120_000); + assertThat(config14DurationUs).isEqualTo(20_000); + assertThat(config15DurationUs).isEqualTo(40_000); + assertThat(config16DurationUs).isEqualTo(120_000); + assertThat(config17DurationUs).isEqualTo(120_000); + assertThat(config18DurationUs).isEqualTo(120_000); + assertThat(config19DurationUs).isEqualTo(120_000); + assertThat(config20DurationUs).isEqualTo(5_000); + assertThat(config21DurationUs).isEqualTo(10_000); + assertThat(config22DurationUs).isEqualTo(20_000); + assertThat(config23DurationUs).isEqualTo(40_000); + assertThat(config24DurationUs).isEqualTo(5_000); + assertThat(config25DurationUs).isEqualTo(10_000); + assertThat(config26DurationUs).isEqualTo(20_000); + assertThat(config27DurationUs).isEqualTo(40_000); + assertThat(config28DurationUs).isEqualTo(5_000); + assertThat(config29DurationUs).isEqualTo(10_000); + assertThat(config30DurationUs).isEqualTo(20_000); + assertThat(config31DurationUs).isEqualTo(40_000); + } + + @Test + public void getPacketAudioSampleCount_code0_returnsExpectedDuration() { + int config0SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("04")); + int config1SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("0C")); + int config2SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("14")); + int config3SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("1C")); + int config4SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("24")); + int config5SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("2C")); + int config6SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("34")); + int config7SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("3C")); + int config8SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("44")); + int config9SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("4C")); + int config10SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("54")); + int config11SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("5C")); + int config12SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("64")); + int config13SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("6C")); + int config14SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("74")); + int config15SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("7C")); + int config16SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("84")); + int config17SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("8C")); + int config18SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("94")); + int config19SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("9C")); + int config20SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("A4")); + int config21SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("AC")); + int config22SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("B4")); + int config23SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("BC")); + int config24SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("C4")); + int config25SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("CC")); + int config26SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("D4")); + int config27SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("DC")); + int config28SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("E4")); + int config29SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("EC")); + int config30SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("F4")); + int config31SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("FC")); + + assertThat(config0SampleCount).isEqualTo(480); + assertThat(config1SampleCount).isEqualTo(960); + assertThat(config2SampleCount).isEqualTo(1920); + assertThat(config3SampleCount).isEqualTo(2880); + assertThat(config4SampleCount).isEqualTo(480); + assertThat(config5SampleCount).isEqualTo(960); + assertThat(config6SampleCount).isEqualTo(1920); + assertThat(config7SampleCount).isEqualTo(2880); + assertThat(config8SampleCount).isEqualTo(480); + assertThat(config9SampleCount).isEqualTo(960); + assertThat(config10SampleCount).isEqualTo(1920); + assertThat(config11SampleCount).isEqualTo(2880); + assertThat(config12SampleCount).isEqualTo(480); + assertThat(config13SampleCount).isEqualTo(960); + assertThat(config14SampleCount).isEqualTo(480); + assertThat(config15SampleCount).isEqualTo(960); + assertThat(config16SampleCount).isEqualTo(120); + assertThat(config17SampleCount).isEqualTo(240); + assertThat(config18SampleCount).isEqualTo(480); + assertThat(config19SampleCount).isEqualTo(960); + assertThat(config20SampleCount).isEqualTo(120); + assertThat(config21SampleCount).isEqualTo(240); + assertThat(config22SampleCount).isEqualTo(480); + assertThat(config23SampleCount).isEqualTo(960); + assertThat(config24SampleCount).isEqualTo(120); + assertThat(config25SampleCount).isEqualTo(240); + assertThat(config26SampleCount).isEqualTo(480); + assertThat(config27SampleCount).isEqualTo(960); + assertThat(config28SampleCount).isEqualTo(120); + assertThat(config29SampleCount).isEqualTo(240); + assertThat(config30SampleCount).isEqualTo(480); + assertThat(config31SampleCount).isEqualTo(960); + } + + @Test + public void getPacketAudioSampleCount_code1_returnsExpectedDuration() { + int config0SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("05")); + int config1SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("0D")); + int config2SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("15")); + int config3SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("1D")); + int config4SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("25")); + int config5SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("2D")); + int config6SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("35")); + int config7SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("3D")); + int config8SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("45")); + int config9SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("4D")); + int config10SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("55")); + int config11SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("5D")); + int config12SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("65")); + int config13SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("6D")); + int config14SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("75")); + int config15SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("7D")); + int config16SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("85")); + int config17SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("8D")); + int config18SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("95")); + int config19SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("9D")); + int config20SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("A5")); + int config21SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("AD")); + int config22SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("B5")); + int config23SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("BD")); + int config24SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("C5")); + int config25SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("CD")); + int config26SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("D5")); + int config27SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("DD")); + int config28SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("E5")); + int config29SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("ED")); + int config30SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("F5")); + int config31SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("FD")); + + assertThat(config0SampleCount).isEqualTo(960); + assertThat(config1SampleCount).isEqualTo(1920); + assertThat(config2SampleCount).isEqualTo(3840); + assertThat(config3SampleCount).isEqualTo(5760); + assertThat(config4SampleCount).isEqualTo(960); + assertThat(config5SampleCount).isEqualTo(1920); + assertThat(config6SampleCount).isEqualTo(3840); + assertThat(config7SampleCount).isEqualTo(5760); + assertThat(config8SampleCount).isEqualTo(960); + assertThat(config9SampleCount).isEqualTo(1920); + assertThat(config10SampleCount).isEqualTo(3840); + assertThat(config11SampleCount).isEqualTo(5760); + assertThat(config12SampleCount).isEqualTo(960); + assertThat(config13SampleCount).isEqualTo(1920); + assertThat(config14SampleCount).isEqualTo(960); + assertThat(config15SampleCount).isEqualTo(1920); + assertThat(config16SampleCount).isEqualTo(240); + assertThat(config17SampleCount).isEqualTo(480); + assertThat(config18SampleCount).isEqualTo(960); + assertThat(config19SampleCount).isEqualTo(1920); + assertThat(config20SampleCount).isEqualTo(240); + assertThat(config21SampleCount).isEqualTo(480); + assertThat(config22SampleCount).isEqualTo(960); + assertThat(config23SampleCount).isEqualTo(1920); + assertThat(config24SampleCount).isEqualTo(240); + assertThat(config25SampleCount).isEqualTo(480); + assertThat(config26SampleCount).isEqualTo(960); + assertThat(config27SampleCount).isEqualTo(1920); + assertThat(config28SampleCount).isEqualTo(240); + assertThat(config29SampleCount).isEqualTo(480); + assertThat(config30SampleCount).isEqualTo(960); + assertThat(config31SampleCount).isEqualTo(1920); + } + + @Test + public void getPacketAudioSampleCount_code2_returnsExpectedDuration() { + int config0SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("06")); + int config1SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("0E")); + int config2SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("16")); + int config3SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("1E")); + int config4SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("26")); + int config5SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("2E")); + int config6SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("36")); + int config7SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("3E")); + int config8SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("46")); + int config9SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("4E")); + int config10SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("56")); + int config11SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("5E")); + int config12SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("66")); + int config13SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("6E")); + int config14SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("76")); + int config15SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("7E")); + int config16SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("86")); + int config17SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("8E")); + int config18SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("96")); + int config19SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("9E")); + int config20SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("A6")); + int config21SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("AE")); + int config22SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("B6")); + int config23SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("BE")); + int config24SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("C6")); + int config25SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("CE")); + int config26SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("D6")); + int config27SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("DE")); + int config28SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("E6")); + int config29SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("EE")); + int config30SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("F6")); + int config31SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("FE")); + + assertThat(config0SampleCount).isEqualTo(960); + assertThat(config1SampleCount).isEqualTo(1920); + assertThat(config2SampleCount).isEqualTo(3840); + assertThat(config3SampleCount).isEqualTo(5760); + assertThat(config4SampleCount).isEqualTo(960); + assertThat(config5SampleCount).isEqualTo(1920); + assertThat(config6SampleCount).isEqualTo(3840); + assertThat(config7SampleCount).isEqualTo(5760); + assertThat(config8SampleCount).isEqualTo(960); + assertThat(config9SampleCount).isEqualTo(1920); + assertThat(config10SampleCount).isEqualTo(3840); + assertThat(config11SampleCount).isEqualTo(5760); + assertThat(config12SampleCount).isEqualTo(960); + assertThat(config13SampleCount).isEqualTo(1920); + assertThat(config14SampleCount).isEqualTo(960); + assertThat(config15SampleCount).isEqualTo(1920); + assertThat(config16SampleCount).isEqualTo(240); + assertThat(config17SampleCount).isEqualTo(480); + assertThat(config18SampleCount).isEqualTo(960); + assertThat(config19SampleCount).isEqualTo(1920); + assertThat(config20SampleCount).isEqualTo(240); + assertThat(config21SampleCount).isEqualTo(480); + assertThat(config22SampleCount).isEqualTo(960); + assertThat(config23SampleCount).isEqualTo(1920); + assertThat(config24SampleCount).isEqualTo(240); + assertThat(config25SampleCount).isEqualTo(480); + assertThat(config26SampleCount).isEqualTo(960); + assertThat(config27SampleCount).isEqualTo(1920); + assertThat(config28SampleCount).isEqualTo(240); + assertThat(config29SampleCount).isEqualTo(480); + assertThat(config30SampleCount).isEqualTo(960); + assertThat(config31SampleCount).isEqualTo(1920); + } + + @Test + public void getPacketAudioSampleCount_code3_returnsExpectedDuration() { + // max possible frame count to reach 120ms duration + int config0SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("078C")); + int config1SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("0F86")); + int config2SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("1783")); + int config3SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("1F82")); + // frame count of 2 + int config4SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("2782")); + int config5SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("2F82")); + int config6SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("3782")); + int config7SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("3F82")); + int config8SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("4782")); + int config9SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("4F82")); + int config10SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("5782")); + int config11SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("5F82")); + // max possible frame count to reach 120ms duration + int config12SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("678C")); + int config13SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("6F86")); + // frame count of 2 + int config14SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("7782")); + int config15SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("7F82")); + // max possible frame count to reach 120ms duration + int config16SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("87B0")); + int config17SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("8F98")); + int config18SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("978C")); + int config19SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("9F86")); + // frame count of 2 + int config20SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("A782")); + int config21SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("AF82")); + int config22SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("B782")); + int config23SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("BF82")); + int config24SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("C782")); + int config25SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("CF82")); + int config26SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("D782")); + int config27SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("DF82")); + int config28SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("E782")); + int config29SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("EF82")); + int config30SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("F782")); + int config31SampleCount = OpusUtil.parsePacketAudioSampleCount(getByteBuffer("FF82")); + + assertThat(config0SampleCount).isEqualTo(5760); + assertThat(config1SampleCount).isEqualTo(5760); + assertThat(config2SampleCount).isEqualTo(5760); + assertThat(config3SampleCount).isEqualTo(5760); + assertThat(config4SampleCount).isEqualTo(960); + assertThat(config5SampleCount).isEqualTo(1920); + assertThat(config6SampleCount).isEqualTo(3840); + assertThat(config7SampleCount).isEqualTo(5760); + assertThat(config8SampleCount).isEqualTo(960); + assertThat(config9SampleCount).isEqualTo(1920); + assertThat(config10SampleCount).isEqualTo(3840); + assertThat(config11SampleCount).isEqualTo(5760); + assertThat(config12SampleCount).isEqualTo(5760); + assertThat(config13SampleCount).isEqualTo(5760); + assertThat(config14SampleCount).isEqualTo(960); + assertThat(config15SampleCount).isEqualTo(1920); + assertThat(config16SampleCount).isEqualTo(5760); + assertThat(config17SampleCount).isEqualTo(5760); + assertThat(config18SampleCount).isEqualTo(5760); + assertThat(config19SampleCount).isEqualTo(5760); + assertThat(config20SampleCount).isEqualTo(240); + assertThat(config21SampleCount).isEqualTo(480); + assertThat(config22SampleCount).isEqualTo(960); + assertThat(config23SampleCount).isEqualTo(1920); + assertThat(config24SampleCount).isEqualTo(240); + assertThat(config25SampleCount).isEqualTo(480); + assertThat(config26SampleCount).isEqualTo(960); + assertThat(config27SampleCount).isEqualTo(1920); + assertThat(config28SampleCount).isEqualTo(240); + assertThat(config29SampleCount).isEqualTo(480); + assertThat(config30SampleCount).isEqualTo(960); + assertThat(config31SampleCount).isEqualTo(1920); + } + private static long sampleCountToNanoseconds(long sampleCount) { return (sampleCount * C.NANOS_PER_SECOND) / OpusUtil.SAMPLE_RATE; } @@ -62,4 +629,8 @@ private static long sampleCountToNanoseconds(long sampleCount) { private static byte[] buildNativeOrderByteArray(long value) { return ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(value).array(); } + + private static ByteBuffer getByteBuffer(String hexString) { + return ByteBuffer.wrap(getBytesFromHexString(hexString)); + } } From bb7e6324d8d8682dc5928324cc37d4f033f75566 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 29 Nov 2022 15:18:11 +0000 Subject: [PATCH 039/141] Use audio bitrate to calculate AudioTrack min buffer in passthrough Use the bitrate of the audio format (when available) in DefaultAudioSink.AudioTrackBufferSizeProvider.getBufferSizeInBytes() to calculate accurate buffer sizes for direct (passthrough) playbacks. #minor-release PiperOrigin-RevId: 491628530 (cherry picked from commit d12afe0596b11c473b242d6389bc7c538a988238) --- RELEASENOTES.md | 3 ++ .../exoplayer/audio/DefaultAudioSink.java | 4 ++ .../DefaultAudioTrackBufferSizeProvider.java | 23 +++++++--- ...ltAudioTrackBufferSizeProviderAC3Test.java | 21 ++++++++- ...dioTrackBufferSizeProviderEncodedTest.java | 43 +++++++++++++++++++ ...ltAudioTrackBufferSizeProviderPcmTest.java | 9 ++++ 6 files changed, 96 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5eeaf2f8d23..887eea6c08c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,9 @@ playback thread for a new ExoPlayer instance. * Allow download manager helpers to be cleared ([#10776](https://github.com/google/ExoPlayer/issues/10776)). +* Audio: + * Use the compressed audio format bitrate to calculate the min buffer size + for `AudioTrack` in direct playbacks (passthrough). * Session: * Add helper method to convert platform session token to Media3 `SessionToken` ([#171](https://github.com/androidx/media/issues/171)). diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 4adcffdaf81..e94d6d24169 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -204,6 +204,8 @@ public interface AudioTrackBufferSizeProvider { * @param pcmFrameSize The size of the PCM frames if the {@code encoding} is PCM, 1 otherwise, * in bytes. * @param sampleRate The sample rate of the format, in Hz. + * @param bitrate The bitrate of the audio stream if the stream is compressed, or {@link + * Format#NO_VALUE} if {@code encoding} is PCM or the bitrate is not known. * @param maxAudioTrackPlaybackSpeed The maximum speed the content will be played using {@link * AudioTrack#setPlaybackParams}. 0.5 is 2x slow motion, 1 is real time, 2 is 2x fast * forward, etc. This will be {@code 1} unless {@link @@ -218,6 +220,7 @@ int getBufferSizeInBytes( @OutputMode int outputMode, int pcmFrameSize, int sampleRate, + int bitrate, double maxAudioTrackPlaybackSpeed); } @@ -791,6 +794,7 @@ public void configure(Format inputFormat, int specifiedBufferSize, @Nullable int outputMode, outputPcmFrameSize != C.LENGTH_UNSET ? outputPcmFrameSize : 1, outputSampleRate, + inputFormat.bitrate, enableAudioTrackPlaybackParams ? MAX_PLAYBACK_SPEED : DEFAULT_PLAYBACK_SPEED); offloadDisabledUntilNextConfiguration = false; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java index 317f06d05c1..ef40d2c4c15 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java @@ -19,6 +19,7 @@ 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_PCM; +import static com.google.common.math.IntMath.divide; import static com.google.common.primitives.Ints.checkedCast; import static java.lang.Math.max; @@ -34,6 +35,7 @@ import androidx.media3.extractor.MpegAudioUtil; import androidx.media3.extractor.OpusUtil; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.math.RoundingMode; /** Provide the buffer size to use when creating an {@link AudioTrack}. */ @UnstableApi @@ -174,10 +176,11 @@ public int getBufferSizeInBytes( @OutputMode int outputMode, int pcmFrameSize, int sampleRate, + int bitrate, double maxAudioTrackPlaybackSpeed) { int bufferSize = get1xBufferSizeInBytes( - minBufferSizeInBytes, encoding, outputMode, pcmFrameSize, sampleRate); + minBufferSizeInBytes, encoding, outputMode, pcmFrameSize, sampleRate, bitrate); // Maintain the buffer duration by scaling the size accordingly. bufferSize = (int) (bufferSize * maxAudioTrackPlaybackSpeed); // Buffer size must not be lower than the AudioTrack min buffer size for this format. @@ -188,12 +191,17 @@ public int getBufferSizeInBytes( /** Returns the buffer size for playback at 1x speed. */ protected int get1xBufferSizeInBytes( - int minBufferSizeInBytes, int encoding, int outputMode, int pcmFrameSize, int sampleRate) { + int minBufferSizeInBytes, + int encoding, + int outputMode, + int pcmFrameSize, + int sampleRate, + int bitrate) { switch (outputMode) { case OUTPUT_MODE_PCM: return getPcmBufferSizeInBytes(minBufferSizeInBytes, sampleRate, pcmFrameSize); case OUTPUT_MODE_PASSTHROUGH: - return getPassthroughBufferSizeInBytes(encoding); + return getPassthroughBufferSizeInBytes(encoding, bitrate); case OUTPUT_MODE_OFFLOAD: return getOffloadBufferSizeInBytes(encoding); default: @@ -210,13 +218,16 @@ protected int getPcmBufferSizeInBytes(int minBufferSizeInBytes, int samplingRate } /** Returns the buffer size for passthrough playback. */ - protected int getPassthroughBufferSizeInBytes(@C.Encoding int encoding) { + protected int getPassthroughBufferSizeInBytes(@C.Encoding int encoding, int bitrate) { int bufferSizeUs = passthroughBufferDurationUs; if (encoding == C.ENCODING_AC3) { bufferSizeUs *= ac3BufferMultiplicationFactor; } - int maxByteRate = getMaximumEncodedRateBytesPerSecond(encoding); - return checkedCast((long) bufferSizeUs * maxByteRate / C.MICROS_PER_SECOND); + int byteRate = + bitrate != Format.NO_VALUE + ? divide(bitrate, 8, RoundingMode.CEILING) + : getMaximumEncodedRateBytesPerSecond(encoding); + return checkedCast((long) bufferSizeUs * byteRate / C.MICROS_PER_SECOND); } /** Returns the buffer size for offload playback. */ diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderAC3Test.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderAC3Test.java index 7f6f41314fb..fae6430f8bb 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderAC3Test.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderAC3Test.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.media3.common.C; +import androidx.media3.common.Format; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,7 +35,7 @@ public class DefaultAudioTrackBufferSizeProviderAC3Test { @Test public void - getBufferSizeInBytes_passthroughAC3_isPassthroughBufferSizeTimesMultiplicationFactor() { + getBufferSizeInBytes_passthroughAc3AndNoBitrate_assumesMaxByteRateTimesMultiplicationFactor() { int bufferSize = DEFAULT.getBufferSizeInBytes( /* minBufferSizeInBytes= */ 0, @@ -42,6 +43,7 @@ public class DefaultAudioTrackBufferSizeProviderAC3Test { /* outputMode= */ OUTPUT_MODE_PASSTHROUGH, /* pcmFrameSize= */ 1, /* sampleRate= */ 0, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize) @@ -50,6 +52,23 @@ public class DefaultAudioTrackBufferSizeProviderAC3Test { * DEFAULT.ac3BufferMultiplicationFactor); } + @Test + public void + getBufferSizeInBytes_passthroughAC3At256Kbits_isPassthroughBufferSizeTimesMultiplicationFactor() { + int bufferSize = + DEFAULT.getBufferSizeInBytes( + /* minBufferSizeInBytes= */ 0, + /* encoding= */ C.ENCODING_AC3, + /* outputMode= */ OUTPUT_MODE_PASSTHROUGH, + /* pcmFrameSize= */ 1, + /* sampleRate= */ 0, + /* bitrate= */ 256_000, + /* maxAudioTrackPlaybackSpeed= */ 1); + + // Default buffer duration 0.25s => 0.25 * 256000 / 8 = 8000 + assertThat(bufferSize).isEqualTo(8000 * DEFAULT.ac3BufferMultiplicationFactor); + } + private static int durationUsToAc3MaxBytes(long durationUs) { return (int) (durationUs * getMaximumEncodedRateBytesPerSecond(C.ENCODING_AC3) / MICROS_PER_SECOND); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderEncodedTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderEncodedTest.java index 638dbf5661c..0d2723bb542 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderEncodedTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderEncodedTest.java @@ -15,10 +15,13 @@ */ package androidx.media3.exoplayer.audio; +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.DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond; import static com.google.common.truth.Truth.assertThat; import androidx.media3.common.C; +import androidx.media3.common.Format; import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,6 +46,8 @@ public static ImmutableList data() { C.ENCODING_MP3, C.ENCODING_AAC_LC, C.ENCODING_AAC_HE_V1, + C.ENCODING_E_AC3, + C.ENCODING_E_AC3_JOC, C.ENCODING_AC4, C.ENCODING_DTS, C.ENCODING_DOLBY_TRUEHD); @@ -57,8 +62,46 @@ public void getBufferSizeInBytes_veryBigMinBufferSize_isMinBufferSize() { /* outputMode= */ OUTPUT_MODE_PASSTHROUGH, /* pcmFrameSize= */ 1, /* sampleRate= */ 0, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 0); assertThat(bufferSize).isEqualTo(123456789); } + + @Test + public void + getBufferSizeInBytes_passThroughAndBitrateNotSet_returnsBufferSizeWithAssumedBitrate() { + int bufferSize = + DEFAULT.getBufferSizeInBytes( + /* minBufferSizeInBytes= */ 0, + /* encoding= */ encoding, + /* outputMode= */ OUTPUT_MODE_PASSTHROUGH, + /* pcmFrameSize= */ 1, + /* sampleRate= */ 0, + /* bitrate= */ Format.NO_VALUE, + /* maxAudioTrackPlaybackSpeed= */ 1); + + assertThat(bufferSize) + .isEqualTo(durationUsToMaxBytes(encoding, DEFAULT.passthroughBufferDurationUs)); + } + + @Test + public void getBufferSizeInBytes_passthroughAndBitrateDefined() { + int bufferSize = + DEFAULT.getBufferSizeInBytes( + /* minBufferSizeInBytes= */ 0, + /* encoding= */ encoding, + /* outputMode= */ OUTPUT_MODE_PASSTHROUGH, + /* pcmFrameSize= */ 1, + /* sampleRate= */ 0, + /* bitrate= */ 256_000, + /* maxAudioTrackPlaybackSpeed= */ 1); + + // Default buffer duration is 250ms => 0.25 * 256000 / 8 = 8000 + assertThat(bufferSize).isEqualTo(8000); + } + + private static int durationUsToMaxBytes(@C.Encoding int encoding, long durationUs) { + return (int) (durationUs * getMaximumEncodedRateBytesPerSecond(encoding) / MICROS_PER_SECOND); + } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderPcmTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderPcmTest.java index 0b922a9c3eb..d27999ed659 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderPcmTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderPcmTest.java @@ -21,6 +21,7 @@ import static java.lang.Math.ceil; import androidx.media3.common.C; +import androidx.media3.common.Format; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -89,6 +90,7 @@ public void getBufferSizeInBytes_veryBigMinBufferSize_isMinBufferSize() { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize).isEqualTo(roundUpToFrame(1234567890)); @@ -103,6 +105,7 @@ public void getBufferSizeInBytes_noMinBufferSize_isMinBufferDuration() { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize) @@ -121,6 +124,7 @@ public void getBufferSizeInBytes_tooSmallMinBufferSize_isMinBufferDuration() { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize) @@ -139,6 +143,7 @@ public void getBufferSizeInBytes_lowMinBufferSize_multipliesAudioTrackMinBuffer( /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize) @@ -157,6 +162,7 @@ public void getBufferSizeInBytes_highMinBufferSize_multipliesAudioTrackMinBuffer /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize) @@ -175,6 +181,7 @@ public void getBufferSizeInBytes_tooHighMinBufferSize_isMaxBufferDuration() { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize) @@ -190,6 +197,7 @@ public void getBufferSizeInBytes_lowPlaybackSpeed_isScaledByPlaybackSpeed() { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1 / 5F); assertThat(bufferSize) @@ -205,6 +213,7 @@ public void getBufferSizeInBytes_highPlaybackSpeed_isScaledByPlaybackSpeed() { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 8F); int expected = roundUpToFrame(durationUsToBytes(DEFAULT.minPcmBufferDurationUs) * 8); From 4ecbd774428d95750f8c0a84d15a13f596eeaf72 Mon Sep 17 00:00:00 2001 From: rohks Date: Tue, 29 Nov 2022 18:18:13 +0000 Subject: [PATCH 040/141] Add public constructors to `DefaultMediaNotificationProvider` Issue: androidx/media#213 Without a public constructor, it is not possible to extend this class and override its method. PiperOrigin-RevId: 491673111 (cherry picked from commit f3e450e7833bbc62237c1f24f9a1f6c4eed21460) --- .../DefaultMediaNotificationProvider.java | 38 ++++++++-- .../DefaultMediaNotificationProviderTest.java | 74 +++++++++++++++++++ 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index 45de1f5a943..5cdc2630333 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -249,11 +249,31 @@ public interface NotificationIdProvider { private @MonotonicNonNull OnBitmapLoadedFutureCallback pendingOnBitmapLoadedFutureCallback; @DrawableRes private int smallIconResourceId; - private DefaultMediaNotificationProvider(Builder builder) { - this.context = builder.context; - this.notificationIdProvider = builder.notificationIdProvider; - this.channelId = builder.channelId; - this.channelNameResourceId = builder.channelNameResourceId; + /** + * Creates an instance. Use this constructor only when you want to override methods of this class. + * Otherwise use {@link Builder}. + */ + public DefaultMediaNotificationProvider(Context context) { + this( + context, + session -> DEFAULT_NOTIFICATION_ID, + DEFAULT_CHANNEL_ID, + DEFAULT_CHANNEL_NAME_RESOURCE_ID); + } + + /** + * Creates an instance. Use this constructor only when you want to override methods of this class. + * Otherwise use {@link Builder}. + */ + public DefaultMediaNotificationProvider( + Context context, + NotificationIdProvider notificationIdProvider, + String channelId, + int channelNameResourceId) { + this.context = context; + this.notificationIdProvider = notificationIdProvider; + this.channelId = channelId; + this.channelNameResourceId = channelNameResourceId; notificationManager = checkStateNotNull( (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); @@ -261,6 +281,14 @@ private DefaultMediaNotificationProvider(Builder builder) { smallIconResourceId = R.drawable.media3_notification_small_icon; } + private DefaultMediaNotificationProvider(Builder builder) { + this( + builder.context, + builder.notificationIdProvider, + builder.channelId, + builder.channelNameResourceId); + } + // MediaNotification.Provider implementation @Override diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java index bf23e9c894f..fe7616bce3d 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java @@ -628,6 +628,80 @@ public void setMediaMetadataArtist_notificationUsesItAsContentText() { assertThat(isMediaMetadataArtistEqualToNotificationContentText).isTrue(); } + /** + * {@link DefaultMediaNotificationProvider} is designed to be extendable. Public constructor + * should not be removed. + */ + @Test + public void createsProviderUsingConstructor_idsNotSpecified_usesDefaultIds() { + Context context = ApplicationProvider.getApplicationContext(); + DefaultMediaNotificationProvider defaultMediaNotificationProvider = + new DefaultMediaNotificationProvider(context); + MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY); + BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); + when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); + when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); + DefaultActionFactory defaultActionFactory = + new DefaultActionFactory(Robolectric.setupService(TestService.class)); + + MediaNotification notification = + defaultMediaNotificationProvider.createNotification( + mockMediaSession, + /* customLayout= */ ImmutableList.of(), + defaultActionFactory, + /* onNotificationChangedCallback= */ mock(MediaNotification.Provider.Callback.class)); + + assertThat(notification.notificationId).isEqualTo(DEFAULT_NOTIFICATION_ID); + assertThat(notification.notification.getChannelId()).isEqualTo(DEFAULT_CHANNEL_ID); + ShadowNotificationManager shadowNotificationManager = + Shadows.shadowOf( + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); + assertHasNotificationChannel( + shadowNotificationManager.getNotificationChannels(), + /* channelId= */ DEFAULT_CHANNEL_ID, + /* channelName= */ context.getString(R.string.default_notification_channel_name)); + } + + /** + * Extends {@link DefaultMediaNotificationProvider} and overrides all known protected methods. If + * by accident we change the signature of the class in a way that affects inheritance, this test + * would no longer compile. + */ + @Test + public void overridesProviderDefinition_compilesSuccessfully() { + Context context = ApplicationProvider.getApplicationContext(); + + DefaultMediaNotificationProvider unused = + new DefaultMediaNotificationProvider(context) { + @Override + public List getMediaButtons( + Player.Commands playerCommands, + List customLayout, + boolean showPauseButton) { + return super.getMediaButtons(playerCommands, customLayout, showPauseButton); + } + + @Override + public int[] addNotificationActions( + MediaSession mediaSession, + List mediaButtons, + NotificationCompat.Builder builder, + MediaNotification.ActionFactory actionFactory) { + return super.addNotificationActions(mediaSession, mediaButtons, builder, actionFactory); + } + + @Override + public CharSequence getNotificationContentTitle(MediaMetadata metadata) { + return super.getNotificationContentTitle(metadata); + } + + @Override + public CharSequence getNotificationContentText(MediaMetadata metadata) { + return super.getNotificationContentText(metadata); + } + }; + } + private static void assertHasNotificationChannel( List notificationChannels, String channelId, String channelName) { boolean found = false; From 665f04d70a574e9cb5b6d92d23165dde51b25afc Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 30 Nov 2022 12:13:28 +0000 Subject: [PATCH 041/141] Use the artist as the subtitle of the legacy media description The Bluetooth AVRCP service expects the metadata of the item currently being played to be in sync with the corresponding media description in the active item of the queue. The comparison expects the metadata values of `METADATA_KEY_TITLE` and `METADATA_KEY_ARTIST` [1] to be equal to the `title` and `subtitle` field of the `MediaDescription` [2] of the corresponding queue item. Hence we need to populate the media description accordingly to avoid the BT service to delay the update for two seconds and log an exception. [1] https://cs.android.com/android/platform/superproject/+/master:packages/modules/Bluetooth/android/app/src/com/android/bluetooth/audio_util/helpers/Metadata.java;l=120 [2] https://cs.android.com/android/platform/superproject/+/master:packages/modules/Bluetooth/android/app/src/com/android/bluetooth/audio_util/MediaPlayerWrapper.java;l=258 Issue: androidx/media#148 PiperOrigin-RevId: 491877806 (cherry picked from commit 2a07a0b44582782b09a96b5819e9899308e79545) --- .../src/main/java/androidx/media3/session/MediaUtils.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index f58882351df..7caf715ea51 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -351,7 +351,9 @@ public static MediaDescriptionCompat convertToMediaDescriptionCompat( } return builder .setTitle(metadata.title) - .setSubtitle(metadata.subtitle) + // The BT AVRPC service expects the subtitle of the media description to be the artist + // (see https://github.com/androidx/media/issues/148). + .setSubtitle(metadata.artist != null ? metadata.artist : metadata.subtitle) .setDescription(metadata.description) .setIconUri(metadata.artworkUri) .setMediaUri(item.requestMetadata.mediaUri) From 05f640ec7dc36234f5ac6dd322edcfa824a8cb51 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 30 Nov 2022 12:48:11 +0000 Subject: [PATCH 042/141] Rename SimpleBasePlayer.PlaylistItem to MediaItemData This better matches the terminology we use elsewhere in the Player interface, where items inside the playlist are referred to as "media item" and only the entire list is called "playlist". PiperOrigin-RevId: 491882849 (cherry picked from commit ff7fe222b83c55c93cc9ee1a3763a11473168ece) --- .../media3/common/SimpleBasePlayer.java | 312 +++++++++--------- .../media3/common/SimpleBasePlayerTest.java | 247 +++++++------- 2 files changed, 281 insertions(+), 278 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index 350a23920c1..acb6b2c3975 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -122,7 +122,7 @@ public static final class Builder { private Size surfaceSize; private boolean newlyRenderedFirstFrame; private Metadata timedMetadata; - private ImmutableList playlistItems; + private ImmutableList playlist; private Timeline timeline; private MediaMetadata playlistMetadata; private int currentMediaItemIndex; @@ -168,7 +168,7 @@ public Builder() { surfaceSize = Size.UNKNOWN; newlyRenderedFirstFrame = false; timedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET); - playlistItems = ImmutableList.of(); + playlist = ImmutableList.of(); timeline = Timeline.EMPTY; playlistMetadata = MediaMetadata.EMPTY; currentMediaItemIndex = 0; @@ -214,7 +214,7 @@ private Builder(State state) { this.surfaceSize = state.surfaceSize; this.newlyRenderedFirstFrame = state.newlyRenderedFirstFrame; this.timedMetadata = state.timedMetadata; - this.playlistItems = state.playlistItems; + this.playlist = state.playlist; this.timeline = state.timeline; this.playlistMetadata = state.playlistMetadata; this.currentMediaItemIndex = state.currentMediaItemIndex; @@ -565,21 +565,21 @@ public Builder setTimedMetadata(Metadata timedMetadata) { } /** - * Sets the playlist items. + * Sets the list of {@link MediaItemData media items} in the playlist. * - *

    All playlist items must have unique {@linkplain PlaylistItem.Builder#setUid UIDs}. + *

    All items must have unique {@linkplain MediaItemData.Builder#setUid UIDs}. * - * @param playlistItems The list of playlist items. + * @param playlist The list of {@link MediaItemData media items} in the playlist. * @return This builder. */ @CanIgnoreReturnValue - public Builder setPlaylist(List playlistItems) { + public Builder setPlaylist(List playlist) { HashSet uids = new HashSet<>(); - for (int i = 0; i < playlistItems.size(); i++) { - checkArgument(uids.add(playlistItems.get(i).uid)); + for (int i = 0; i < playlist.size(); i++) { + checkArgument(uids.add(playlist.get(i).uid)); } - this.playlistItems = ImmutableList.copyOf(playlistItems); - this.timeline = new PlaylistTimeline(this.playlistItems); + this.playlist = ImmutableList.copyOf(playlist); + this.timeline = new PlaylistTimeline(this.playlist); return this; } @@ -598,8 +598,8 @@ public Builder setPlaylistMetadata(MediaMetadata playlistMetadata) { /** * Sets the current media item index. * - *

    The media item index must be less than the number of {@linkplain #setPlaylist playlist - * items}, if set. + *

    The media item index must be less than the number of {@linkplain #setPlaylist media + * items in the playlist}, if set. * * @param currentMediaItemIndex The current media item index. * @return This builder. @@ -612,15 +612,15 @@ public Builder setCurrentMediaItemIndex(int currentMediaItemIndex) { /** * Sets the current period index, or {@link C#INDEX_UNSET} to assume the first period of the - * current playlist item is played. + * current media item is played. * *

    The period index must be less than the total number of {@linkplain - * PlaylistItem.Builder#setPeriods periods} in the playlist, if set, and the period at the - * specified index must be part of the {@linkplain #setCurrentMediaItemIndex current playlist + * MediaItemData.Builder#setPeriods periods} in the media item, if set, and the period at the + * specified index must be part of the {@linkplain #setCurrentMediaItemIndex current media * item}. * * @param currentPeriodIndex The current period index, or {@link C#INDEX_UNSET} to assume the - * first period of the current playlist item is played. + * first period of the current media item is played. * @return This builder. */ @CanIgnoreReturnValue @@ -637,7 +637,7 @@ public Builder setCurrentPeriodIndex(int currentPeriodIndex) { * C#INDEX_UNSET}. * *

    Ads indices can only be set if there is a corresponding {@link AdPlaybackState} defined - * in the current {@linkplain PlaylistItem.Builder#setPeriods period}. + * in the current {@linkplain MediaItemData.Builder#setPeriods period}. * * @param adGroupIndex The current ad group index, or {@link C#INDEX_UNSET} if no ad is * playing. @@ -863,9 +863,9 @@ public State build() { public final boolean newlyRenderedFirstFrame; /** The most recent timed metadata. */ public final Metadata timedMetadata; - /** The playlist items. */ - public final ImmutableList playlistItems; - /** The {@link Timeline} derived from the {@linkplain #playlistItems playlist items}. */ + /** The media items in the playlist. */ + public final ImmutableList playlist; + /** The {@link Timeline} derived from the {@link #playlist}. */ public final Timeline timeline; /** The playlist {@link MediaMetadata}. */ public final MediaMetadata playlistMetadata; @@ -873,7 +873,7 @@ public State build() { public final int currentMediaItemIndex; /** * The current period index, or {@link C#INDEX_UNSET} to assume the first period of the current - * playlist item is played. + * media item is played. */ public final int currentPeriodIndex; /** The current ad group index, or {@link C#INDEX_UNSET} if no ad is playing. */ @@ -999,7 +999,7 @@ private State(Builder builder) { this.surfaceSize = builder.surfaceSize; this.newlyRenderedFirstFrame = builder.newlyRenderedFirstFrame; this.timedMetadata = builder.timedMetadata; - this.playlistItems = builder.playlistItems; + this.playlist = builder.playlist; this.timeline = builder.timeline; this.playlistMetadata = builder.playlistMetadata; this.currentMediaItemIndex = builder.currentMediaItemIndex; @@ -1056,7 +1056,7 @@ public boolean equals(@Nullable Object o) { && surfaceSize.equals(state.surfaceSize) && newlyRenderedFirstFrame == state.newlyRenderedFirstFrame && timedMetadata.equals(state.timedMetadata) - && playlistItems.equals(state.playlistItems) + && playlist.equals(state.playlist) && playlistMetadata.equals(state.playlistMetadata) && currentMediaItemIndex == state.currentMediaItemIndex && currentPeriodIndex == state.currentPeriodIndex @@ -1102,7 +1102,7 @@ public int hashCode() { result = 31 * result + surfaceSize.hashCode(); result = 31 * result + (newlyRenderedFirstFrame ? 1 : 0); result = 31 * result + timedMetadata.hashCode(); - result = 31 * result + playlistItems.hashCode(); + result = 31 * result + playlist.hashCode(); result = 31 * result + playlistMetadata.hashCode(); result = 31 * result + currentMediaItemIndex; result = 31 * result + currentPeriodIndex; @@ -1122,28 +1122,28 @@ public int hashCode() { private static final class PlaylistTimeline extends Timeline { - private final ImmutableList playlistItems; + private final ImmutableList playlist; private final int[] firstPeriodIndexByWindowIndex; private final int[] windowIndexByPeriodIndex; private final HashMap periodIndexByUid; - public PlaylistTimeline(ImmutableList playlistItems) { - int playlistItemCount = playlistItems.size(); - this.playlistItems = playlistItems; - this.firstPeriodIndexByWindowIndex = new int[playlistItemCount]; + public PlaylistTimeline(ImmutableList playlist) { + int mediaItemCount = playlist.size(); + this.playlist = playlist; + this.firstPeriodIndexByWindowIndex = new int[mediaItemCount]; int periodCount = 0; - for (int i = 0; i < playlistItemCount; i++) { - PlaylistItem playlistItem = playlistItems.get(i); + for (int i = 0; i < mediaItemCount; i++) { + MediaItemData mediaItemData = playlist.get(i); firstPeriodIndexByWindowIndex[i] = periodCount; - periodCount += getPeriodCountInPlaylistItem(playlistItem); + periodCount += getPeriodCountInMediaItem(mediaItemData); } this.windowIndexByPeriodIndex = new int[periodCount]; this.periodIndexByUid = new HashMap<>(); int periodIndex = 0; - for (int i = 0; i < playlistItemCount; i++) { - PlaylistItem playlistItem = playlistItems.get(i); - for (int j = 0; j < getPeriodCountInPlaylistItem(playlistItem); j++) { - periodIndexByUid.put(playlistItem.getPeriodUid(j), periodIndex); + for (int i = 0; i < mediaItemCount; i++) { + MediaItemData mediaItemData = playlist.get(i); + for (int j = 0; j < getPeriodCountInMediaItem(mediaItemData); j++) { + periodIndexByUid.put(mediaItemData.getPeriodUid(j), periodIndex); windowIndexByPeriodIndex[periodIndex] = i; periodIndex++; } @@ -1152,7 +1152,7 @@ public PlaylistTimeline(ImmutableList playlistItems) { @Override public int getWindowCount() { - return playlistItems.size(); + return playlist.size(); } @Override @@ -1181,7 +1181,7 @@ public int getFirstWindowIndex(boolean shuffleModeEnabled) { @Override public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { - return playlistItems + return playlist .get(windowIndex) .getWindow(firstPeriodIndexByWindowIndex[windowIndex], window); } @@ -1201,7 +1201,7 @@ public Period getPeriodByUid(Object periodUid, Period period) { public Period getPeriod(int periodIndex, Period period, boolean setIds) { int windowIndex = windowIndexByPeriodIndex[periodIndex]; int periodIndexInWindow = periodIndex - firstPeriodIndexByWindowIndex[windowIndex]; - return playlistItems.get(windowIndex).getPeriod(windowIndex, periodIndexInWindow, period); + return playlist.get(windowIndex).getPeriod(windowIndex, periodIndexInWindow, period); } @Override @@ -1214,21 +1214,22 @@ public int getIndexOfPeriod(Object uid) { public Object getUidOfPeriod(int periodIndex) { int windowIndex = windowIndexByPeriodIndex[periodIndex]; int periodIndexInWindow = periodIndex - firstPeriodIndexByWindowIndex[windowIndex]; - return playlistItems.get(windowIndex).getPeriodUid(periodIndexInWindow); + return playlist.get(windowIndex).getPeriodUid(periodIndexInWindow); } - private static int getPeriodCountInPlaylistItem(PlaylistItem playlistItem) { - return playlistItem.periods.isEmpty() ? 1 : playlistItem.periods.size(); + private static int getPeriodCountInMediaItem(MediaItemData mediaItemData) { + return mediaItemData.periods.isEmpty() ? 1 : mediaItemData.periods.size(); } } /** - * An immutable description of a playlist item, containing both static setup information like - * {@link MediaItem} and dynamic data that is generally read from the media like the duration. + * An immutable description of an item in the playlist, containing both static setup information + * like {@link MediaItem} and dynamic data that is generally read from the media like the + * duration. */ - protected static final class PlaylistItem { + protected static final class MediaItemData { - /** A builder for {@link PlaylistItem} objects. */ + /** A builder for {@link MediaItemData} objects. */ public static final class Builder { private Object uid; @@ -1251,7 +1252,7 @@ public static final class Builder { /** * Creates the builder. * - * @param uid The unique identifier of the playlist item within a playlist. This value will be + * @param uid The unique identifier of the media item within a playlist. This value will be * set as {@link Timeline.Window#uid} for this item. */ public Builder(Object uid) { @@ -1273,31 +1274,31 @@ public Builder(Object uid) { periods = ImmutableList.of(); } - private Builder(PlaylistItem playlistItem) { - this.uid = playlistItem.uid; - this.tracks = playlistItem.tracks; - this.mediaItem = playlistItem.mediaItem; - this.mediaMetadata = playlistItem.mediaMetadata; - this.manifest = playlistItem.manifest; - this.liveConfiguration = playlistItem.liveConfiguration; - this.presentationStartTimeMs = playlistItem.presentationStartTimeMs; - this.windowStartTimeMs = playlistItem.windowStartTimeMs; - this.elapsedRealtimeEpochOffsetMs = playlistItem.elapsedRealtimeEpochOffsetMs; - this.isSeekable = playlistItem.isSeekable; - this.isDynamic = playlistItem.isDynamic; - this.defaultPositionUs = playlistItem.defaultPositionUs; - this.durationUs = playlistItem.durationUs; - this.positionInFirstPeriodUs = playlistItem.positionInFirstPeriodUs; - this.isPlaceholder = playlistItem.isPlaceholder; - this.periods = playlistItem.periods; + private Builder(MediaItemData mediaItemData) { + this.uid = mediaItemData.uid; + this.tracks = mediaItemData.tracks; + this.mediaItem = mediaItemData.mediaItem; + this.mediaMetadata = mediaItemData.mediaMetadata; + this.manifest = mediaItemData.manifest; + this.liveConfiguration = mediaItemData.liveConfiguration; + this.presentationStartTimeMs = mediaItemData.presentationStartTimeMs; + this.windowStartTimeMs = mediaItemData.windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = mediaItemData.elapsedRealtimeEpochOffsetMs; + this.isSeekable = mediaItemData.isSeekable; + this.isDynamic = mediaItemData.isDynamic; + this.defaultPositionUs = mediaItemData.defaultPositionUs; + this.durationUs = mediaItemData.durationUs; + this.positionInFirstPeriodUs = mediaItemData.positionInFirstPeriodUs; + this.isPlaceholder = mediaItemData.isPlaceholder; + this.periods = mediaItemData.periods; } /** - * Sets the unique identifier of this playlist item within a playlist. + * Sets the unique identifier of this media item within a playlist. * *

    This value will be set as {@link Timeline.Window#uid} for this item. * - * @param uid The unique identifier of this playlist item within a playlist. + * @param uid The unique identifier of this media item within a playlist. * @return This builder. */ @CanIgnoreReturnValue @@ -1307,9 +1308,9 @@ public Builder setUid(Object uid) { } /** - * Sets the {@link Tracks} of this playlist item. + * Sets the {@link Tracks} of this media item. * - * @param tracks The {@link Tracks} of this playlist item. + * @param tracks The {@link Tracks} of this media item. * @return This builder. */ @CanIgnoreReturnValue @@ -1319,9 +1320,9 @@ public Builder setTracks(Tracks tracks) { } /** - * Sets the {@link MediaItem} for this playlist item. + * Sets the {@link MediaItem}. * - * @param mediaItem The {@link MediaItem} for this playlist item. + * @param mediaItem The {@link MediaItem}. * @return This builder. */ @CanIgnoreReturnValue @@ -1351,9 +1352,9 @@ public Builder setMediaMetadata(@Nullable MediaMetadata mediaMetadata) { } /** - * Sets the manifest of the playlist item. + * Sets the manifest of the media item. * - * @param manifest The manifest of the playlist item, or null if not applicable. + * @param manifest The manifest of the media item, or null if not applicable. * @return This builder. */ @CanIgnoreReturnValue @@ -1363,11 +1364,10 @@ public Builder setManifest(@Nullable Object manifest) { } /** - * Sets the active {@link MediaItem.LiveConfiguration}, or null if the playlist item is not - * live. + * Sets the active {@link MediaItem.LiveConfiguration}, or null if the media item is not live. * * @param liveConfiguration The active {@link MediaItem.LiveConfiguration}, or null if the - * playlist item is not live. + * media item is not live. * @return This builder. */ @CanIgnoreReturnValue @@ -1428,9 +1428,9 @@ public Builder setElapsedRealtimeEpochOffsetMs(long elapsedRealtimeEpochOffsetMs } /** - * Sets whether it's possible to seek within this playlist item. + * Sets whether it's possible to seek within this media item. * - * @param isSeekable Whether it's possible to seek within this playlist item. + * @param isSeekable Whether it's possible to seek within this media item. * @return This builder. */ @CanIgnoreReturnValue @@ -1440,9 +1440,9 @@ public Builder setIsSeekable(boolean isSeekable) { } /** - * Sets whether this playlist item may change over time, for example a moving live window. + * Sets whether this media item may change over time, for example a moving live window. * - * @param isDynamic Whether this playlist item may change over time, for example a moving live + * @param isDynamic Whether this media item may change over time, for example a moving live * window. * @return This builder. */ @@ -1453,13 +1453,13 @@ public Builder setIsDynamic(boolean isDynamic) { } /** - * Sets the default position relative to the start of the playlist item at which to begin + * Sets the default position relative to the start of the media item at which to begin * playback, in microseconds. * *

    The default position must be less or equal to the {@linkplain #setDurationUs duration}, * is set. * - * @param defaultPositionUs The default position relative to the start of the playlist item at + * @param defaultPositionUs The default position relative to the start of the media item at * which to begin playback, in microseconds. * @return This builder. */ @@ -1471,14 +1471,14 @@ public Builder setDefaultPositionUs(long defaultPositionUs) { } /** - * Sets the duration of the playlist item, in microseconds. + * Sets the duration of the media item, in microseconds. * *

    If both this duration and all {@linkplain #setPeriods period} durations are set, the sum * of this duration and the {@linkplain #setPositionInFirstPeriodUs offset in the first * period} must match the total duration of all periods. * - * @param durationUs The duration of the playlist item, in microseconds, or {@link - * C#TIME_UNSET} if unknown. + * @param durationUs The duration of the media item, in microseconds, or {@link C#TIME_UNSET} + * if unknown. * @return This builder. */ @CanIgnoreReturnValue @@ -1489,11 +1489,11 @@ public Builder setDurationUs(long durationUs) { } /** - * Sets the position of the start of this playlist item relative to the start of the first - * period belonging to it, in microseconds. + * Sets the position of the start of this media item relative to the start of the first period + * belonging to it, in microseconds. * - * @param positionInFirstPeriodUs The position of the start of this playlist item relative to - * the start of the first period belonging to it, in microseconds. + * @param positionInFirstPeriodUs The position of the start of this media item relative to the + * start of the first period belonging to it, in microseconds. * @return This builder. */ @CanIgnoreReturnValue @@ -1504,11 +1504,11 @@ public Builder setPositionInFirstPeriodUs(long positionInFirstPeriodUs) { } /** - * Sets whether this playlist item contains placeholder information because the real - * information has yet to be loaded. + * Sets whether this media item contains placeholder information because the real information + * has yet to be loaded. * - * @param isPlaceholder Whether this playlist item contains placeholder information because - * the real information has yet to be loaded. + * @param isPlaceholder Whether this media item contains placeholder information because the + * real information has yet to be loaded. * @return This builder. */ @CanIgnoreReturnValue @@ -1518,15 +1518,14 @@ public Builder setIsPlaceholder(boolean isPlaceholder) { } /** - * Sets the list of {@linkplain PeriodData periods} in this playlist item. + * Sets the list of {@linkplain PeriodData periods} in this media item. * *

    All periods must have unique {@linkplain PeriodData.Builder#setUid UIDs} and only the * last period is allowed to have an unset {@linkplain PeriodData.Builder#setDurationUs * duration}. * - * @param periods The list of {@linkplain PeriodData periods} in this playlist item, or an - * empty list to assume a single period without ads and the same duration as the playlist - * item. + * @param periods The list of {@linkplain PeriodData periods} in this media item, or an empty + * list to assume a single period without ads and the same duration as the media item. * @return This builder. */ @CanIgnoreReturnValue @@ -1542,17 +1541,17 @@ public Builder setPeriods(List periods) { return this; } - /** Builds the {@link PlaylistItem}. */ - public PlaylistItem build() { - return new PlaylistItem(this); + /** Builds the {@link MediaItemData}. */ + public MediaItemData build() { + return new MediaItemData(this); } } - /** The unique identifier of this playlist item. */ + /** The unique identifier of this media item. */ public final Object uid; - /** The {@link Tracks} of this playlist item. */ + /** The {@link Tracks} of this media item. */ public final Tracks tracks; - /** The {@link MediaItem} for this playlist item. */ + /** The {@link MediaItem}. */ public final MediaItem mediaItem; /** * The {@link MediaMetadata}, including static data from the {@link MediaItem#mediaMetadata @@ -1562,9 +1561,9 @@ public PlaylistItem build() { * {@link Format#metadata Formats}. */ @Nullable public final MediaMetadata mediaMetadata; - /** The manifest of the playlist item, or null if not applicable. */ + /** The manifest of the media item, or null if not applicable. */ @Nullable public final Object manifest; - /** The active {@link MediaItem.LiveConfiguration}, or null if the playlist item is not live. */ + /** The active {@link MediaItem.LiveConfiguration}, or null if the media item is not live. */ @Nullable public final MediaItem.LiveConfiguration liveConfiguration; /** * The start time of the live presentation, in milliseconds since the Unix epoch, or {@link @@ -1582,37 +1581,37 @@ public PlaylistItem build() { * applicable. */ public final long elapsedRealtimeEpochOffsetMs; - /** Whether it's possible to seek within this playlist item. */ + /** Whether it's possible to seek within this media item. */ public final boolean isSeekable; - /** Whether this playlist item may change over time, for example a moving live window. */ + /** Whether this media item may change over time, for example a moving live window. */ public final boolean isDynamic; /** - * The default position relative to the start of the playlist item at which to begin playback, - * in microseconds. + * The default position relative to the start of the media item at which to begin playback, in + * microseconds. */ public final long defaultPositionUs; - /** The duration of the playlist item, in microseconds, or {@link C#TIME_UNSET} if unknown. */ + /** The duration of the media item, in microseconds, or {@link C#TIME_UNSET} if unknown. */ public final long durationUs; /** - * The position of the start of this playlist item relative to the start of the first period + * The position of the start of this media item relative to the start of the first period * belonging to it, in microseconds. */ public final long positionInFirstPeriodUs; /** - * Whether this playlist item contains placeholder information because the real information has - * yet to be loaded. + * Whether this media item contains placeholder information because the real information has yet + * to be loaded. */ public final boolean isPlaceholder; /** - * The list of {@linkplain PeriodData periods} in this playlist item, or an empty list to assume - * a single period without ads and the same duration as the playlist item. + * The list of {@linkplain PeriodData periods} in this media item, or an empty list to assume a + * single period without ads and the same duration as the media item. */ public final ImmutableList periods; private final long[] periodPositionInWindowUs; private final MediaMetadata combinedMediaMetadata; - private PlaylistItem(Builder builder) { + private MediaItemData(Builder builder) { if (builder.liveConfiguration == null) { checkArgument(builder.presentationStartTimeMs == C.TIME_UNSET); checkArgument(builder.windowStartTimeMs == C.TIME_UNSET); @@ -1662,26 +1661,26 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof PlaylistItem)) { + if (!(o instanceof MediaItemData)) { return false; } - PlaylistItem playlistItem = (PlaylistItem) o; - return this.uid.equals(playlistItem.uid) - && this.tracks.equals(playlistItem.tracks) - && this.mediaItem.equals(playlistItem.mediaItem) - && Util.areEqual(this.mediaMetadata, playlistItem.mediaMetadata) - && Util.areEqual(this.manifest, playlistItem.manifest) - && Util.areEqual(this.liveConfiguration, playlistItem.liveConfiguration) - && this.presentationStartTimeMs == playlistItem.presentationStartTimeMs - && this.windowStartTimeMs == playlistItem.windowStartTimeMs - && this.elapsedRealtimeEpochOffsetMs == playlistItem.elapsedRealtimeEpochOffsetMs - && this.isSeekable == playlistItem.isSeekable - && this.isDynamic == playlistItem.isDynamic - && this.defaultPositionUs == playlistItem.defaultPositionUs - && this.durationUs == playlistItem.durationUs - && this.positionInFirstPeriodUs == playlistItem.positionInFirstPeriodUs - && this.isPlaceholder == playlistItem.isPlaceholder - && this.periods.equals(playlistItem.periods); + MediaItemData mediaItemData = (MediaItemData) o; + return this.uid.equals(mediaItemData.uid) + && this.tracks.equals(mediaItemData.tracks) + && this.mediaItem.equals(mediaItemData.mediaItem) + && Util.areEqual(this.mediaMetadata, mediaItemData.mediaMetadata) + && Util.areEqual(this.manifest, mediaItemData.manifest) + && Util.areEqual(this.liveConfiguration, mediaItemData.liveConfiguration) + && this.presentationStartTimeMs == mediaItemData.presentationStartTimeMs + && this.windowStartTimeMs == mediaItemData.windowStartTimeMs + && this.elapsedRealtimeEpochOffsetMs == mediaItemData.elapsedRealtimeEpochOffsetMs + && this.isSeekable == mediaItemData.isSeekable + && this.isDynamic == mediaItemData.isDynamic + && this.defaultPositionUs == mediaItemData.defaultPositionUs + && this.durationUs == mediaItemData.durationUs + && this.positionInFirstPeriodUs == mediaItemData.positionInFirstPeriodUs + && this.isPlaceholder == mediaItemData.isPlaceholder + && this.periods.equals(mediaItemData.periods); } @Override @@ -1730,7 +1729,7 @@ private Timeline.Window getWindow(int firstPeriodIndex, Timeline.Window window) } private Timeline.Period getPeriod( - int windowIndex, int periodIndexInPlaylistItem, Timeline.Period period) { + int windowIndex, int periodIndexInMediaItem, Timeline.Period period) { if (periods.isEmpty()) { period.set( /* id= */ uid, @@ -1741,7 +1740,7 @@ private Timeline.Period getPeriod( AdPlaybackState.NONE, isPlaceholder); } else { - PeriodData periodData = periods.get(periodIndexInPlaylistItem); + PeriodData periodData = periods.get(periodIndexInMediaItem); Object periodId = periodData.uid; Object periodUid = Pair.create(uid, periodId); period.set( @@ -1749,18 +1748,18 @@ private Timeline.Period getPeriod( periodUid, windowIndex, periodData.durationUs, - periodPositionInWindowUs[periodIndexInPlaylistItem], + periodPositionInWindowUs[periodIndexInMediaItem], periodData.adPlaybackState, periodData.isPlaceholder); } return period; } - private Object getPeriodUid(int periodIndexInPlaylistItem) { + private Object getPeriodUid(int periodIndexInMediaItem) { if (periods.isEmpty()) { return uid; } - Object periodId = periods.get(periodIndexInPlaylistItem).uid; + Object periodId = periods.get(periodIndexInMediaItem).uid; return Pair.create(uid, periodId); } @@ -1784,7 +1783,7 @@ private static MediaMetadata getCombinedMediaMetadata(MediaItem mediaItem, Track } } - /** Data describing the properties of a period inside a {@link PlaylistItem}. */ + /** Data describing the properties of a period inside a {@link MediaItemData}. */ protected static final class PeriodData { /** A builder for {@link PeriodData} objects. */ @@ -1798,7 +1797,7 @@ public static final class Builder { /** * Creates the builder. * - * @param uid The unique identifier of the period within its playlist item. + * @param uid The unique identifier of the period within its media item. */ public Builder(Object uid) { this.uid = uid; @@ -1815,9 +1814,9 @@ private Builder(PeriodData periodData) { } /** - * Sets the unique identifier of the period within its playlist item. + * Sets the unique identifier of the period within its media item. * - * @param uid The unique identifier of the period within its playlist item. + * @param uid The unique identifier of the period within its media item. * @return This builder. */ @CanIgnoreReturnValue @@ -1829,7 +1828,7 @@ public Builder setUid(Object uid) { /** * Sets the total duration of the period, in microseconds, or {@link C#TIME_UNSET} if unknown. * - *

    Only the last period in a playlist item can have an unknown duration. + *

    Only the last period in a media item can have an unknown duration. * * @param durationUs The total duration of the period, in microseconds, or {@link * C#TIME_UNSET} if unknown. @@ -1875,11 +1874,11 @@ public PeriodData build() { } } - /** The unique identifier of the period within its playlist item. */ + /** The unique identifier of the period within its media item. */ public final Object uid; /** * The total duration of the period, in microseconds, or {@link C#TIME_UNSET} if unknown. Only - * the last period in a playlist item can have an unknown duration. + * the last period in a media item can have an unknown duration. */ public final long durationUs; /** @@ -2536,8 +2535,7 @@ private void updateStateAndInformListeners(State newState) { if (timelineChanged) { @Player.TimelineChangeReason - int timelineChangeReason = - getTimelineChangeReason(previousState.playlistItems, newState.playlistItems); + int timelineChangeReason = getTimelineChangeReason(previousState.playlist, newState.playlist); listeners.queueEvent( Player.EVENT_TIMELINE_CHANGED, listener -> listener.onTimelineChanged(newState.timeline, timelineChangeReason)); @@ -2564,7 +2562,7 @@ private void updateStateAndInformListeners(State newState) { MediaItem mediaItem = state.timeline.isEmpty() ? null - : state.playlistItems.get(state.currentMediaItemIndex).mediaItem; + : state.playlist.get(state.currentMediaItemIndex).mediaItem; listeners.queueEvent( Player.EVENT_MEDIA_ITEM_TRANSITION, listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason)); @@ -2792,15 +2790,15 @@ private static boolean isPlaying(State state) { } private static Tracks getCurrentTracksInternal(State state) { - return state.playlistItems.isEmpty() + return state.playlist.isEmpty() ? Tracks.EMPTY - : state.playlistItems.get(state.currentMediaItemIndex).tracks; + : state.playlist.get(state.currentMediaItemIndex).tracks; } private static MediaMetadata getMediaMetadataInternal(State state) { - return state.playlistItems.isEmpty() + return state.playlist.isEmpty() ? MediaMetadata.EMPTY - : state.playlistItems.get(state.currentMediaItemIndex).combinedMediaMetadata; + : state.playlist.get(state.currentMediaItemIndex).combinedMediaMetadata; } private static int getCurrentPeriodIndexInternal(State state, Timeline.Window window) { @@ -2814,7 +2812,7 @@ private static int getCurrentPeriodIndexInternal(State state, Timeline.Window wi } private static @Player.TimelineChangeReason int getTimelineChangeReason( - List previousPlaylist, List newPlaylist) { + List previousPlaylist, List newPlaylist) { if (previousPlaylist.size() != newPlaylist.size()) { return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; } @@ -2832,11 +2830,11 @@ private static int getPositionDiscontinuityReason( // We were asked to report a discontinuity. return newState.positionDiscontinuityReason; } - if (previousState.playlistItems.isEmpty()) { - // First change from an empty timeline is not reported as a discontinuity. + if (previousState.playlist.isEmpty()) { + // First change from an empty playlist is not reported as a discontinuity. return C.INDEX_UNSET; } - if (newState.playlistItems.isEmpty()) { + if (newState.playlist.isEmpty()) { // The playlist became empty. return Player.DISCONTINUITY_REASON_REMOVE; } diff --git a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java index aef72f644fa..d8a0e336d02 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -110,15 +110,15 @@ public void stateBuildUpon_build_isEqual() { .setTimedMetadata(new Metadata()) .setPlaylist( ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setPeriods( ImmutableList.of( new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) .setAdPlaybackState( new AdPlaybackState( /* adsId= */ new Object(), - /* adGroupTimesUs= */ 555, + /* adGroupTimesUs...= */ 555, 666)) .build())) .build())) @@ -142,9 +142,9 @@ public void stateBuildUpon_build_isEqual() { } @Test - public void playlistItemBuildUpon_build_isEqual() { - SimpleBasePlayer.PlaylistItem playlistItem = - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + public void mediaItemDataBuildUpon_build_isEqual() { + SimpleBasePlayer.MediaItemData mediaItemData = + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setTracks( new Tracks( ImmutableList.of( @@ -172,10 +172,10 @@ public void playlistItemBuildUpon_build_isEqual() { new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()).build())) .build(); - SimpleBasePlayer.PlaylistItem newPlaylistItem = playlistItem.buildUpon().build(); + SimpleBasePlayer.MediaItemData newMediaItemData = mediaItemData.buildUpon().build(); - assertThat(newPlaylistItem).isEqualTo(playlistItem); - assertThat(newPlaylistItem.hashCode()).isEqualTo(playlistItem.hashCode()); + assertThat(newMediaItemData).isEqualTo(mediaItemData); + assertThat(newMediaItemData.hashCode()).isEqualTo(mediaItemData.hashCode()); } @Test @@ -185,7 +185,7 @@ public void periodDataBuildUpon_build_isEqual() { .setIsPlaceholder(true) .setDurationUs(600_000) .setAdPlaybackState( - new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666)) + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 555, 666)) .build(); SimpleBasePlayer.PeriodData newPeriodData = periodData.buildUpon().build(); @@ -220,16 +220,16 @@ public void stateBuilderBuild_setsCorrectValues() { Size surfaceSize = new Size(480, 360); DeviceInfo deviceInfo = new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 3, /* maxVolume= */ 7); - ImmutableList playlist = + ImmutableList playlist = ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setPeriods( ImmutableList.of( new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) .setAdPlaybackState( new AdPlaybackState( - /* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666)) + /* adsId= */ new Object(), /* adGroupTimesUs...= */ 555, 666)) .build())) .build()); MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build(); @@ -312,7 +312,7 @@ public void stateBuilderBuild_setsCorrectValues() { assertThat(state.surfaceSize).isEqualTo(surfaceSize); assertThat(state.newlyRenderedFirstFrame).isTrue(); assertThat(state.timedMetadata).isEqualTo(timedMetadata); - assertThat(state.playlistItems).isEqualTo(playlist); + assertThat(state.playlist).isEqualTo(playlist); assertThat(state.playlistMetadata).isEqualTo(playlistMetadata); assertThat(state.currentMediaItemIndex).isEqualTo(1); assertThat(state.currentPeriodIndex).isEqualTo(1); @@ -369,8 +369,9 @@ public void stateBuilderBuild_currentWindowIndexExceedsPlaylistLength_throwsExce new SimpleBasePlayer.State.Builder() .setPlaylist( ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) + .build())) .setCurrentMediaItemIndex(2) .build()); } @@ -383,8 +384,9 @@ public void stateBuilderBuild_currentPeriodIndexExceedsPlaylistLength_throwsExce new SimpleBasePlayer.State.Builder() .setPlaylist( ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) + .build())) .setCurrentPeriodIndex(2) .build()); } @@ -397,8 +399,9 @@ public void stateBuilderBuild_currentPeriodIndexInOtherMediaItem_throwsException new SimpleBasePlayer.State.Builder() .setPlaylist( ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) + .build())) .setCurrentMediaItemIndex(0) .setCurrentPeriodIndex(1) .build()); @@ -412,14 +415,14 @@ public void stateBuilderBuild_currentAdGroupIndexExceedsAdGroupCount_throwsExcep new SimpleBasePlayer.State.Builder() .setPlaylist( ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setPeriods( ImmutableList.of( new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) .setAdPlaybackState( new AdPlaybackState( /* adsId= */ new Object(), - /* adGroupTimesUs= */ 123)) + /* adGroupTimesUs...= */ 123)) .build())) .build())) .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2) @@ -434,14 +437,14 @@ public void stateBuilderBuild_currentAdIndexExceedsAdCountInAdGroup_throwsExcept new SimpleBasePlayer.State.Builder() .setPlaylist( ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setPeriods( ImmutableList.of( new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) .setAdPlaybackState( new AdPlaybackState( /* adsId= */ new Object(), - /* adGroupTimesUs= */ 123) + /* adGroupTimesUs...= */ 123) .withAdCount( /* adGroupIndex= */ 0, /* adCount= */ 2)) .build())) @@ -466,7 +469,7 @@ public void stateBuilderBuild_playerErrorInNonIdleState_throwsException() { } @Test - public void stateBuilderBuild_multiplePlaylistItemsWithSameIds_throwsException() { + public void stateBuilderBuild_multipleMediaItemsWithSameIds_throwsException() { Object uid = new Object(); assertThrows( @@ -475,8 +478,8 @@ public void stateBuilderBuild_multiplePlaylistItemsWithSameIds_throwsException() new SimpleBasePlayer.State.Builder() .setPlaylist( ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(uid).build(), - new SimpleBasePlayer.PlaylistItem.Builder(uid).build())) + new SimpleBasePlayer.MediaItemData.Builder(uid).build(), + new SimpleBasePlayer.MediaItemData.Builder(uid).build())) .build()); } @@ -517,7 +520,7 @@ public void stateBuilderBuild_returnsAdvancingContentPositionWhenPlaying() { new SimpleBasePlayer.State.Builder() .setPlaylist( ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) .setContentPositionMs(4000) .setPlayWhenReady(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) .setPlaybackState(Player.STATE_READY) @@ -539,7 +542,7 @@ public void stateBuilderBuild_returnsConstantContentPositionWhenNotPlaying() { new SimpleBasePlayer.State.Builder() .setPlaylist( ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) .setContentPositionMs(4000) .setPlaybackState(Player.STATE_BUFFERING) .build(); @@ -559,14 +562,14 @@ public void stateBuilderBuild_returnsAdvancingAdPositionWhenPlaying() { new SimpleBasePlayer.State.Builder() .setPlaylist( ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setPeriods( ImmutableList.of( new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) .setAdPlaybackState( new AdPlaybackState( /* adsId= */ new Object(), - /* adGroupTimesUs= */ 123) + /* adGroupTimesUs...= */ 123) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2)) .build())) .build())) @@ -593,14 +596,14 @@ public void stateBuilderBuild_returnsConstantAdPositionWhenNotPlaying() { new SimpleBasePlayer.State.Builder() .setPlaylist( ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setPeriods( ImmutableList.of( new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) .setAdPlaybackState( new AdPlaybackState( /* adsId= */ new Object(), - /* adGroupTimesUs= */ 123) + /* adGroupTimesUs...= */ 123) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2)) .build())) .build())) @@ -617,7 +620,7 @@ public void stateBuilderBuild_returnsConstantAdPositionWhenNotPlaying() { } @Test - public void playlistItemBuilderBuild_setsCorrectValues() { + public void mediaItemDataBuilderBuild_setsCorrectValues() { Object uid = new Object(); Tracks tracks = new Tracks( @@ -635,8 +638,8 @@ public void playlistItemBuilderBuild_setsCorrectValues() { ImmutableList periods = ImmutableList.of(new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()).build()); - SimpleBasePlayer.PlaylistItem playlistItem = - new SimpleBasePlayer.PlaylistItem.Builder(uid) + SimpleBasePlayer.MediaItemData mediaItemData = + new SimpleBasePlayer.MediaItemData.Builder(uid) .setTracks(tracks) .setMediaItem(mediaItem) .setMediaMetadata(mediaMetadata) @@ -654,61 +657,61 @@ public void playlistItemBuilderBuild_setsCorrectValues() { .setPeriods(periods) .build(); - assertThat(playlistItem.uid).isEqualTo(uid); - assertThat(playlistItem.tracks).isEqualTo(tracks); - assertThat(playlistItem.mediaItem).isEqualTo(mediaItem); - assertThat(playlistItem.mediaMetadata).isEqualTo(mediaMetadata); - assertThat(playlistItem.manifest).isEqualTo(manifest); - assertThat(playlistItem.liveConfiguration).isEqualTo(liveConfiguration); - assertThat(playlistItem.presentationStartTimeMs).isEqualTo(12); - assertThat(playlistItem.windowStartTimeMs).isEqualTo(23); - assertThat(playlistItem.elapsedRealtimeEpochOffsetMs).isEqualTo(10234); - assertThat(playlistItem.isSeekable).isTrue(); - assertThat(playlistItem.isDynamic).isTrue(); - assertThat(playlistItem.defaultPositionUs).isEqualTo(456_789); - assertThat(playlistItem.durationUs).isEqualTo(500_000); - assertThat(playlistItem.positionInFirstPeriodUs).isEqualTo(100_000); - assertThat(playlistItem.isPlaceholder).isTrue(); - assertThat(playlistItem.periods).isEqualTo(periods); + assertThat(mediaItemData.uid).isEqualTo(uid); + assertThat(mediaItemData.tracks).isEqualTo(tracks); + assertThat(mediaItemData.mediaItem).isEqualTo(mediaItem); + assertThat(mediaItemData.mediaMetadata).isEqualTo(mediaMetadata); + assertThat(mediaItemData.manifest).isEqualTo(manifest); + assertThat(mediaItemData.liveConfiguration).isEqualTo(liveConfiguration); + assertThat(mediaItemData.presentationStartTimeMs).isEqualTo(12); + assertThat(mediaItemData.windowStartTimeMs).isEqualTo(23); + assertThat(mediaItemData.elapsedRealtimeEpochOffsetMs).isEqualTo(10234); + assertThat(mediaItemData.isSeekable).isTrue(); + assertThat(mediaItemData.isDynamic).isTrue(); + assertThat(mediaItemData.defaultPositionUs).isEqualTo(456_789); + assertThat(mediaItemData.durationUs).isEqualTo(500_000); + assertThat(mediaItemData.positionInFirstPeriodUs).isEqualTo(100_000); + assertThat(mediaItemData.isPlaceholder).isTrue(); + assertThat(mediaItemData.periods).isEqualTo(periods); } @Test - public void playlistItemBuilderBuild_presentationStartTimeIfNotLive_throwsException() { + public void mediaItemDataBuilderBuild_presentationStartTimeIfNotLive_throwsException() { assertThrows( IllegalArgumentException.class, () -> - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setPresentationStartTimeMs(12) .build()); } @Test - public void playlistItemBuilderBuild_windowStartTimeIfNotLive_throwsException() { + public void mediaItemDataBuilderBuild_windowStartTimeIfNotLive_throwsException() { assertThrows( IllegalArgumentException.class, () -> - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setWindowStartTimeMs(12) .build()); } @Test - public void playlistItemBuilderBuild_elapsedEpochOffsetIfNotLive_throwsException() { + public void mediaItemDataBuilderBuild_elapsedEpochOffsetIfNotLive_throwsException() { assertThrows( IllegalArgumentException.class, () -> - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setElapsedRealtimeEpochOffsetMs(12) .build()); } @Test public void - playlistItemBuilderBuild_windowStartTimeLessThanPresentationStartTime_throwsException() { + mediaItemDataBuilderBuild_windowStartTimeLessThanPresentationStartTime_throwsException() { assertThrows( IllegalArgumentException.class, () -> - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setLiveConfiguration(MediaItem.LiveConfiguration.UNSET) .setWindowStartTimeMs(12) .setPresentationStartTimeMs(13) @@ -716,13 +719,13 @@ public void playlistItemBuilderBuild_elapsedEpochOffsetIfNotLive_throwsException } @Test - public void playlistItemBuilderBuild_multiplePeriodsWithSameUid_throwsException() { + public void mediaItemDataBuilderBuild_multiplePeriodsWithSameUid_throwsException() { Object uid = new Object(); assertThrows( IllegalArgumentException.class, () -> - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setPeriods( ImmutableList.of( new SimpleBasePlayer.PeriodData.Builder(uid).build(), @@ -731,11 +734,11 @@ public void playlistItemBuilderBuild_multiplePeriodsWithSameUid_throwsException( } @Test - public void playlistItemBuilderBuild_defaultPositionGreaterThanDuration_throwsException() { + public void mediaItemDataBuilderBuild_defaultPositionGreaterThanDuration_throwsException() { assertThrows( IllegalArgumentException.class, () -> - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setDefaultPositionUs(16) .setDurationUs(15) .build()); @@ -745,7 +748,7 @@ public void playlistItemBuilderBuild_defaultPositionGreaterThanDuration_throwsEx public void periodDataBuilderBuild_setsCorrectValues() { Object uid = new Object(); AdPlaybackState adPlaybackState = - new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666); + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 555, 666); SimpleBasePlayer.PeriodData periodData = new SimpleBasePlayer.PeriodData.Builder(uid) @@ -788,7 +791,7 @@ public void getterMethods_noOtherMethodCalls_returnCurrentState() { SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456; SimpleBasePlayer.PositionSupplier contentBufferedPositionSupplier = () -> 499; SimpleBasePlayer.PositionSupplier totalBufferedPositionSupplier = () -> 567; - Object playlistItemUid = new Object(); + Object mediaItemUid = new Object(); Object periodUid = new Object(); Tracks tracks = new Tracks( @@ -804,10 +807,10 @@ public void getterMethods_noOtherMethodCalls_returnCurrentState() { Size surfaceSize = new Size(480, 360); MediaItem.LiveConfiguration liveConfiguration = new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build(); - ImmutableList playlist = + ImmutableList playlist = ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), - new SimpleBasePlayer.PlaylistItem.Builder(playlistItemUid) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid) .setTracks(tracks) .setMediaItem(mediaItem) .setMediaMetadata(mediaMetadata) @@ -829,7 +832,7 @@ public void getterMethods_noOtherMethodCalls_returnCurrentState() { .setDurationUs(600_000) .setAdPlaybackState( new AdPlaybackState( - /* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666)) + /* adsId= */ new Object(), /* adGroupTimesUs...= */ 555, 666)) .build())) .build()); State state = @@ -948,7 +951,7 @@ protected State getState() { assertThat(window.liveConfiguration).isEqualTo(liveConfiguration); assertThat(window.manifest).isEqualTo(manifest); assertThat(window.mediaItem).isEqualTo(mediaItem); - assertThat(window.uid).isEqualTo(playlistItemUid); + assertThat(window.uid).isEqualTo(mediaItemUid); Timeline.Period period = timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true); assertThat(period.durationUs).isEqualTo(C.TIME_UNSET); @@ -974,10 +977,10 @@ public void getterMethods_duringAd_returnAdState() { SimpleBasePlayer.PositionSupplier totalBufferedPositionSupplier = () -> 567; SimpleBasePlayer.PositionSupplier adPositionSupplier = () -> 321; SimpleBasePlayer.PositionSupplier adBufferedPositionSupplier = () -> 345; - ImmutableList playlist = + ImmutableList playlist = ImmutableList.of( - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .setDurationUs(500_000) .setPeriods( ImmutableList.of( @@ -986,7 +989,9 @@ public void getterMethods_duringAd_returnAdState() { .setDurationUs(600_000) .setAdPlaybackState( new AdPlaybackState( - /* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666) + /* adsId= */ new Object(), /* adGroupTimesUs...= */ + 555, + 666) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1) .withAdDurationsUs( @@ -1051,8 +1056,8 @@ protected State getState() { public void invalidateState_updatesStateAndInformsListeners() throws Exception { Object mediaItemUid0 = new Object(); MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); - SimpleBasePlayer.PlaylistItem playlistItem0 = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0).setMediaItem(mediaItem0).build(); + SimpleBasePlayer.MediaItemData mediaItemData0 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid0).setMediaItem(mediaItem0).build(); State state1 = new State.Builder() .setAvailableCommands(new Commands.Builder().addAllCommands().build()) @@ -1078,7 +1083,7 @@ public void invalidateState_updatesStateAndInformsListeners() throws Exception { .setDeviceInfo(DeviceInfo.UNKNOWN) .setDeviceVolume(0) .setIsDeviceMuted(false) - .setPlaylist(ImmutableList.of(playlistItem0)) + .setPlaylist(ImmutableList.of(mediaItemData0)) .setPlaylistMetadata(MediaMetadata.EMPTY) .setCurrentMediaItemIndex(0) .setContentPositionMs(8_000) @@ -1094,8 +1099,8 @@ public void invalidateState_updatesStateAndInformsListeners() throws Exception { /* adaptiveSupported= */ true, /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, /* trackSelected= */ new boolean[] {true}))); - SimpleBasePlayer.PlaylistItem playlistItem1 = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1) + SimpleBasePlayer.MediaItemData mediaItemData1 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid1) .setMediaItem(mediaItem1) .setMediaMetadata(mediaMetadata) .setTracks(tracks) @@ -1156,7 +1161,7 @@ public void invalidateState_updatesStateAndInformsListeners() throws Exception { .setSurfaceSize(surfaceSize) .setNewlyRenderedFirstFrame(true) .setTimedMetadata(timedMetadata) - .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setPlaylist(ImmutableList.of(mediaItemData0, mediaItemData1)) .setPlaylistMetadata(playlistMetadata) .setCurrentMediaItemIndex(1) .setContentPositionMs(12_000) @@ -1297,20 +1302,20 @@ protected State getState() { } @Test - public void invalidateState_withPlaylistItemDetailChange_reportsTimelineSourceUpdate() { + public void invalidateState_withMediaItemDetailChange_reportsTimelineSourceUpdate() { Object mediaItemUid0 = new Object(); - SimpleBasePlayer.PlaylistItem playlistItem0 = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0).build(); + SimpleBasePlayer.MediaItemData mediaItemData0 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid0).build(); Object mediaItemUid1 = new Object(); - SimpleBasePlayer.PlaylistItem playlistItem1 = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).build(); + SimpleBasePlayer.MediaItemData mediaItemData1 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid1).build(); State state1 = - new State.Builder().setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)).build(); - SimpleBasePlayer.PlaylistItem playlistItem1Updated = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setDurationUs(10_000).build(); + new State.Builder().setPlaylist(ImmutableList.of(mediaItemData0, mediaItemData1)).build(); + SimpleBasePlayer.MediaItemData mediaItemData1Updated = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid1).setDurationUs(10_000).build(); State state2 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1Updated)) + .setPlaylist(ImmutableList.of(mediaItemData0, mediaItemData1Updated)) .build(); AtomicBoolean returnState2 = new AtomicBoolean(); SimpleBasePlayer player = @@ -1336,21 +1341,21 @@ protected State getState() { public void invalidateState_withCurrentMediaItemRemoval_reportsDiscontinuityReasonRemoved() { Object mediaItemUid0 = new Object(); MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); - SimpleBasePlayer.PlaylistItem playlistItem0 = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0).setMediaItem(mediaItem0).build(); + SimpleBasePlayer.MediaItemData mediaItemData0 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid0).setMediaItem(mediaItem0).build(); Object mediaItemUid1 = new Object(); MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); - SimpleBasePlayer.PlaylistItem playlistItem1 = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); + SimpleBasePlayer.MediaItemData mediaItemData1 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); State state1 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setPlaylist(ImmutableList.of(mediaItemData0, mediaItemData1)) .setCurrentMediaItemIndex(1) .setContentPositionMs(5000) .build(); State state2 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem0)) + .setPlaylist(ImmutableList.of(mediaItemData0)) .setCurrentMediaItemIndex(0) .setContentPositionMs(2000) .build(); @@ -1402,24 +1407,24 @@ protected State getState() { invalidateState_withTransitionFromEndOfItem_reportsDiscontinuityReasonAutoTransition() { Object mediaItemUid0 = new Object(); MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); - SimpleBasePlayer.PlaylistItem playlistItem0 = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0) + SimpleBasePlayer.MediaItemData mediaItemData0 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid0) .setMediaItem(mediaItem0) .setDurationUs(50_000) .build(); Object mediaItemUid1 = new Object(); MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); - SimpleBasePlayer.PlaylistItem playlistItem1 = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); + SimpleBasePlayer.MediaItemData mediaItemData1 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); State state1 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setPlaylist(ImmutableList.of(mediaItemData0, mediaItemData1)) .setCurrentMediaItemIndex(0) .setContentPositionMs(50) .build(); State state2 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setPlaylist(ImmutableList.of(mediaItemData0, mediaItemData1)) .setCurrentMediaItemIndex(1) .setContentPositionMs(10) .build(); @@ -1469,24 +1474,24 @@ protected State getState() { public void invalidateState_withTransitionFromMiddleOfItem_reportsDiscontinuityReasonSkip() { Object mediaItemUid0 = new Object(); MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); - SimpleBasePlayer.PlaylistItem playlistItem0 = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0) + SimpleBasePlayer.MediaItemData mediaItemData0 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid0) .setMediaItem(mediaItem0) .setDurationUs(50_000) .build(); Object mediaItemUid1 = new Object(); MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); - SimpleBasePlayer.PlaylistItem playlistItem1 = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); + SimpleBasePlayer.MediaItemData mediaItemData1 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); State state1 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setPlaylist(ImmutableList.of(mediaItemData0, mediaItemData1)) .setCurrentMediaItemIndex(0) .setContentPositionMs(20) .build(); State state2 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setPlaylist(ImmutableList.of(mediaItemData0, mediaItemData1)) .setCurrentMediaItemIndex(1) .setContentPositionMs(10) .build(); @@ -1537,20 +1542,20 @@ protected State getState() { public void invalidateState_withRepeatingItem_reportsDiscontinuityReasonAutoTransition() { Object mediaItemUid = new Object(); MediaItem mediaItem = new MediaItem.Builder().setMediaId("0").build(); - SimpleBasePlayer.PlaylistItem playlistItem = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid) + SimpleBasePlayer.MediaItemData mediaItemData = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid) .setMediaItem(mediaItem) .setDurationUs(5_000_000) .build(); State state1 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem)) + .setPlaylist(ImmutableList.of(mediaItemData)) .setCurrentMediaItemIndex(0) .setContentPositionMs(5_000) .build(); State state2 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem)) + .setPlaylist(ImmutableList.of(mediaItemData)) .setCurrentMediaItemIndex(0) .setContentPositionMs(0) .build(); @@ -1600,20 +1605,20 @@ protected State getState() { public void invalidateState_withDiscontinuityInsideItem_reportsDiscontinuityReasonInternal() { Object mediaItemUid = new Object(); MediaItem mediaItem = new MediaItem.Builder().setMediaId("0").build(); - SimpleBasePlayer.PlaylistItem playlistItem = - new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid) + SimpleBasePlayer.MediaItemData mediaItemData = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid) .setMediaItem(mediaItem) .setDurationUs(5_000_000) .build(); State state1 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem)) + .setPlaylist(ImmutableList.of(mediaItemData)) .setCurrentMediaItemIndex(0) .setContentPositionMs(1_000) .build(); State state2 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem)) + .setPlaylist(ImmutableList.of(mediaItemData)) .setCurrentMediaItemIndex(0) .setContentPositionMs(3_000) .build(); @@ -1661,17 +1666,17 @@ protected State getState() { @Test public void invalidateState_withMinorPositionDrift_doesNotReportsDiscontinuity() { - SimpleBasePlayer.PlaylistItem playlistItem = - new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(); + SimpleBasePlayer.MediaItemData mediaItemData = + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(); State state1 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem)) + .setPlaylist(ImmutableList.of(mediaItemData)) .setCurrentMediaItemIndex(0) .setContentPositionMs(1_000) .build(); State state2 = new State.Builder() - .setPlaylist(ImmutableList.of(playlistItem)) + .setPlaylist(ImmutableList.of(mediaItemData)) .setCurrentMediaItemIndex(0) .setContentPositionMs(1_500) .build(); From 93694b22839de311876c676f27edd246f538ec4d Mon Sep 17 00:00:00 2001 From: Googler Date: Wed, 30 Nov 2022 19:51:02 +0000 Subject: [PATCH 043/141] Decomission ControllerInfoProxy in favor of ControllerInfo. This CL makes it possible to create a media3 ControllerInfo in test code, which is needed to test several aspects of a media3-based media app. It does this by exposing a test-only static factory method. This is a hacky low-effort approach; a better solution could be to split ControllerInfo up into a public interface that was exposed to client logic, and that they could extend, and a package-private implementation with internal fields like the callback. That's a much bigger change, however. PiperOrigin-RevId: 491978830 (cherry picked from commit 69093db7f5889037a3b55e3d1a7242c31ce62f2f) --- .../androidx/media3/session/MediaSession.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 6b25c8d56cd..2bf05e7a272 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -15,6 +15,7 @@ */ package androidx.media3.session; +import static androidx.annotation.VisibleForTesting.PRIVATE; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; @@ -502,6 +503,24 @@ public String toString() { /* cb= */ null, /* connectionHints= */ Bundle.EMPTY); } + + // TODO(b/259546357): Remove when ControllerInfo can be instantiated cleanly in tests. + /** Returns a {@link ControllerInfo} suitable for use when testing client code. */ + @VisibleForTesting(otherwise = PRIVATE) + public static ControllerInfo createTestOnlyControllerInfo( + RemoteUserInfo remoteUserInfo, + int libraryVersion, + int interfaceVersion, + boolean trusted, + Bundle connectionHints) { + return new MediaSession.ControllerInfo( + remoteUserInfo, + libraryVersion, + interfaceVersion, + trusted, + /* cb= */ null, + connectionHints); + } } private final MediaSessionImpl impl; From 8932c5212242dd5d9206d472149e751bfc09854f Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 30 Nov 2022 21:29:53 +0000 Subject: [PATCH 044/141] Parse and set bitrates in `Ac3Reader` PiperOrigin-RevId: 492003800 (cherry picked from commit c7aa54cb411e485c2c17e630779d9e27d758a550) --- .../androidx/media3/extractor/Ac3Util.java | 21 +++++++++++++++++-- .../media3/extractor/ts/Ac3Reader.java | 10 +++++++-- .../extractordumps/ts/sample.ac3.0.dump | 2 ++ .../ts/sample.ac3.unknown_length.dump | 2 ++ .../extractordumps/ts/sample.eac3.0.dump | 1 + .../ts/sample.eac3.unknown_length.dump | 1 + .../extractordumps/ts/sample_ac3.ps.0.dump | 2 ++ .../ts/sample_ac3.ps.unknown_length.dump | 2 ++ .../extractordumps/ts/sample_ac3.ts.0.dump | 2 ++ .../extractordumps/ts/sample_ac3.ts.1.dump | 2 ++ .../extractordumps/ts/sample_ac3.ts.2.dump | 2 ++ .../extractordumps/ts/sample_ac3.ts.3.dump | 2 ++ .../ts/sample_ac3.ts.unknown_length.dump | 2 ++ .../extractordumps/ts/sample_ait.ts.0.dump | 1 + .../ts/sample_ait.ts.unknown_length.dump | 1 + .../extractordumps/ts/sample_eac3.ts.0.dump | 1 + .../extractordumps/ts/sample_eac3.ts.1.dump | 1 + .../extractordumps/ts/sample_eac3.ts.2.dump | 1 + .../extractordumps/ts/sample_eac3.ts.3.dump | 1 + .../ts/sample_eac3.ts.unknown_length.dump | 1 + .../ts/sample_eac3joc.ec3.0.dump | 1 + .../ts/sample_eac3joc.ec3.unknown_length.dump | 1 + .../ts/sample_eac3joc.ts.0.dump | 1 + .../ts/sample_eac3joc.ts.1.dump | 1 + .../ts/sample_eac3joc.ts.2.dump | 1 + .../ts/sample_eac3joc.ts.3.dump | 1 + .../ts/sample_eac3joc.ts.unknown_length.dump | 1 + 27 files changed, 61 insertions(+), 4 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java index b9279635d83..9fe613aac2a 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac3Util.java @@ -80,6 +80,8 @@ public static final class SyncFrameInfo { public final int frameSize; /** Number of audio samples in the frame. */ public final int sampleCount; + /** The bitrate of audio samples. */ + public final int bitrate; private SyncFrameInfo( @Nullable String mimeType, @@ -87,13 +89,15 @@ private SyncFrameInfo( int channelCount, int sampleRate, int frameSize, - int sampleCount) { + int sampleCount, + int bitrate) { this.mimeType = mimeType; this.streamType = streamType; this.channelCount = channelCount; this.sampleRate = sampleRate; this.frameSize = frameSize; this.sampleCount = sampleCount; + this.bitrate = bitrate; } } @@ -261,6 +265,7 @@ public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { int sampleCount; boolean lfeon; int channelCount; + int bitrate; if (isEac3) { // Subsection E.1.2. data.skipBits(16); // syncword @@ -293,6 +298,7 @@ public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; } sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks; + bitrate = calculateEac3Bitrate(frameSize, sampleRate, audioBlocks); acmod = data.readBits(3); lfeon = data.readBit(); channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); @@ -448,6 +454,7 @@ public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { mimeType = null; } int frmsizecod = data.readBits(6); + bitrate = BITRATE_BY_HALF_FRMSIZECOD[frmsizecod / 2] * 1000; frameSize = getAc3SyncframeSize(fscod, frmsizecod); data.skipBits(5 + 3); // bsid, bsmod acmod = data.readBits(3); @@ -467,7 +474,7 @@ public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); } return new SyncFrameInfo( - mimeType, streamType, channelCount, sampleRate, frameSize, sampleCount); + mimeType, streamType, channelCount, sampleRate, frameSize, sampleCount, bitrate); } /** @@ -589,5 +596,15 @@ private static int getAc3SyncframeSize(int fscod, int frmsizecod) { } } + /** + * Derived from the formula defined in F.6.2.2 to calculate data_rate for the (E-)AC3 bitstream. + * Note: The formula is based on frmsiz read from the spec. We already do some modifications to it + * when deriving frameSize from the read value. The formula used here is adapted to accommodate + * that modification. + */ + private static int calculateEac3Bitrate(int frameSize, int sampleRate, int audioBlocks) { + return (frameSize * sampleRate) / (audioBlocks * 32); + } + private Ac3Util() {} } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/Ac3Reader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/Ac3Reader.java index 1d80fbca086..02d674c9734 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/Ac3Reader.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/Ac3Reader.java @@ -22,6 +22,7 @@ import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.ParsableBitArray; import androidx.media3.common.util.ParsableByteArray; @@ -209,14 +210,19 @@ private void parseHeader() { || frameInfo.channelCount != format.channelCount || frameInfo.sampleRate != format.sampleRate || !Util.areEqual(frameInfo.mimeType, format.sampleMimeType)) { - format = + Format.Builder formatBuilder = new Format.Builder() .setId(formatId) .setSampleMimeType(frameInfo.mimeType) .setChannelCount(frameInfo.channelCount) .setSampleRate(frameInfo.sampleRate) .setLanguage(language) - .build(); + .setPeakBitrate(frameInfo.bitrate); + // AC3 has constant bitrate, so averageBitrate = peakBitrate + if (MimeTypes.AUDIO_AC3.equals(frameInfo.mimeType)) { + formatBuilder.setAverageBitrate(frameInfo.bitrate); + } + format = formatBuilder.build(); output.format(format); } sampleSize = frameInfo.frameSize; diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample.ac3.0.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample.ac3.0.dump index 3f582caedd9..8aad7940f23 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample.ac3.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample.ac3.0.dump @@ -7,6 +7,8 @@ track 0: total output bytes = 13281 sample count = 8 format 0: + averageBitrate = 384000 + peakBitrate = 384000 id = 0 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample.ac3.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample.ac3.unknown_length.dump index 3f582caedd9..8aad7940f23 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample.ac3.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample.ac3.unknown_length.dump @@ -7,6 +7,8 @@ track 0: total output bytes = 13281 sample count = 8 format 0: + averageBitrate = 384000 + peakBitrate = 384000 id = 0 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample.eac3.0.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample.eac3.0.dump index f3d9d3997df..f8be0e618c2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample.eac3.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample.eac3.0.dump @@ -7,6 +7,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 6000000 id = 0 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample.eac3.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample.eac3.unknown_length.dump index f3d9d3997df..f8be0e618c2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample.eac3.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample.eac3.unknown_length.dump @@ -7,6 +7,7 @@ track 0: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 6000000 id = 0 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ps.0.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ps.0.dump index 27d0c450fd4..143245058f0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ps.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ps.0.dump @@ -10,6 +10,8 @@ track 189: total output bytes = 1252 sample count = 3 format 0: + averageBitrate = 96000 + peakBitrate = 96000 id = 189 sampleMimeType = audio/ac3 channelCount = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ps.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ps.unknown_length.dump index 960882156b5..62c215256f3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ps.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ps.unknown_length.dump @@ -7,6 +7,8 @@ track 189: total output bytes = 1252 sample count = 3 format 0: + averageBitrate = 96000 + peakBitrate = 96000 id = 189 sampleMimeType = audio/ac3 channelCount = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.0.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.0.dump index 561963e10c5..f3ac4e2018c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.0.dump @@ -10,6 +10,8 @@ track 1900: total output bytes = 13281 sample count = 8 format 0: + averageBitrate = 384000 + peakBitrate = 384000 id = 1/1900 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.1.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.1.dump index d778af898d9..9f141492b2a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.1.dump @@ -10,6 +10,8 @@ track 1900: total output bytes = 10209 sample count = 6 format 0: + averageBitrate = 384000 + peakBitrate = 384000 id = 1/1900 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.2.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.2.dump index f48ba43854d..e6cea3993f0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.2.dump @@ -10,6 +10,8 @@ track 1900: total output bytes = 7137 sample count = 4 format 0: + averageBitrate = 384000 + peakBitrate = 384000 id = 1/1900 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.3.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.3.dump index 997d7a6b02b..da9814ead36 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.3.dump @@ -10,6 +10,8 @@ track 1900: total output bytes = 0 sample count = 0 format 0: + averageBitrate = 384000 + peakBitrate = 384000 id = 1/1900 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.unknown_length.dump index a98cb798cb6..f992ac64e81 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ac3.ts.unknown_length.dump @@ -7,6 +7,8 @@ track 1900: total output bytes = 13281 sample count = 8 format 0: + averageBitrate = 384000 + peakBitrate = 384000 id = 1/1900 sampleMimeType = audio/ac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ait.ts.0.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ait.ts.0.dump index 355b4032938..3a305ed6620 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ait.ts.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ait.ts.0.dump @@ -7,6 +7,7 @@ track 330: total output bytes = 9928 sample count = 19 format 0: + peakBitrate = 128000 id = 1031/330 sampleMimeType = audio/eac3 channelCount = 2 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ait.ts.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ait.ts.unknown_length.dump index 355b4032938..3a305ed6620 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_ait.ts.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_ait.ts.unknown_length.dump @@ -7,6 +7,7 @@ track 330: total output bytes = 9928 sample count = 19 format 0: + peakBitrate = 128000 id = 1031/330 sampleMimeType = audio/eac3 channelCount = 2 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.0.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.0.dump index dfc89c5f19a..2ccaceef7fc 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.0.dump @@ -10,6 +10,7 @@ track 1900: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 6000000 id = 1/1900 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.1.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.1.dump index c06294df2c6..ccf162fc315 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.1.dump @@ -10,6 +10,7 @@ track 1900: total output bytes = 168000 sample count = 42 format 0: + peakBitrate = 6000000 id = 1/1900 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.2.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.2.dump index 91046074984..286fc25b16d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.2.dump @@ -10,6 +10,7 @@ track 1900: total output bytes = 96000 sample count = 24 format 0: + peakBitrate = 6000000 id = 1/1900 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.3.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.3.dump index c490b7eca8b..cfbfae20b65 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.3.dump @@ -10,6 +10,7 @@ track 1900: total output bytes = 0 sample count = 0 format 0: + peakBitrate = 6000000 id = 1/1900 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.unknown_length.dump index 0aae4097a71..4bb8650d721 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3.ts.unknown_length.dump @@ -7,6 +7,7 @@ track 1900: total output bytes = 216000 sample count = 54 format 0: + peakBitrate = 6000000 id = 1/1900 sampleMimeType = audio/eac3 channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ec3.0.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ec3.0.dump index f8888698bde..5a8f9181056 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ec3.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ec3.0.dump @@ -7,6 +7,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640000 id = 0 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ec3.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ec3.unknown_length.dump index f8888698bde..5a8f9181056 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ec3.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ec3.unknown_length.dump @@ -7,6 +7,7 @@ track 0: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640000 id = 0 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.0.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.0.dump index a3cf812691f..2fc2f492801 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.0.dump @@ -10,6 +10,7 @@ track 1900: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640000 id = 1/1900 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.1.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.1.dump index 77951bd767b..771c5216c5c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.1.dump @@ -10,6 +10,7 @@ track 1900: total output bytes = 112640 sample count = 44 format 0: + peakBitrate = 640000 id = 1/1900 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.2.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.2.dump index 0354754df27..452d8eea13d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.2.dump @@ -10,6 +10,7 @@ track 1900: total output bytes = 56320 sample count = 22 format 0: + peakBitrate = 640000 id = 1/1900 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.3.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.3.dump index 742d87e2719..8da152a79ff 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.3.dump @@ -10,6 +10,7 @@ track 1900: total output bytes = 5120 sample count = 2 format 0: + peakBitrate = 640000 id = 1/1900 sampleMimeType = audio/eac3-joc channelCount = 6 diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.unknown_length.dump index 269dd635936..82ce1d24bbb 100644 --- a/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_eac3joc.ts.unknown_length.dump @@ -7,6 +7,7 @@ track 1900: total output bytes = 163840 sample count = 64 format 0: + peakBitrate = 640000 id = 1/1900 sampleMimeType = audio/eac3-joc channelCount = 6 From ed38ec79bc433e338f5bacae8a02a071135f7ff4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 1 Dec 2022 08:30:19 +0000 Subject: [PATCH 045/141] Add media type to MediaMetadata This helps to denote what type of content or folder the metadata describes. PiperOrigin-RevId: 492123690 (cherry picked from commit 32fafefae81e0ab6d3769152e584981c1a62fc60) --- RELEASENOTES.md | 2 + .../androidx/media3/common/MediaMetadata.java | 217 +++++++++++++++++- .../media3/common/MediaMetadataTest.java | 2 + 3 files changed, 218 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 887eea6c08c..0d6f026f794 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ * Metadata: * Parse multiple null-separated values from ID3 frames, as permitted by ID3 v2.4. + * Add `MediaMetadata.mediaType` to denote the type of content or the type + of folder described by the metadata. * Cast extension * Bump Cast SDK version to 21.2.0. diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java index 05d37b29de6..b1d23866a06 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java @@ -76,6 +76,7 @@ public static final class Builder { @Nullable private CharSequence genre; @Nullable private CharSequence compilation; @Nullable private CharSequence station; + @Nullable private @MediaType Integer mediaType; @Nullable private Bundle extras; public Builder() {} @@ -111,6 +112,7 @@ private Builder(MediaMetadata mediaMetadata) { this.genre = mediaMetadata.genre; this.compilation = mediaMetadata.compilation; this.station = mediaMetadata.station; + this.mediaType = mediaMetadata.mediaType; this.extras = mediaMetadata.extras; } @@ -383,6 +385,14 @@ public Builder setStation(@Nullable CharSequence station) { return this; } + /** Sets the {@link MediaType}. */ + @CanIgnoreReturnValue + @UnstableApi + public Builder setMediaType(@Nullable @MediaType Integer mediaType) { + this.mediaType = mediaType; + return this; + } + /** Sets the extras {@link Bundle}. */ @CanIgnoreReturnValue public Builder setExtras(@Nullable Bundle extras) { @@ -529,6 +539,9 @@ public Builder populate(@Nullable MediaMetadata mediaMetadata) { if (mediaMetadata.station != null) { setStation(mediaMetadata.station); } + if (mediaMetadata.mediaType != null) { + setMediaType(mediaMetadata.mediaType); + } if (mediaMetadata.extras != null) { setExtras(mediaMetadata.extras); } @@ -542,12 +555,186 @@ public MediaMetadata build() { } } + /** + * The type of content described by the media item. + * + *

    One of {@link #MEDIA_TYPE_MIXED}, {@link #MEDIA_TYPE_MUSIC}, {@link + * #MEDIA_TYPE_AUDIO_BOOK_CHAPTER}, {@link #MEDIA_TYPE_PODCAST_EPISODE}, {@link + * #MEDIA_TYPE_RADIO_STATION}, {@link #MEDIA_TYPE_NEWS}, {@link #MEDIA_TYPE_VIDEO}, {@link + * #MEDIA_TYPE_TRAILER}, {@link #MEDIA_TYPE_MOVIE}, {@link #MEDIA_TYPE_TV_SHOW}, {@link + * #MEDIA_TYPE_ALBUM}, {@link #MEDIA_TYPE_ARTIST}, {@link #MEDIA_TYPE_GENRE}, {@link + * #MEDIA_TYPE_PLAYLIST}, {@link #MEDIA_TYPE_YEAR}, {@link #MEDIA_TYPE_AUDIO_BOOK}, {@link + * #MEDIA_TYPE_PODCAST}, {@link #MEDIA_TYPE_TV_CHANNEL}, {@link #MEDIA_TYPE_TV_SERIES}, {@link + * #MEDIA_TYPE_TV_SEASON}, {@link #MEDIA_TYPE_FOLDER_MIXED}, {@link #MEDIA_TYPE_FOLDER_ALBUMS}, + * {@link #MEDIA_TYPE_FOLDER_ARTISTS}, {@link #MEDIA_TYPE_FOLDER_GENRES}, {@link + * #MEDIA_TYPE_FOLDER_PLAYLISTS}, {@link #MEDIA_TYPE_FOLDER_YEARS}, {@link + * #MEDIA_TYPE_FOLDER_AUDIO_BOOKS}, {@link #MEDIA_TYPE_FOLDER_PODCASTS}, {@link + * #MEDIA_TYPE_FOLDER_TV_CHANNELS}, {@link #MEDIA_TYPE_FOLDER_TV_SERIES}, {@link + * #MEDIA_TYPE_FOLDER_TV_SHOWS}, {@link #MEDIA_TYPE_FOLDER_RADIO_STATIONS}, {@link + * #MEDIA_TYPE_FOLDER_NEWS}, {@link #MEDIA_TYPE_FOLDER_VIDEOS}, {@link + * #MEDIA_TYPE_FOLDER_TRAILERS} or {@link #MEDIA_TYPE_FOLDER_MOVIES}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @UnstableApi + @IntDef({ + MEDIA_TYPE_MIXED, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_AUDIO_BOOK_CHAPTER, + MEDIA_TYPE_PODCAST_EPISODE, + MEDIA_TYPE_RADIO_STATION, + MEDIA_TYPE_NEWS, + MEDIA_TYPE_VIDEO, + MEDIA_TYPE_TRAILER, + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_TV_SHOW, + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_GENRE, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_YEAR, + MEDIA_TYPE_AUDIO_BOOK, + MEDIA_TYPE_PODCAST, + MEDIA_TYPE_TV_CHANNEL, + MEDIA_TYPE_TV_SERIES, + MEDIA_TYPE_TV_SEASON, + MEDIA_TYPE_FOLDER_MIXED, + MEDIA_TYPE_FOLDER_ALBUMS, + MEDIA_TYPE_FOLDER_ARTISTS, + MEDIA_TYPE_FOLDER_GENRES, + MEDIA_TYPE_FOLDER_PLAYLISTS, + MEDIA_TYPE_FOLDER_YEARS, + MEDIA_TYPE_FOLDER_AUDIO_BOOKS, + MEDIA_TYPE_FOLDER_PODCASTS, + MEDIA_TYPE_FOLDER_TV_CHANNELS, + MEDIA_TYPE_FOLDER_TV_SERIES, + MEDIA_TYPE_FOLDER_TV_SHOWS, + MEDIA_TYPE_FOLDER_RADIO_STATIONS, + MEDIA_TYPE_FOLDER_NEWS, + MEDIA_TYPE_FOLDER_VIDEOS, + MEDIA_TYPE_FOLDER_TRAILERS, + MEDIA_TYPE_FOLDER_MOVIES, + }) + public @interface MediaType {} + + /** Media of undetermined type or a mix of multiple {@linkplain MediaType media types}. */ + @UnstableApi public static final int MEDIA_TYPE_MIXED = 0; + /** {@link MediaType} for music. */ + @UnstableApi public static final int MEDIA_TYPE_MUSIC = 1; + /** {@link MediaType} for an audio book chapter. */ + @UnstableApi public static final int MEDIA_TYPE_AUDIO_BOOK_CHAPTER = 2; + /** {@link MediaType} for a podcast episode. */ + @UnstableApi public static final int MEDIA_TYPE_PODCAST_EPISODE = 3; + /** {@link MediaType} for a radio station. */ + @UnstableApi public static final int MEDIA_TYPE_RADIO_STATION = 4; + /** {@link MediaType} for news. */ + @UnstableApi public static final int MEDIA_TYPE_NEWS = 5; + /** {@link MediaType} for a video. */ + @UnstableApi public static final int MEDIA_TYPE_VIDEO = 6; + /** {@link MediaType} for a movie trailer. */ + @UnstableApi public static final int MEDIA_TYPE_TRAILER = 7; + /** {@link MediaType} for a movie. */ + @UnstableApi public static final int MEDIA_TYPE_MOVIE = 8; + /** {@link MediaType} for a TV show. */ + @UnstableApi public static final int MEDIA_TYPE_TV_SHOW = 9; + /** + * {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) belonging to an + * album. + */ + @UnstableApi public static final int MEDIA_TYPE_ALBUM = 10; + /** + * {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) from the same + * artist. + */ + @UnstableApi public static final int MEDIA_TYPE_ARTIST = 11; + /** + * {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) of the same + * genre. + */ + @UnstableApi public static final int MEDIA_TYPE_GENRE = 12; + /** + * {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) forming a + * playlist. + */ + @UnstableApi public static final int MEDIA_TYPE_PLAYLIST = 13; + /** + * {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) from the same + * year. + */ + @UnstableApi public static final int MEDIA_TYPE_YEAR = 14; + /** + * {@link MediaType} for a group of items forming an audio book. Items in this group are typically + * of type {@link #MEDIA_TYPE_AUDIO_BOOK_CHAPTER}. + */ + @UnstableApi public static final int MEDIA_TYPE_AUDIO_BOOK = 15; + /** + * {@link MediaType} for a group of items belonging to a podcast. Items in this group are + * typically of type {@link #MEDIA_TYPE_PODCAST_EPISODE}. + */ + @UnstableApi public static final int MEDIA_TYPE_PODCAST = 16; + /** + * {@link MediaType} for a group of items that are part of a TV channel. Items in this group are + * typically of type {@link #MEDIA_TYPE_TV_SHOW}, {@link #MEDIA_TYPE_TV_SERIES} or {@link + * #MEDIA_TYPE_MOVIE}. + */ + @UnstableApi public static final int MEDIA_TYPE_TV_CHANNEL = 17; + /** + * {@link MediaType} for a group of items that are part of a TV series. Items in this group are + * typically of type {@link #MEDIA_TYPE_TV_SHOW} or {@link #MEDIA_TYPE_TV_SEASON}. + */ + @UnstableApi public static final int MEDIA_TYPE_TV_SERIES = 18; + /** + * {@link MediaType} for a group of items that are part of a TV series. Items in this group are + * typically of type {@link #MEDIA_TYPE_TV_SHOW}. + */ + @UnstableApi public static final int MEDIA_TYPE_TV_SEASON = 19; + /** {@link MediaType} for a folder with mixed or undetermined content. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_MIXED = 20; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_ALBUM albums}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_ALBUMS = 21; + /** {@link MediaType} for a folder containing {@linkplain #FIELD_ARTIST artists}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_ARTISTS = 22; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_GENRE genres}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_GENRES = 23; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_PLAYLIST playlists}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_PLAYLISTS = 24; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_YEAR years}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_YEARS = 25; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_AUDIO_BOOK audio books}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_AUDIO_BOOKS = 26; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_PODCAST podcasts}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_PODCASTS = 27; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_CHANNEL TV channels}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_TV_CHANNELS = 28; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_SERIES TV series}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_TV_SERIES = 29; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_SHOW TV shows}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_TV_SHOWS = 30; + /** + * {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_RADIO_STATION radio + * stations}. + */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_RADIO_STATIONS = 31; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_NEWS news}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_NEWS = 32; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_VIDEO videos}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_VIDEOS = 33; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TRAILER movie trailers}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_TRAILERS = 34; + /** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_MOVIE movies}. */ + @UnstableApi public static final int MEDIA_TYPE_FOLDER_MOVIES = 35; + /** * The folder type of the media item. * *

    This can be used as the type of a browsable bluetooth folder (see section 6.10.2.2 of the Bluetooth * AVRCP 1.6.2). + * + *

    One of {@link #FOLDER_TYPE_NONE}, {@link #FOLDER_TYPE_MIXED}, {@link #FOLDER_TYPE_TITLES}, + * {@link #FOLDER_TYPE_ALBUMS}, {@link #FOLDER_TYPE_ARTISTS}, {@link #FOLDER_TYPE_GENRES}, {@link + * #FOLDER_TYPE_PLAYLISTS} or {@link #FOLDER_TYPE_YEARS}. */ // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility // with Kotlin usages from before TYPE_USE was added. @@ -588,6 +775,17 @@ public MediaMetadata build() { * *

    Values sourced from the ID3 v2.4 specification (See section 4.14 of * https://id3.org/id3v2.4.0-frames). + * + *

    One of {@link #PICTURE_TYPE_OTHER}, {@link #PICTURE_TYPE_FILE_ICON}, {@link + * #PICTURE_TYPE_FILE_ICON_OTHER}, {@link #PICTURE_TYPE_FRONT_COVER}, {@link + * #PICTURE_TYPE_BACK_COVER}, {@link #PICTURE_TYPE_LEAFLET_PAGE}, {@link #PICTURE_TYPE_MEDIA}, + * {@link #PICTURE_TYPE_LEAD_ARTIST_PERFORMER}, {@link #PICTURE_TYPE_ARTIST_PERFORMER}, {@link + * #PICTURE_TYPE_CONDUCTOR}, {@link #PICTURE_TYPE_BAND_ORCHESTRA}, {@link #PICTURE_TYPE_COMPOSER}, + * {@link #PICTURE_TYPE_LYRICIST}, {@link #PICTURE_TYPE_RECORDING_LOCATION}, {@link + * #PICTURE_TYPE_DURING_RECORDING}, {@link #PICTURE_TYPE_DURING_PERFORMANCE}, {@link + * #PICTURE_TYPE_MOVIE_VIDEO_SCREEN_CAPTURE}, {@link #PICTURE_TYPE_A_BRIGHT_COLORED_FISH}, {@link + * #PICTURE_TYPE_ILLUSTRATION}, {@link #PICTURE_TYPE_BAND_ARTIST_LOGO} or {@link + * #PICTURE_TYPE_PUBLISHER_STUDIO_LOGO}. */ // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility // with Kotlin usages from before TYPE_USE was added. @@ -729,6 +927,8 @@ public MediaMetadata build() { @Nullable public final CharSequence compilation; /** Optional name of the station streaming the media. */ @Nullable public final CharSequence station; + /** Optional {@link MediaType}. */ + @UnstableApi @Nullable public final @MediaType Integer mediaType; /** * Optional extras {@link Bundle}. @@ -770,6 +970,7 @@ private MediaMetadata(Builder builder) { this.genre = builder.genre; this.compilation = builder.compilation; this.station = builder.station; + this.mediaType = builder.mediaType; this.extras = builder.extras; } @@ -816,7 +1017,8 @@ public boolean equals(@Nullable Object obj) { && Util.areEqual(totalDiscCount, that.totalDiscCount) && Util.areEqual(genre, that.genre) && Util.areEqual(compilation, that.compilation) - && Util.areEqual(station, that.station); + && Util.areEqual(station, that.station) + && Util.areEqual(mediaType, that.mediaType); } @Override @@ -851,7 +1053,8 @@ public int hashCode() { totalDiscCount, genre, compilation, - station); + station, + mediaType); } // Bundleable implementation. @@ -891,7 +1094,8 @@ public int hashCode() { FIELD_GENRE, FIELD_COMPILATION, FIELD_STATION, - FIELD_EXTRAS + FIELD_MEDIA_TYPE, + FIELD_EXTRAS, }) private @interface FieldNumber {} @@ -926,6 +1130,7 @@ public int hashCode() { private static final int FIELD_COMPILATION = 28; private static final int FIELD_ARTWORK_DATA_TYPE = 29; private static final int FIELD_STATION = 30; + private static final int FIELD_MEDIA_TYPE = 31; private static final int FIELD_EXTRAS = 1000; @UnstableApi @@ -993,6 +1198,9 @@ public Bundle toBundle() { if (artworkDataType != null) { bundle.putInt(keyForField(FIELD_ARTWORK_DATA_TYPE), artworkDataType); } + if (mediaType != null) { + bundle.putInt(keyForField(FIELD_MEDIA_TYPE), mediaType); + } if (extras != null) { bundle.putBundle(keyForField(FIELD_EXTRAS), extras); } @@ -1074,6 +1282,9 @@ private static MediaMetadata fromBundle(Bundle bundle) { if (bundle.containsKey(keyForField(FIELD_TOTAL_DISC_COUNT))) { builder.setTotalDiscCount(bundle.getInt(keyForField(FIELD_TOTAL_DISC_COUNT))); } + if (bundle.containsKey(keyForField(FIELD_MEDIA_TYPE))) { + builder.setMediaType(bundle.getInt(keyForField(FIELD_MEDIA_TYPE))); + } return builder.build(); } diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java index 7e606597c4d..4d66cd922a2 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java @@ -64,6 +64,7 @@ public void builder_minimal_correctDefaults() { assertThat(mediaMetadata.genre).isNull(); assertThat(mediaMetadata.compilation).isNull(); assertThat(mediaMetadata.station).isNull(); + assertThat(mediaMetadata.mediaType).isNull(); assertThat(mediaMetadata.extras).isNull(); } @@ -149,6 +150,7 @@ private static MediaMetadata getFullyPopulatedMediaMetadata() { .setGenre("Pop") .setCompilation("Amazing songs.") .setStation("radio station") + .setMediaType(MediaMetadata.MEDIA_TYPE_MIXED) .setExtras(extras) .build(); } From f8155f1cd4ce2b8917ce28db06a10fe76c2b1585 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 1 Dec 2022 08:34:14 +0000 Subject: [PATCH 046/141] Add support for most setters in SimpleBasePlayer This adds the forwarding logic for most setters in SimpleExoPlayer in the same style as the existing logic for setPlayWhenReady. This change doesn't implement the setters for modifying media items, seeking and releasing yet as they require additional handling that goes beyond the repeated implementation pattern in this change. PiperOrigin-RevId: 492124399 (cherry picked from commit f007238745850791f8521e61f6adaf8ed2467c45) --- .../media3/common/SimpleBasePlayer.java | 493 +++++- .../androidx/media3/common/util/Size.java | 3 + .../media3/common/SimpleBasePlayerTest.java | 1511 ++++++++++++++++- 3 files changed, 1947 insertions(+), 60 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index acb6b2c3975..f42e912fc57 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -21,6 +21,7 @@ import static androidx.media3.common.util.Util.usToMs; import static java.lang.Math.max; +import android.graphics.Rect; import android.os.Looper; import android.os.SystemClock; import android.util.Pair; @@ -2036,6 +2037,7 @@ public final Commands getAvailableCommands() { @Override public final void setPlayWhenReady(boolean playWhenReady) { verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; if (!state.availableCommands.contains(Player.COMMAND_PLAY_PAUSE)) { return; @@ -2088,8 +2090,20 @@ public final void removeMediaItems(int fromIndex, int toIndex) { @Override public final void prepare() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_PREPARE)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handlePrepare(), + /* placeholderStateSupplier= */ () -> + state + .buildUpon() + .setPlayerError(null) + .setPlaybackState(state.timeline.isEmpty() ? STATE_ENDED : STATE_BUFFERING) + .build()); } @Override @@ -2114,8 +2128,15 @@ public final PlaybackException getPlayerError() { @Override public final void setRepeatMode(@Player.RepeatMode int repeatMode) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_REPEAT_MODE)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetRepeatMode(repeatMode), + /* placeholderStateSupplier= */ () -> state.buildUpon().setRepeatMode(repeatMode).build()); } @Override @@ -2127,8 +2148,16 @@ public final int getRepeatMode() { @Override public final void setShuffleModeEnabled(boolean shuffleModeEnabled) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_SHUFFLE_MODE)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetShuffleModeEnabled(shuffleModeEnabled), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setShuffleModeEnabled(shuffleModeEnabled).build()); } @Override @@ -2149,6 +2178,12 @@ public final void seekTo(int mediaItemIndex, long positionMs) { throw new IllegalStateException(); } + @Override + protected final void repeatCurrentMediaItem() { + // TODO: implement. + throw new IllegalStateException(); + } + @Override public final long getSeekBackIncrement() { verifyApplicationThreadAndInitState(); @@ -2169,8 +2204,16 @@ public final long getMaxSeekToPreviousPosition() { @Override public final void setPlaybackParameters(PlaybackParameters playbackParameters) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_SPEED_AND_PITCH)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetPlaybackParameters(playbackParameters), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setPlaybackParameters(playbackParameters).build()); } @Override @@ -2181,14 +2224,30 @@ public final PlaybackParameters getPlaybackParameters() { @Override public final void stop() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_STOP)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleStop(), + /* placeholderStateSupplier= */ () -> + state + .buildUpon() + .setPlaybackState(Player.STATE_IDLE) + .setTotalBufferedDurationMs(PositionSupplier.ZERO) + .setContentBufferedPositionMs(state.contentPositionMsSupplier) + .setAdBufferedPositionMs(state.adPositionMsSupplier) + .build()); } @Override public final void stop(boolean reset) { - // TODO: implement. - throw new IllegalStateException(); + stop(); + if (reset) { + clearMediaItems(); + } } @Override @@ -2211,8 +2270,16 @@ public final TrackSelectionParameters getTrackSelectionParameters() { @Override public final void setTrackSelectionParameters(TrackSelectionParameters parameters) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetTrackSelectionParameters(parameters), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setTrackSelectionParameters(parameters).build()); } @Override @@ -2229,8 +2296,16 @@ public final MediaMetadata getPlaylistMetadata() { @Override public final void setPlaylistMetadata(MediaMetadata mediaMetadata) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_MEDIA_ITEMS_METADATA)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetPlaylistMetadata(mediaMetadata), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setPlaylistMetadata(mediaMetadata).build()); } @Override @@ -2322,8 +2397,15 @@ public final AudioAttributes getAudioAttributes() { @Override public final void setVolume(float volume) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VOLUME)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVolume(volume), + /* placeholderStateSupplier= */ () -> state.buildUpon().setVolume(volume).build()); } @Override @@ -2333,57 +2415,121 @@ public final float getVolume() { } @Override - public final void clearVideoSurface() { - // TODO: implement. - throw new IllegalStateException(); + public final void setVideoSurface(@Nullable Surface surface) { + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + return; + } + if (surface == null) { + clearVideoSurface(); + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVideoOutput(surface), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setSurfaceSize(Size.UNKNOWN).build()); } @Override - public final void clearVideoSurface(@Nullable Surface surface) { - // TODO: implement. - throw new IllegalStateException(); + public final void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + return; + } + if (surfaceHolder == null) { + clearVideoSurface(); + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVideoOutput(surfaceHolder), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setSurfaceSize(getSurfaceHolderSize(surfaceHolder)).build()); } @Override - public final void setVideoSurface(@Nullable Surface surface) { - // TODO: implement. - throw new IllegalStateException(); + public final void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + return; + } + if (surfaceView == null) { + clearVideoSurface(); + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVideoOutput(surfaceView), + /* placeholderStateSupplier= */ () -> + state + .buildUpon() + .setSurfaceSize(getSurfaceHolderSize(surfaceView.getHolder())) + .build()); } @Override - public final void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { - // TODO: implement. - throw new IllegalStateException(); + public final void setVideoTextureView(@Nullable TextureView textureView) { + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + return; + } + if (textureView == null) { + clearVideoSurface(); + return; + } + Size surfaceSize; + if (textureView.isAvailable()) { + surfaceSize = new Size(textureView.getWidth(), textureView.getHeight()); + } else { + surfaceSize = Size.ZERO; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVideoOutput(textureView), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setSurfaceSize(surfaceSize).build()); } @Override - public final void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { - // TODO: implement. - throw new IllegalStateException(); + public final void clearVideoSurface() { + clearVideoOutput(/* videoOutput= */ null); } @Override - public final void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { - // TODO: implement. - throw new IllegalStateException(); + public final void clearVideoSurface(@Nullable Surface surface) { + clearVideoOutput(surface); } @Override - public final void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) { - // TODO: implement. - throw new IllegalStateException(); + public final void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { + clearVideoOutput(surfaceHolder); } @Override - public final void setVideoTextureView(@Nullable TextureView textureView) { - // TODO: implement. - throw new IllegalStateException(); + public final void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) { + clearVideoOutput(surfaceView); } @Override public final void clearVideoTextureView(@Nullable TextureView textureView) { - // TODO: implement. - throw new IllegalStateException(); + clearVideoOutput(textureView); + } + + private void clearVideoOutput(@Nullable Object videoOutput) { + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleClearVideoOutput(videoOutput), + /* placeholderStateSupplier= */ () -> state.buildUpon().setSurfaceSize(Size.ZERO).build()); } @Override @@ -2424,26 +2570,56 @@ public final boolean isDeviceMuted() { @Override public final void setDeviceVolume(int volume) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_DEVICE_VOLUME)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetDeviceVolume(volume), + /* placeholderStateSupplier= */ () -> state.buildUpon().setDeviceVolume(volume).build()); } @Override public final void increaseDeviceVolume() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleIncreaseDeviceVolume(), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setDeviceVolume(state.deviceVolume + 1).build()); } @Override public final void decreaseDeviceVolume() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleDecreaseDeviceVolume(), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setDeviceVolume(max(0, state.deviceVolume - 1)).build()); } @Override public final void setDeviceMuted(boolean muted) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetDeviceMuted(muted), + /* placeholderStateSupplier= */ () -> state.buildUpon().setIsDeviceMuted(muted).build()); } /** @@ -2497,22 +2673,217 @@ protected State getPlaceholderState(State suggestedPlaceholderState) { } /** - * Handles calls to set {@link State#playWhenReady}. + * Handles calls to {@link Player#setPlayWhenReady}, {@link Player#play} and {@link Player#pause}. * - *

    Will only be called if {@link Player.Command#COMMAND_PLAY_PAUSE} is available. + *

    Will only be called if {@link Player#COMMAND_PLAY_PAUSE} is available. * * @param playWhenReady The requested {@link State#playWhenReady} * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} * changes caused by this call. - * @see Player#setPlayWhenReady(boolean) - * @see Player#play() - * @see Player#pause() */ @ForOverride protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { throw new IllegalStateException(); } + /** + * Handles calls to {@link Player#prepare}. + * + *

    Will only be called if {@link Player#COMMAND_PREPARE} is available. + * + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handlePrepare() { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#stop}. + * + *

    Will only be called if {@link Player#COMMAND_STOP} is available. + * + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleStop() { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setRepeatMode}. + * + *

    Will only be called if {@link Player#COMMAND_SET_REPEAT_MODE} is available. + * + * @param repeatMode The requested {@link RepeatMode}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetRepeatMode(@RepeatMode int repeatMode) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setShuffleModeEnabled}. + * + *

    Will only be called if {@link Player#COMMAND_SET_SHUFFLE_MODE} is available. + * + * @param shuffleModeEnabled Whether shuffle mode was requested to be enabled. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEnabled) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setPlaybackParameters} or {@link Player#setPlaybackSpeed}. + * + *

    Will only be called if {@link Player#COMMAND_SET_SPEED_AND_PITCH} is available. + * + * @param playbackParameters The requested {@link PlaybackParameters}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetPlaybackParameters(PlaybackParameters playbackParameters) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setTrackSelectionParameters}. + * + *

    Will only be called if {@link Player#COMMAND_SET_TRACK_SELECTION_PARAMETERS} is available. + * + * @param trackSelectionParameters The requested {@link TrackSelectionParameters}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetTrackSelectionParameters( + TrackSelectionParameters trackSelectionParameters) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setPlaylistMetadata}. + * + *

    Will only be called if {@link Player#COMMAND_SET_MEDIA_ITEMS_METADATA} is available. + * + * @param playlistMetadata The requested {@linkplain MediaMetadata playlist metadata}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetPlaylistMetadata(MediaMetadata playlistMetadata) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setVolume}. + * + *

    Will only be called if {@link Player#COMMAND_SET_VOLUME} is available. + * + * @param volume The requested audio volume, with 0 being silence and 1 being unity gain (signal + * unchanged). + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetVolume(@FloatRange(from = 0, to = 1.0) float volume) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setDeviceVolume}. + * + *

    Will only be called if {@link Player#COMMAND_SET_DEVICE_VOLUME} is available. + * + * @param deviceVolume The requested device volume. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetDeviceVolume(@IntRange(from = 0) int deviceVolume) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#increaseDeviceVolume()}. + * + *

    Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available. + * + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleIncreaseDeviceVolume() { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#decreaseDeviceVolume()}. + * + *

    Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available. + * + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleDecreaseDeviceVolume() { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setDeviceMuted}. + * + *

    Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available. + * + * @param muted Whether the device was requested to be muted. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetDeviceMuted(boolean muted) { + throw new IllegalStateException(); + } + + /** + * Handles calls to set the video output. + * + *

    Will only be called if {@link Player#COMMAND_SET_VIDEO_SURFACE} is available. + * + * @param videoOutput The requested video output. This is either a {@link Surface}, {@link + * SurfaceHolder}, {@link TextureView} or {@link SurfaceView}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetVideoOutput(Object videoOutput) { + throw new IllegalStateException(); + } + + /** + * Handles calls to clear the video output. + * + *

    Will only be called if {@link Player#COMMAND_SET_VIDEO_SURFACE} is available. + * + * @param videoOutput The video output to clear. If null any current output should be cleared. If + * non-null, the output should only be cleared if it matches the provided argument. This is + * either a {@link Surface}, {@link SurfaceHolder}, {@link TextureView} or {@link + * SurfaceView}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutput) { + throw new IllegalStateException(); + } + @SuppressWarnings("deprecation") // Calling deprecated listener methods. @RequiresNonNull("state") private void updateStateAndInformListeners(State newState) { @@ -2971,4 +3342,12 @@ private static int getMediaItemTransitionReason( } return C.INDEX_UNSET; } + + private static Size getSurfaceHolderSize(SurfaceHolder surfaceHolder) { + if (!surfaceHolder.getSurface().isValid()) { + return Size.ZERO; + } + Rect surfaceFrame = surfaceHolder.getSurfaceFrame(); + return new Size(surfaceFrame.width(), surfaceFrame.height()); + } } diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Size.java b/libraries/common/src/main/java/androidx/media3/common/util/Size.java index dddb834edd4..5ffaac59113 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Size.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Size.java @@ -29,6 +29,9 @@ public final class Size { public static final Size UNKNOWN = new Size(/* width= */ C.LENGTH_UNSET, /* height= */ C.LENGTH_UNSET); + /* A static instance to represent a size of zero height and width. */ + public static final Size ZERO = new Size(/* width= */ 0, /* height= */ 0); + private final int width; private final int height; diff --git a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java index d8a0e336d02..dc306838b5a 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -27,8 +27,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import android.graphics.SurfaceTexture; import android.os.Looper; import android.os.SystemClock; +import android.view.Surface; +import androidx.annotation.Nullable; import androidx.media3.common.Player.Commands; import androidx.media3.common.Player.Listener; import androidx.media3.common.SimpleBasePlayer.State; @@ -36,6 +39,7 @@ import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Size; import androidx.media3.test.utils.FakeMetadataEntry; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; @@ -1826,17 +1830,18 @@ public void setPlayWhenReady_immediateHandling_updatesStateAndInformsListeners() .setPlayWhenReady( /* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE) .build(); - AtomicBoolean stateUpdated = new AtomicBoolean(); SimpleBasePlayer player = new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + @Override protected State getState() { - return stateUpdated.get() ? updatedState : state; + return playerState; } @Override protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { - stateUpdated.set(true); + playerState = updatedState; return Futures.immediateVoidFuture(); } }; @@ -1934,6 +1939,1506 @@ protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { assertThat(callForwarded.get()).isFalse(); } + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void prepare_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaybackState(Player.STATE_IDLE) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .build(); + State updatedState = state.buildUpon().setPlaybackState(Player.STATE_READY).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handlePrepare() { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.prepare(); + + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_READY); + verify(listener).onPlaybackStateChanged(Player.STATE_READY); + verify(listener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_READY); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void prepare_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaybackState(Player.STATE_IDLE) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .build(); + State updatedState = state.buildUpon().setPlaybackState(Player.STATE_READY).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handlePrepare() { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.prepare(); + + // Verify placeholder state and listener calls. + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); + verify(listener).onPlaybackStateChanged(Player.STATE_BUFFERING); + verify(listener) + .onPlayerStateChanged( + /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_READY); + verify(listener).onPlaybackStateChanged(Player.STATE_READY); + verify(listener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_READY); + verifyNoMoreInteractions(listener); + } + + @Test + public void prepare_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_PREPARE).build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handlePrepare() { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.prepare(); + + assertThat(callForwarded.get()).isFalse(); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void stop_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaybackState(Player.STATE_READY) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .build(); + State updatedState = state.buildUpon().setPlaybackState(Player.STATE_IDLE).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleStop() { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.stop(); + + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + verify(listener).onPlaybackStateChanged(Player.STATE_IDLE); + verify(listener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void stop_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaybackState(Player.STATE_READY) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .build(); + // Additionally set the repeat mode to see a difference between the placeholder and new state. + State updatedState = + state + .buildUpon() + .setPlaybackState(Player.STATE_IDLE) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleStop() { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.stop(); + + // Verify placeholder state and listener calls. + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF); + verify(listener).onPlaybackStateChanged(Player.STATE_IDLE); + verify(listener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verifyNoMoreInteractions(listener); + } + + @Test + public void stop_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_STOP).build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleStop() { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.stop(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setRepeatMode_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = state.buildUpon().setRepeatMode(Player.REPEAT_MODE_ALL).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetRepeatMode(@Player.RepeatMode int repeatMode) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setRepeatMode(Player.REPEAT_MODE_ONE); + + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verifyNoMoreInteractions(listener); + } + + @Test + public void setRepeatMode_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a new repeat mode to see a difference between the placeholder and new state. + State updatedState = state.buildUpon().setRepeatMode(Player.REPEAT_MODE_ALL).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetRepeatMode(@Player.RepeatMode int repeatMode) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setRepeatMode(Player.REPEAT_MODE_ONE); + + // Verify placeholder state and listener calls. + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verifyNoMoreInteractions(listener); + } + + @Test + public void setRepeatMode_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_REPEAT_MODE) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetRepeatMode(@Player.RepeatMode int repeatMode) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setRepeatMode(Player.REPEAT_MODE_ONE); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setShuffleModeEnabled_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Also change the repeat mode to ensure the updated state is used. + State updatedState = + state.buildUpon().setShuffleModeEnabled(true).setRepeatMode(Player.REPEAT_MODE_ALL).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEnabled) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setShuffleModeEnabled(true); + + assertThat(player.getShuffleModeEnabled()).isTrue(); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + verify(listener).onShuffleModeEnabledChanged(true); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verifyNoMoreInteractions(listener); + } + + @Test + public void setShuffleModeEnabled_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + // Always return the same state to revert the shuffle mode change. This allows to see a + // difference between the placeholder and new state. + return state; + } + + @Override + protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEnabled) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setShuffleModeEnabled(true); + + // Verify placeholder state and listener calls. + assertThat(player.getShuffleModeEnabled()).isTrue(); + verify(listener).onShuffleModeEnabledChanged(true); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getShuffleModeEnabled()).isFalse(); + verify(listener).onShuffleModeEnabledChanged(false); + verifyNoMoreInteractions(listener); + } + + @Test + public void setShuffleModeEnabled_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_SHUFFLE_MODE) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEnabled) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setShuffleModeEnabled(true); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setPlaybackParameters_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = + state.buildUpon().setPlaybackParameters(new PlaybackParameters(/* speed= */ 3f)).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetPlaybackParameters( + PlaybackParameters playbackParameters) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)); + + assertThat(player.getPlaybackParameters()).isEqualTo(new PlaybackParameters(/* speed= */ 3f)); + verify(listener).onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 3f)); + verifyNoMoreInteractions(listener); + } + + @Test + public void setPlaybackParameters_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a new repeat mode to see a difference between the placeholder and new state. + State updatedState = + state.buildUpon().setPlaybackParameters(new PlaybackParameters(/* speed= */ 3f)).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetPlaybackParameters( + PlaybackParameters playbackParameters) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)); + + // Verify placeholder state and listener calls. + assertThat(player.getPlaybackParameters()).isEqualTo(new PlaybackParameters(/* speed= */ 2f)); + verify(listener).onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 2f)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getPlaybackParameters()).isEqualTo(new PlaybackParameters(/* speed= */ 3f)); + verify(listener).onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 3f)); + verifyNoMoreInteractions(listener); + } + + @Test + public void setPlaybackParameters_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_SPEED_AND_PITCH) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetPlaybackParameters( + PlaybackParameters playbackParameters) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setTrackSelectionParameters_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + TrackSelectionParameters updatedParameters = + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxVideoBitrate(3000) + .build(); + State updatedState = state.buildUpon().setTrackSelectionParameters(updatedParameters).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetTrackSelectionParameters( + TrackSelectionParameters trackSelectionParameters) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setTrackSelectionParameters( + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxVideoBitrate(1000) + .build()); + + assertThat(player.getTrackSelectionParameters()).isEqualTo(updatedParameters); + verify(listener).onTrackSelectionParametersChanged(updatedParameters); + verifyNoMoreInteractions(listener); + } + + @Test + public void setTrackSelectionParameters_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set new parameters to see a difference between the placeholder and new state. + TrackSelectionParameters updatedParameters = + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxVideoBitrate(3000) + .build(); + State updatedState = state.buildUpon().setTrackSelectionParameters(updatedParameters).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetTrackSelectionParameters( + TrackSelectionParameters trackSelectionParameters) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + TrackSelectionParameters requestedParameters = + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxVideoBitrate(3000) + .build(); + player.setTrackSelectionParameters(requestedParameters); + + // Verify placeholder state and listener calls. + assertThat(player.getTrackSelectionParameters()).isEqualTo(requestedParameters); + verify(listener).onTrackSelectionParametersChanged(requestedParameters); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getTrackSelectionParameters()).isEqualTo(updatedParameters); + verify(listener).onTrackSelectionParametersChanged(updatedParameters); + verifyNoMoreInteractions(listener); + } + + @Test + public void setTrackSelectionParameters_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetTrackSelectionParameters( + TrackSelectionParameters trackSelectionParameters) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setTrackSelectionParameters( + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxVideoBitrate(1000) + .build()); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setPlaylistMetadata_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + MediaMetadata updatedMetadata = new MediaMetadata.Builder().setArtist("artist").build(); + State updatedState = state.buildUpon().setPlaylistMetadata(updatedMetadata).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetPlaylistMetadata(MediaMetadata playlistMetadata) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setPlaylistMetadata(new MediaMetadata.Builder().setTitle("title").build()); + + assertThat(player.getPlaylistMetadata()).isEqualTo(updatedMetadata); + verify(listener).onPlaylistMetadataChanged(updatedMetadata); + verifyNoMoreInteractions(listener); + } + + @Test + public void setPlaylistMetadata_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set new metadata to see a difference between the placeholder and new state. + MediaMetadata updatedMetadata = new MediaMetadata.Builder().setArtist("artist").build(); + State updatedState = state.buildUpon().setPlaylistMetadata(updatedMetadata).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetPlaylistMetadata(MediaMetadata playlistMetadata) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + MediaMetadata requestedMetadata = new MediaMetadata.Builder().setTitle("title").build(); + player.setPlaylistMetadata(requestedMetadata); + + // Verify placeholder state and listener calls. + assertThat(player.getPlaylistMetadata()).isEqualTo(requestedMetadata); + verify(listener).onPlaylistMetadataChanged(requestedMetadata); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getPlaylistMetadata()).isEqualTo(updatedMetadata); + verify(listener).onPlaylistMetadataChanged(updatedMetadata); + verifyNoMoreInteractions(listener); + } + + @Test + public void setPlaylistMetadata_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_MEDIA_ITEMS_METADATA) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetPlaylistMetadata(MediaMetadata playlistMetadata) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setPlaylistMetadata(new MediaMetadata.Builder().setTitle("title").build()); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setVolume_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = state.buildUpon().setVolume(.8f).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetVolume(float volume) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setVolume(.5f); + + assertThat(player.getVolume()).isEqualTo(.8f); + verify(listener).onVolumeChanged(.8f); + verifyNoMoreInteractions(listener); + } + + @Test + public void setVolume_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a new volume to see a difference between the placeholder and new state. + State updatedState = state.buildUpon().setVolume(.8f).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetVolume(float volume) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setVolume(.5f); + + // Verify placeholder state and listener calls. + assertThat(player.getVolume()).isEqualTo(.5f); + verify(listener).onVolumeChanged(.5f); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getVolume()).isEqualTo(.8f); + verify(listener).onVolumeChanged(.8f); + verifyNoMoreInteractions(listener); + } + + @Test + public void setVolume_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_SET_VOLUME).build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetVolume(float volume) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setVolume(.5f); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setDeviceVolume_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = state.buildUpon().setDeviceVolume(6).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetDeviceVolume(int volume) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setDeviceVolume(3); + + assertThat(player.getDeviceVolume()).isEqualTo(6); + verify(listener).onDeviceVolumeChanged(6, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void setDeviceVolume_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a new volume to see a difference between the placeholder and new state. + State updatedState = state.buildUpon().setDeviceVolume(6).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetDeviceVolume(int volume) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setDeviceVolume(3); + + // Verify placeholder state and listener calls. + assertThat(player.getDeviceVolume()).isEqualTo(3); + verify(listener).onDeviceVolumeChanged(3, /* muted= */ false); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getDeviceVolume()).isEqualTo(6); + verify(listener).onDeviceVolumeChanged(6, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void setDeviceVolume_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_DEVICE_VOLUME) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetDeviceVolume(int volume) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setDeviceVolume(3); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void increaseDeviceVolume_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setDeviceVolume(3) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = state.buildUpon().setDeviceVolume(6).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleIncreaseDeviceVolume() { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.increaseDeviceVolume(); + + assertThat(player.getDeviceVolume()).isEqualTo(6); + verify(listener).onDeviceVolumeChanged(6, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void increaseDeviceVolume_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setDeviceVolume(3) + .build(); + // Set a new volume to see a difference between the placeholder and new state. + State updatedState = state.buildUpon().setDeviceVolume(6).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleIncreaseDeviceVolume() { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.increaseDeviceVolume(); + + // Verify placeholder state and listener calls. + assertThat(player.getDeviceVolume()).isEqualTo(4); + verify(listener).onDeviceVolumeChanged(4, /* muted= */ false); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getDeviceVolume()).isEqualTo(6); + verify(listener).onDeviceVolumeChanged(6, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void increaseDeviceVolume_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_ADJUST_DEVICE_VOLUME) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleIncreaseDeviceVolume() { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.increaseDeviceVolume(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void decreaseDeviceVolume_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setDeviceVolume(3) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = state.buildUpon().setDeviceVolume(1).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleDecreaseDeviceVolume() { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.decreaseDeviceVolume(); + + assertThat(player.getDeviceVolume()).isEqualTo(1); + verify(listener).onDeviceVolumeChanged(1, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void decreaseDeviceVolume_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setDeviceVolume(3) + .build(); + // Set a new volume to see a difference between the placeholder and new state. + State updatedState = state.buildUpon().setDeviceVolume(1).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleDecreaseDeviceVolume() { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.decreaseDeviceVolume(); + + // Verify placeholder state and listener calls. + assertThat(player.getDeviceVolume()).isEqualTo(2); + verify(listener).onDeviceVolumeChanged(2, /* muted= */ false); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getDeviceVolume()).isEqualTo(1); + verify(listener).onDeviceVolumeChanged(1, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void decreaseDeviceVolume_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_ADJUST_DEVICE_VOLUME) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleDecreaseDeviceVolume() { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.decreaseDeviceVolume(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setDeviceMuted_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Also change the volume to ensure the updated state is used. + State updatedState = state.buildUpon().setIsDeviceMuted(true).setDeviceVolume(6).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetDeviceMuted(boolean muted) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setDeviceMuted(true); + + assertThat(player.isDeviceMuted()).isTrue(); + assertThat(player.getDeviceVolume()).isEqualTo(6); + verify(listener).onDeviceVolumeChanged(6, /* muted= */ true); + verifyNoMoreInteractions(listener); + } + + @Test + public void setDeviceMuted_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + // Always return the same state to revert the muted change. This allows to see a + // difference between the placeholder and new state. + return state; + } + + @Override + protected ListenableFuture handleSetDeviceMuted(boolean muted) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setDeviceMuted(true); + + // Verify placeholder state and listener calls. + assertThat(player.isDeviceMuted()).isTrue(); + verify(listener).onDeviceVolumeChanged(0, /* muted= */ true); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.isDeviceMuted()).isFalse(); + verify(listener).onDeviceVolumeChanged(0, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void setDeviceMuted_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_ADJUST_DEVICE_VOLUME) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetDeviceMuted(boolean muted) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setDeviceMuted(true); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setVideoSurface_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setSurfaceSize(Size.ZERO) + .build(); + Size updatedSize = new Size(/* width= */ 300, /* height= */ 200); + State updatedState = state.buildUpon().setSurfaceSize(updatedSize).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetVideoOutput(Object videoOutput) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setVideoSurface(new Surface(new SurfaceTexture(0))); + + assertThat(player.getSurfaceSize()).isEqualTo(updatedSize); + verify(listener).onSurfaceSizeChanged(updatedSize.getWidth(), updatedSize.getHeight()); + verifyNoMoreInteractions(listener); + } + + @Test + public void setVideoSurface_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setSurfaceSize(Size.ZERO) + .build(); + SettableFuture future = SettableFuture.create(); + Size updatedSize = new Size(/* width= */ 300, /* height= */ 200); + State updatedState = state.buildUpon().setSurfaceSize(updatedSize).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetVideoOutput(Object videoOutput) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setVideoSurface(new Surface(new SurfaceTexture(0))); + + // Verify placeholder state and listener calls. + assertThat(player.getSurfaceSize()).isEqualTo(Size.UNKNOWN); + verify(listener) + .onSurfaceSizeChanged(/* width= */ C.LENGTH_UNSET, /* height= */ C.LENGTH_UNSET); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getSurfaceSize()).isEqualTo(updatedSize); + verify(listener).onSurfaceSizeChanged(updatedSize.getWidth(), updatedSize.getHeight()); + verifyNoMoreInteractions(listener); + } + + @Test + public void setVideoSurface_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_VIDEO_SURFACE) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetVideoOutput(Object videoOutput) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setVideoSurface(new Surface(new SurfaceTexture(0))); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void clearVideoSurface_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setSurfaceSize(new Size(/* width= */ 300, /* height= */ 200)) + .build(); + // Change something else in addition to ensure we actually use the updated state. + State updatedState = + state.buildUpon().setSurfaceSize(Size.ZERO).setRepeatMode(Player.REPEAT_MODE_ONE).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutput) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.clearVideoSurface(); + + assertThat(player.getSurfaceSize()).isEqualTo(Size.ZERO); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); + verify(listener).onSurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + verifyNoMoreInteractions(listener); + } + + @Test + public void clearVideoSurface_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setSurfaceSize(new Size(/* width= */ 300, /* height= */ 200)) + .build(); + // Change something else in addition to ensure we actually use the updated state. + State updatedState = + state.buildUpon().setSurfaceSize(Size.ZERO).setRepeatMode(Player.REPEAT_MODE_ONE).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutput) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.clearVideoSurface(); + + // Verify placeholder state and listener calls. + assertThat(player.getSurfaceSize()).isEqualTo(Size.ZERO); + verify(listener).onSurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getSurfaceSize()).isEqualTo(Size.ZERO); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + verifyNoMoreInteractions(listener); + } + + @Test + public void clearVideoSurface_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_VIDEO_SURFACE) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutput) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.clearVideoSurface(); + + assertThat(callForwarded.get()).isFalse(); + } + private static Object[] getAnyArguments(Method method) { Object[] arguments = new Object[method.getParameterCount()]; Class[] argumentTypes = method.getParameterTypes(); From 6e58ca6baad6f1ad4e35818b1e561d54e6b79a1a Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Mon, 12 Dec 2022 10:55:15 +0000 Subject: [PATCH 047/141] Merge pull request #10750 from Stronger197:subrip_utf_16 PiperOrigin-RevId: 492164739 (cherry picked from commit a9191418051a19681ddf884163ac5553871ec658) --- RELEASENOTES.md | 3 + .../media3/common/util/ParsableByteArray.java | 156 +++++++-- .../common/util/ParsableByteArrayTest.java | 298 +++++++++++++++++- .../extractor/text/subrip/SubripDecoder.java | 20 +- .../extractor/text/tx3g/Tx3gDecoder.java | 18 +- .../text/subrip/SubripDecoderTest.java | 28 ++ .../test/assets/media/subrip/typical_utf16be | Bin 0 -> 434 bytes .../test/assets/media/subrip/typical_utf16le | Bin 0 -> 434 bytes 8 files changed, 473 insertions(+), 50 deletions(-) create mode 100644 libraries/test_data/src/test/assets/media/subrip/typical_utf16be create mode 100644 libraries/test_data/src/test/assets/media/subrip/typical_utf16le diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0d6f026f794..ce0948036b7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,9 @@ * Audio: * Use the compressed audio format bitrate to calculate the min buffer size for `AudioTrack` in direct playbacks (passthrough). +* Text: + * SubRip: Add support for UTF-16 files if they start with a byte order + mark. * Session: * Add helper method to convert platform session token to Media3 `SessionToken` ([#171](https://github.com/androidx/media/issues/171)). diff --git a/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java b/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java index 0367ab8f224..bd1117bc785 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java @@ -17,6 +17,9 @@ import androidx.annotation.Nullable; import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.Chars; +import com.google.common.primitives.UnsignedBytes; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Arrays; @@ -28,6 +31,12 @@ @UnstableApi public final class ParsableByteArray { + private static final char[] CR_AND_LF = {'\r', '\n'}; + private static final char[] LF = {'\n'}; + private static final ImmutableSet SUPPORTED_CHARSETS_FOR_READLINE = + ImmutableSet.of( + Charsets.US_ASCII, Charsets.UTF_8, Charsets.UTF_16, Charsets.UTF_16BE, Charsets.UTF_16LE); + private byte[] data; private int position; // TODO(internal b/147657250): Enforce this limit on all read methods. @@ -490,45 +499,47 @@ public String readDelimiterTerminatedString(char delimiter) { } /** - * Reads a line of text. + * Reads a line of text in UTF-8. + * + *

    Equivalent to passing {@link Charsets#UTF_8} to {@link #readLine(Charset)}. + */ + @Nullable + public String readLine() { + return readLine(Charsets.UTF_8); + } + + /** + * Reads a line of text in {@code charset}. * *

    A line is considered to be terminated by any one of a carriage return ('\r'), a line feed - * ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The UTF-8 charset is - * used. This method discards leading UTF-8 byte order marks, if present. + * ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). This method discards + * leading UTF byte order marks (BOM), if present. + * + *

    The {@linkplain #getPosition() position} is advanced to start of the next line (i.e. any + * line terminators are skipped). * + * @param charset The charset used to interpret the bytes as a {@link String}. * @return The line not including any line-termination characters, or null if the end of the data * has already been reached. + * @throws IllegalArgumentException if charset is not supported. Only US_ASCII, UTF-8, UTF-16, + * UTF-16BE, and UTF-16LE are supported. */ @Nullable - public String readLine() { + public String readLine(Charset charset) { + Assertions.checkArgument( + SUPPORTED_CHARSETS_FOR_READLINE.contains(charset), "Unsupported charset: " + charset); if (bytesLeft() == 0) { return null; } - int lineLimit = position; - while (lineLimit < limit && !Util.isLinebreak(data[lineLimit])) { - lineLimit++; + if (!charset.equals(Charsets.US_ASCII)) { + readUtfCharsetFromBom(); // Skip BOM if present } - if (lineLimit - position >= 3 - && data[position] == (byte) 0xEF - && data[position + 1] == (byte) 0xBB - && data[position + 2] == (byte) 0xBF) { - // There's a UTF-8 byte order mark at the start of the line. Discard it. - position += 3; - } - String line = Util.fromUtf8Bytes(data, position, lineLimit - position); - position = lineLimit; + int lineLimit = findNextLineTerminator(charset); + String line = readString(lineLimit - position, charset); if (position == limit) { return line; } - if (data[position] == '\r') { - position++; - if (position == limit) { - return line; - } - } - if (data[position] == '\n') { - position++; - } + skipLineTerminator(charset); return line; } @@ -566,4 +577,99 @@ public long readUtf8EncodedLong() { position += length; return value; } + + /** + * Reads a UTF byte order mark (BOM) and returns the UTF {@link Charset} it represents. Returns + * {@code null} without advancing {@link #getPosition() position} if no BOM is found. + */ + @Nullable + public Charset readUtfCharsetFromBom() { + if (bytesLeft() >= 3 + && data[position] == (byte) 0xEF + && data[position + 1] == (byte) 0xBB + && data[position + 2] == (byte) 0xBF) { + position += 3; + return Charsets.UTF_8; + } else if (bytesLeft() >= 2) { + if (data[position] == (byte) 0xFE && data[position + 1] == (byte) 0xFF) { + position += 2; + return Charsets.UTF_16BE; + } else if (data[position] == (byte) 0xFF && data[position + 1] == (byte) 0xFE) { + position += 2; + return Charsets.UTF_16LE; + } + } + return null; + } + + /** + * Returns the index of the next occurrence of '\n' or '\r', or {@link #limit} if none is found. + */ + private int findNextLineTerminator(Charset charset) { + int stride; + if (charset.equals(Charsets.UTF_8) || charset.equals(Charsets.US_ASCII)) { + stride = 1; + } else if (charset.equals(Charsets.UTF_16) + || charset.equals(Charsets.UTF_16LE) + || charset.equals(Charsets.UTF_16BE)) { + stride = 2; + } else { + throw new IllegalArgumentException("Unsupported charset: " + charset); + } + for (int i = position; i < limit - (stride - 1); i += stride) { + if ((charset.equals(Charsets.UTF_8) || charset.equals(Charsets.US_ASCII)) + && Util.isLinebreak(data[i])) { + return i; + } else if ((charset.equals(Charsets.UTF_16) || charset.equals(Charsets.UTF_16BE)) + && data[i] == 0x00 + && Util.isLinebreak(data[i + 1])) { + return i; + } else if (charset.equals(Charsets.UTF_16LE) + && data[i + 1] == 0x00 + && Util.isLinebreak(data[i])) { + return i; + } + } + return limit; + } + + private void skipLineTerminator(Charset charset) { + if (readCharacterIfInList(charset, CR_AND_LF) == '\r') { + readCharacterIfInList(charset, LF); + } + } + + /** + * Peeks at the character at {@link #position} (as decoded by {@code charset}), returns it and + * advances {@link #position} past it if it's in {@code chars}, otherwise returns {@code 0} + * without advancing {@link #position}. Returns {@code 0} if {@link #bytesLeft()} doesn't allow + * reading a whole character in {@code charset}. + * + *

    Only supports characters in {@code chars} that occupy a single code unit (i.e. one byte for + * UTF-8 and two bytes for UTF-16). + */ + private char readCharacterIfInList(Charset charset, char[] chars) { + char character; + int characterSize; + if ((charset.equals(Charsets.UTF_8) || charset.equals(Charsets.US_ASCII)) && bytesLeft() >= 1) { + character = Chars.checkedCast(UnsignedBytes.toInt(data[position])); + characterSize = 1; + } else if ((charset.equals(Charsets.UTF_16) || charset.equals(Charsets.UTF_16BE)) + && bytesLeft() >= 2) { + character = Chars.fromBytes(data[position], data[position + 1]); + characterSize = 2; + } else if (charset.equals(Charsets.UTF_16LE) && bytesLeft() >= 2) { + character = Chars.fromBytes(data[position + 1], data[position]); + characterSize = 2; + } else { + return 0; + } + + if (Chars.contains(chars, character)) { + position += characterSize; + return Chars.checkedCast(character); + } else { + return 0; + } + } } diff --git a/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java b/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java index ddaf7ee981d..cddf95c9f8a 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java @@ -15,11 +15,13 @@ */ package androidx.media3.common.util; +import static androidx.media3.test.utils.TestUtil.createByteArray; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.Charset.forName; import static org.junit.Assert.fail; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.base.Charsets; import com.google.common.primitives.Bytes; import java.nio.ByteBuffer; import java.util.Arrays; @@ -548,48 +550,324 @@ public void readDelimiterTerminatedStringWithoutEndingDelimiter() { } @Test - public void readSingleLineWithoutEndingTrail() { - byte[] bytes = new byte[] {'f', 'o', 'o'}; + public void readSingleLineWithoutEndingTrail_ascii() { + byte[] bytes = "foo".getBytes(Charsets.US_ASCII); ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(3); + assertThat(parser.readLine(Charsets.US_ASCII)).isNull(); + } + + @Test + public void readSingleLineWithEndingLf_ascii() { + byte[] bytes = "foo\n".getBytes(Charsets.US_ASCII); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readLine(Charsets.US_ASCII)).isNull(); + } + + @Test + public void readTwoLinesWithCrFollowedByLf_ascii() { + byte[] bytes = "foo\r\nbar".getBytes(Charsets.US_ASCII); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(5); + assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readLine(Charsets.US_ASCII)).isNull(); + } + + @Test + public void readThreeLinesWithEmptyLine_ascii() { + byte[] bytes = "foo\r\n\rbar".getBytes(Charsets.US_ASCII); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(5); + assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(6); + assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(9); + assertThat(parser.readLine(Charsets.US_ASCII)).isNull(); + } + + @Test + public void readFourLinesWithLfFollowedByCr_ascii() { + byte[] bytes = "foo\n\r\rbar\r\n".getBytes(Charsets.US_ASCII); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(5); + assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(6); + assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(11); + assertThat(parser.readLine(Charsets.US_ASCII)).isNull(); + } + + @Test + public void readSingleLineWithoutEndingTrail_utf8() { + byte[] bytes = "foo".getBytes(Charsets.UTF_8); + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(3); assertThat(parser.readLine()).isNull(); } @Test - public void readSingleLineWithEndingLf() { - byte[] bytes = new byte[] {'f', 'o', 'o', '\n'}; + public void readSingleLineWithEndingLf_utf8() { + byte[] bytes = "foo\n".getBytes(Charsets.UTF_8); ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.readLine()).isNull(); } @Test - public void readTwoLinesWithCrFollowedByLf() { - byte[] bytes = new byte[] {'f', 'o', 'o', '\r', '\n', 'b', 'a', 'r'}; + public void readTwoLinesWithCrFollowedByLf_utf8() { + byte[] bytes = "foo\r\nbar".getBytes(Charsets.UTF_8); ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(5); assertThat(parser.readLine()).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(8); assertThat(parser.readLine()).isNull(); } @Test - public void readThreeLinesWithEmptyLine() { - byte[] bytes = new byte[] {'f', 'o', 'o', '\r', '\n', '\r', 'b', 'a', 'r'}; + public void readThreeLinesWithEmptyLineAndLeadingBom_utf8() { + byte[] bytes = + Bytes.concat(createByteArray(0xEF, 0xBB, 0xBF), "foo\r\n\rbar".getBytes(Charsets.UTF_8)); ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(8); assertThat(parser.readLine()).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(9); assertThat(parser.readLine()).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(12); assertThat(parser.readLine()).isNull(); } @Test - public void readFourLinesWithLfFollowedByCr() { - byte[] bytes = new byte[] {'f', 'o', 'o', '\n', '\r', '\r', 'b', 'a', 'r', '\r', '\n'}; + public void readFourLinesWithLfFollowedByCr_utf8() { + byte[] bytes = "foo\n\r\rbar\r\n".getBytes(Charsets.UTF_8); ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.readLine()).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(5); assertThat(parser.readLine()).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(6); assertThat(parser.readLine()).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(11); assertThat(parser.readLine()).isNull(); } + + @Test + public void readSingleLineWithoutEndingTrail_utf16() { + // Use UTF_16BE because we don't want the leading BOM that's added by getBytes(UTF_16). We + // explicitly test with a BOM elsewhere. + byte[] bytes = "foo".getBytes(Charsets.UTF_16BE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(6); + assertThat(parser.readLine(Charsets.UTF_16)).isNull(); + } + + @Test + public void readSingleLineWithEndingLf_utf16() { + // Use UTF_16BE because we don't want the leading BOM that's added by getBytes(UTF_16). We + // explicitly test with a BOM elsewhere. + byte[] bytes = "foo\n".getBytes(Charsets.UTF_16BE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readLine(Charsets.UTF_16)).isNull(); + } + + @Test + public void readTwoLinesWithCrFollowedByLf_utf16() { + // Use UTF_16BE because we don't want the leading BOM that's added by getBytes(UTF_16). We + // explicitly test with a BOM elsewhere. + byte[] bytes = "foo\r\nbar".getBytes(Charsets.UTF_16BE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(10); + assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(16); + assertThat(parser.readLine(Charsets.UTF_16)).isNull(); + } + + @Test + public void readThreeLinesWithEmptyLineAndLeadingBom_utf16() { + // getBytes(UTF_16) always adds the leading BOM. + byte[] bytes = "foo\r\n\rbar".getBytes(Charsets.UTF_16); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(12); + assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(14); + assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(20); + assertThat(parser.readLine(Charsets.UTF_16)).isNull(); + } + + @Test + public void readFourLinesWithLfFollowedByCr_utf16() { + // Use UTF_16BE because we don't want the leading BOM that's added by getBytes(UTF_16). We + // explicitly test with a BOM elsewhere. + byte[] bytes = "foo\n\r\rbar\r\n".getBytes(Charsets.UTF_16BE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(10); + assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(12); + assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(22); + assertThat(parser.readLine(Charsets.UTF_16)).isNull(); + } + + @Test + public void readSingleLineWithoutEndingTrail_utf16be() { + byte[] bytes = "foo".getBytes(Charsets.UTF_16BE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(6); + assertThat(parser.readLine(Charsets.UTF_16BE)).isNull(); + } + + @Test + public void readSingleLineWithEndingLf_utf16be() { + byte[] bytes = "foo\n".getBytes(Charsets.UTF_16BE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readLine(Charsets.UTF_16BE)).isNull(); + } + + @Test + public void readTwoLinesWithCrFollowedByLf_utf16be() { + byte[] bytes = "foo\r\nbar".getBytes(Charsets.UTF_16BE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(10); + assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(16); + assertThat(parser.readLine(Charsets.UTF_16BE)).isNull(); + } + + @Test + public void readThreeLinesWithEmptyLineAndLeadingBom_utf16be() { + byte[] bytes = + Bytes.concat(createByteArray(0xFE, 0xFF), "foo\r\n\rbar".getBytes(Charsets.UTF_16BE)); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(12); + assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(14); + assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(20); + assertThat(parser.readLine(Charsets.UTF_16BE)).isNull(); + } + + @Test + public void readFourLinesWithLfFollowedByCr_utf16be() { + byte[] bytes = "foo\n\r\rbar\r\n".getBytes(Charsets.UTF_16BE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(10); + assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(12); + assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(22); + assertThat(parser.readLine(Charsets.UTF_16BE)).isNull(); + } + + @Test + public void readSingleLineWithoutEndingTrail_utf16le() { + byte[] bytes = "foo".getBytes(Charsets.UTF_16LE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(6); + assertThat(parser.readLine(Charsets.UTF_16LE)).isNull(); + } + + @Test + public void readSingleLineWithEndingLf_utf16le() { + byte[] bytes = "foo\n".getBytes(Charsets.UTF_16LE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readLine(Charsets.UTF_16LE)).isNull(); + } + + @Test + public void readTwoLinesWithCrFollowedByLf_utf16le() { + byte[] bytes = "foo\r\nbar".getBytes(Charsets.UTF_16LE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(10); + assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(16); + assertThat(parser.readLine(Charsets.UTF_16LE)).isNull(); + } + + @Test + public void readThreeLinesWithEmptyLineAndLeadingBom_utf16le() { + byte[] bytes = + Bytes.concat(createByteArray(0xFF, 0xFE), "foo\r\n\rbar".getBytes(Charsets.UTF_16LE)); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(12); + assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(14); + assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(20); + assertThat(parser.readLine(Charsets.UTF_16LE)).isNull(); + } + + @Test + public void readFourLinesWithLfFollowedByCr_utf16le() { + byte[] bytes = "foo\n\r\rbar\r\n".getBytes(Charsets.UTF_16LE); + ParsableByteArray parser = new ParsableByteArray(bytes); + + assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(10); + assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(12); + assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(22); + assertThat(parser.readLine(Charsets.UTF_16LE)).isNull(); + } } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripDecoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripDecoder.java index 1ecc7f425d5..6147ff92ada 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripDecoder.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripDecoder.java @@ -27,6 +27,8 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.extractor.text.SimpleSubtitleDecoder; import androidx.media3.extractor.text.Subtitle; +import com.google.common.base.Charsets; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -76,9 +78,10 @@ protected Subtitle decode(byte[] data, int length, boolean reset) { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(data, length); + Charset charset = detectUtfCharset(subripData); @Nullable String currentLine; - while ((currentLine = subripData.readLine()) != null) { + while ((currentLine = subripData.readLine(charset)) != null) { if (currentLine.length() == 0) { // Skip blank lines. continue; @@ -93,7 +96,7 @@ protected Subtitle decode(byte[] data, int length, boolean reset) { } // Read and parse the timing line. - currentLine = subripData.readLine(); + currentLine = subripData.readLine(charset); if (currentLine == null) { Log.w(TAG, "Unexpected end"); break; @@ -111,13 +114,13 @@ protected Subtitle decode(byte[] data, int length, boolean reset) { // Read and parse the text and tags. textBuilder.setLength(0); tags.clear(); - currentLine = subripData.readLine(); + currentLine = subripData.readLine(charset); while (!TextUtils.isEmpty(currentLine)) { if (textBuilder.length() > 0) { textBuilder.append("
    "); } textBuilder.append(processLine(currentLine, tags)); - currentLine = subripData.readLine(); + currentLine = subripData.readLine(charset); } Spanned text = Html.fromHtml(textBuilder.toString()); @@ -140,6 +143,15 @@ protected Subtitle decode(byte[] data, int length, boolean reset) { return new SubripSubtitle(cuesArray, cueTimesUsArray); } + /** + * Determine UTF encoding of the byte array from a byte order mark (BOM), defaulting to UTF-8 if + * no BOM is found. + */ + private Charset detectUtfCharset(ParsableByteArray data) { + @Nullable Charset charset = data.readUtfCharsetFromBom(); + return charset != null ? charset : Charsets.UTF_8; + } + /** * Trims and removes tags from the given line. The removed tags are added to {@code tags}. * diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/tx3g/Tx3gDecoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/tx3g/Tx3gDecoder.java index e0339d8f97a..e66888b8072 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/tx3g/Tx3gDecoder.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/tx3g/Tx3gDecoder.java @@ -26,6 +26,7 @@ import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.text.Cue; import androidx.media3.common.util.Log; @@ -36,6 +37,7 @@ import androidx.media3.extractor.text.Subtitle; import androidx.media3.extractor.text.SubtitleDecoderException; import com.google.common.base.Charsets; +import java.nio.charset.Charset; import java.util.List; /** @@ -48,16 +50,12 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { private static final String TAG = "Tx3gDecoder"; - private static final char BOM_UTF16_BE = '\uFEFF'; - private static final char BOM_UTF16_LE = '\uFFFE'; - private static final int TYPE_STYL = 0x7374796c; private static final int TYPE_TBOX = 0x74626f78; private static final String TX3G_SERIF = "Serif"; private static final int SIZE_ATOM_HEADER = 8; private static final int SIZE_SHORT = 2; - private static final int SIZE_BOM_UTF16 = 2; private static final int SIZE_STYLE_RECORD = 12; private static final int FONT_FACE_BOLD = 0x0001; @@ -173,13 +171,11 @@ private static String readSubtitleText(ParsableByteArray parsableByteArray) if (textLength == 0) { return ""; } - if (parsableByteArray.bytesLeft() >= SIZE_BOM_UTF16) { - char firstChar = parsableByteArray.peekChar(); - if (firstChar == BOM_UTF16_BE || firstChar == BOM_UTF16_LE) { - return parsableByteArray.readString(textLength, Charsets.UTF_16); - } - } - return parsableByteArray.readString(textLength, Charsets.UTF_8); + int textStartPosition = parsableByteArray.getPosition(); + @Nullable Charset charset = parsableByteArray.readUtfCharsetFromBom(); + int bomSize = parsableByteArray.getPosition() - textStartPosition; + return parsableByteArray.readString( + textLength - bomSize, charset != null ? charset : Charsets.UTF_8); } private void applyStyleRecord(ParsableByteArray parsableByteArray, SpannableStringBuilder cueText) diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripDecoderTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripDecoderTest.java index e9a4b8f8b84..259a72809d5 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripDecoderTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripDecoderTest.java @@ -40,6 +40,8 @@ public final class SubripDecoderTest { private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "media/subrip/typical_negative_timestamps"; private static final String TYPICAL_UNEXPECTED_END = "media/subrip/typical_unexpected_end"; + private static final String TYPICAL_UTF16BE = "media/subrip/typical_utf16be"; + private static final String TYPICAL_UTF16LE = "media/subrip/typical_utf16le"; private static final String TYPICAL_WITH_TAGS = "media/subrip/typical_with_tags"; private static final String TYPICAL_NO_HOURS_AND_MILLIS = "media/subrip/typical_no_hours_and_millis"; @@ -148,6 +150,32 @@ public void decodeTypicalUnexpectedEnd() throws IOException { assertTypicalCue2(subtitle, 2); } + @Test + public void decodeTypicalUtf16LittleEndian() throws IOException { + SubripDecoder decoder = new SubripDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16LE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); + assertTypicalCue1(subtitle, 0); + assertTypicalCue2(subtitle, 2); + assertTypicalCue3(subtitle, 4); + } + + @Test + public void decodeTypicalUtf16BigEndian() throws IOException { + SubripDecoder decoder = new SubripDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16BE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); + assertTypicalCue1(subtitle, 0); + assertTypicalCue2(subtitle, 2); + assertTypicalCue3(subtitle, 4); + } + @Test public void decodeCueWithTag() throws IOException { SubripDecoder decoder = new SubripDecoder(); diff --git a/libraries/test_data/src/test/assets/media/subrip/typical_utf16be b/libraries/test_data/src/test/assets/media/subrip/typical_utf16be new file mode 100644 index 0000000000000000000000000000000000000000..9531c268087bec207cf8b766bc60ef01c13b354a GIT binary patch literal 434 zcmaKoYYM_J5QOJ$fhL z8D_R1{Qq)-*}-tqO|8x&-1|vHs%KDIh3R-#L%;p$w?V&&oGVf1wXJ&kW5gQ71~ Date: Thu, 1 Dec 2022 14:48:29 +0000 Subject: [PATCH 048/141] Split SubripDecoder and ParsableByteArray tests In some cases we split a test method, and in other cases we just add line breaks to make the separation between arrange/act/assert more clear. PiperOrigin-RevId: 492182769 (cherry picked from commit e4fb663b23e38eb6e41b742681bf80b872baad24) --- .../common/util/ParsableByteArrayTest.java | 85 ++++++++++++++----- .../text/subrip/SubripDecoderTest.java | 17 ++-- 2 files changed, 78 insertions(+), 24 deletions(-) diff --git a/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java b/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java index cddf95c9f8a..2a97c0dfd9c 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java @@ -332,6 +332,7 @@ public void readingBytesReturnsCopy() { public void readLittleEndianLong() { ParsableByteArray byteArray = new ParsableByteArray(new byte[] {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFF}); + assertThat(byteArray.readLittleEndianLong()).isEqualTo(0xFF00000000000001L); assertThat(byteArray.getPosition()).isEqualTo(8); } @@ -339,6 +340,7 @@ public void readLittleEndianLong() { @Test public void readLittleEndianUnsignedInt() { ParsableByteArray byteArray = new ParsableByteArray(new byte[] {0x10, 0x00, 0x00, (byte) 0xFF}); + assertThat(byteArray.readLittleEndianUnsignedInt()).isEqualTo(0xFF000010L); assertThat(byteArray.getPosition()).isEqualTo(4); } @@ -346,6 +348,7 @@ public void readLittleEndianUnsignedInt() { @Test public void readLittleEndianInt() { ParsableByteArray byteArray = new ParsableByteArray(new byte[] {0x01, 0x00, 0x00, (byte) 0xFF}); + assertThat(byteArray.readLittleEndianInt()).isEqualTo(0xFF000001); assertThat(byteArray.getPosition()).isEqualTo(4); } @@ -354,6 +357,7 @@ public void readLittleEndianInt() { public void readLittleEndianUnsignedInt24() { byte[] data = {0x01, 0x02, (byte) 0xFF}; ParsableByteArray byteArray = new ParsableByteArray(data); + assertThat(byteArray.readLittleEndianUnsignedInt24()).isEqualTo(0xFF0201); assertThat(byteArray.getPosition()).isEqualTo(3); } @@ -362,6 +366,7 @@ public void readLittleEndianUnsignedInt24() { public void readInt24Positive() { byte[] data = {0x01, 0x02, (byte) 0xFF}; ParsableByteArray byteArray = new ParsableByteArray(data); + assertThat(byteArray.readInt24()).isEqualTo(0x0102FF); assertThat(byteArray.getPosition()).isEqualTo(3); } @@ -370,6 +375,7 @@ public void readInt24Positive() { public void readInt24Negative() { byte[] data = {(byte) 0xFF, 0x02, (byte) 0x01}; ParsableByteArray byteArray = new ParsableByteArray(data); + assertThat(byteArray.readInt24()).isEqualTo(0xFFFF0201); assertThat(byteArray.getPosition()).isEqualTo(3); } @@ -378,6 +384,7 @@ public void readInt24Negative() { public void readLittleEndianUnsignedShort() { ParsableByteArray byteArray = new ParsableByteArray(new byte[] {0x01, (byte) 0xFF, 0x02, (byte) 0xFF}); + assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF01); assertThat(byteArray.getPosition()).isEqualTo(2); assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF02); @@ -388,6 +395,7 @@ public void readLittleEndianUnsignedShort() { public void readLittleEndianShort() { ParsableByteArray byteArray = new ParsableByteArray(new byte[] {0x01, (byte) 0xFF, 0x02, (byte) 0xFF}); + assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF01); assertThat(byteArray.getPosition()).isEqualTo(2); assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF02); @@ -422,6 +430,7 @@ public void readString() { (byte) 0x20, }; ParsableByteArray byteArray = new ParsableByteArray(data); + assertThat(byteArray.readString(data.length)).isEqualTo("ä ö ® π √ ± 谢 "); assertThat(byteArray.getPosition()).isEqualTo(data.length); } @@ -430,6 +439,7 @@ public void readString() { public void readAsciiString() { byte[] data = new byte[] {'t', 'e', 's', 't'}; ParsableByteArray testArray = new ParsableByteArray(data); + assertThat(testArray.readString(data.length, forName("US-ASCII"))).isEqualTo("test"); assertThat(testArray.getPosition()).isEqualTo(data.length); } @@ -438,6 +448,7 @@ public void readAsciiString() { public void readStringOutOfBoundsDoesNotMovePosition() { byte[] data = {(byte) 0xC3, (byte) 0xA4, (byte) 0x20}; ParsableByteArray byteArray = new ParsableByteArray(data); + try { byteArray.readString(data.length + 1); fail(); @@ -454,17 +465,22 @@ public void readEmptyString() { } @Test - public void readNullTerminatedStringWithLengths() { + public void readNullTerminatedStringWithLengths_readLengthsMatchNullPositions() { byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0}; - // Test with lengths that match NUL byte positions. + ParsableByteArray parser = new ParsableByteArray(bytes); assertThat(parser.readNullTerminatedString(4)).isEqualTo("foo"); assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.readNullTerminatedString(4)).isEqualTo("bar"); assertThat(parser.getPosition()).isEqualTo(8); assertThat(parser.readNullTerminatedString()).isNull(); - // Test with lengths that do not match NUL byte positions. - parser = new ParsableByteArray(bytes); + } + + @Test + public void readNullTerminatedStringWithLengths_readLengthsDontMatchNullPositions() { + byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0}; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readNullTerminatedString(2)).isEqualTo("fo"); assertThat(parser.getPosition()).isEqualTo(2); assertThat(parser.readNullTerminatedString(2)).isEqualTo("o"); @@ -474,13 +490,23 @@ public void readNullTerminatedStringWithLengths() { assertThat(parser.readNullTerminatedString(1)).isEqualTo(""); assertThat(parser.getPosition()).isEqualTo(8); assertThat(parser.readNullTerminatedString()).isNull(); - // Test with limit at NUL - parser = new ParsableByteArray(bytes, 4); + } + + @Test + public void readNullTerminatedStringWithLengths_limitAtNull() { + byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0}; + ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 4); + assertThat(parser.readNullTerminatedString(4)).isEqualTo("foo"); assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.readNullTerminatedString()).isNull(); - // Test with limit before NUL - parser = new ParsableByteArray(bytes, 3); + } + + @Test + public void readNullTerminatedStringWithLengths_limitBeforeNull() { + byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0}; + ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 3); + assertThat(parser.readNullTerminatedString(3)).isEqualTo("foo"); assertThat(parser.getPosition()).isEqualTo(3); assertThat(parser.readNullTerminatedString()).isNull(); @@ -489,20 +515,30 @@ public void readNullTerminatedStringWithLengths() { @Test public void readNullTerminatedString() { byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0}; - // Test normal case. ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.readNullTerminatedString()).isEqualTo("bar"); assertThat(parser.getPosition()).isEqualTo(8); assertThat(parser.readNullTerminatedString()).isNull(); - // Test with limit at NUL. - parser = new ParsableByteArray(bytes, 4); + } + + @Test + public void readNullTerminatedString_withLimitAtNull() { + byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0}; + ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 4); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.readNullTerminatedString()).isNull(); - // Test with limit before NUL. - parser = new ParsableByteArray(bytes, 3); + } + + @Test + public void readNullTerminatedString_withLimitBeforeNull() { + byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0}; + ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 3); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); assertThat(parser.getPosition()).isEqualTo(3); assertThat(parser.readNullTerminatedString()).isNull(); @@ -512,6 +548,7 @@ public void readNullTerminatedString() { public void readNullTerminatedStringWithoutEndingNull() { byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r'}; ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); assertThat(parser.readNullTerminatedString()).isEqualTo("bar"); assertThat(parser.readNullTerminatedString()).isNull(); @@ -520,30 +557,40 @@ public void readNullTerminatedStringWithoutEndingNull() { @Test public void readDelimiterTerminatedString() { byte[] bytes = new byte[] {'f', 'o', 'o', '*', 'b', 'a', 'r', '*'}; - // Test normal case. ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo"); assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("bar"); assertThat(parser.getPosition()).isEqualTo(8); assertThat(parser.readDelimiterTerminatedString('*')).isNull(); + } + + @Test + public void readDelimiterTerminatedString_limitAtDelimiter() { + byte[] bytes = new byte[] {'f', 'o', 'o', '*', 'b', 'a', 'r', '*'}; + ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 4); - // Test with limit at delimiter. - parser = new ParsableByteArray(bytes, 4); assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo"); assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.readDelimiterTerminatedString('*')).isNull(); - // Test with limit before delimiter. - parser = new ParsableByteArray(bytes, 3); + } + + @Test + public void readDelimiterTerminatedString_limitBeforeDelimiter() { + byte[] bytes = new byte[] {'f', 'o', 'o', '*', 'b', 'a', 'r', '*'}; + ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 3); + assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo"); assertThat(parser.getPosition()).isEqualTo(3); assertThat(parser.readDelimiterTerminatedString('*')).isNull(); } @Test - public void readDelimiterTerminatedStringWithoutEndingDelimiter() { + public void readDelimiterTerminatedStringW_noDelimiter() { byte[] bytes = new byte[] {'f', 'o', 'o', '*', 'b', 'a', 'r'}; ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo"); assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("bar"); assertThat(parser.readDelimiterTerminatedString('*')).isNull(); diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripDecoderTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripDecoderTest.java index 259a72809d5..642e20e2595 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripDecoderTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripDecoderTest.java @@ -50,6 +50,7 @@ public final class SubripDecoderTest { public void decodeEmpty() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(0); @@ -60,6 +61,7 @@ public void decodeEmpty() throws IOException { public void decodeTypical() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); @@ -74,6 +76,7 @@ public void decodeTypicalWithByteOrderMark() throws IOException { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_WITH_BYTE_ORDER_MARK); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); @@ -88,6 +91,7 @@ public void decodeTypicalExtraBlankLine() throws IOException { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_EXTRA_BLANK_LINE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); @@ -103,6 +107,7 @@ public void decodeTypicalMissingTimecode() throws IOException { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_TIMECODE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); @@ -117,6 +122,7 @@ public void decodeTypicalMissingSequence() throws IOException { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_SEQUENCE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); @@ -131,6 +137,7 @@ public void decodeTypicalNegativeTimestamps() throws IOException { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_NEGATIVE_TIMESTAMPS); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(2); @@ -143,6 +150,7 @@ public void decodeTypicalUnexpectedEnd() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UNEXPECTED_END); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); @@ -155,6 +163,7 @@ public void decodeTypicalUtf16LittleEndian() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16LE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); @@ -168,6 +177,7 @@ public void decodeTypicalUtf16BigEndian() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16BE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); @@ -181,23 +191,19 @@ public void decodeCueWithTag() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()) .isEqualTo("This is the first subtitle."); - assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()) .isEqualTo("This is the second subtitle.\nSecond subtitle with second line."); - assertThat(subtitle.getCues(subtitle.getEventTime(4)).get(0).text.toString()) .isEqualTo("This is the third subtitle."); - assertThat(subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString()) .isEqualTo("This { \\an2} is not a valid tag due to the space after the opening bracket."); - assertThat(subtitle.getCues(subtitle.getEventTime(8)).get(0).text.toString()) .isEqualTo("This is the fifth subtitle with multiple valid tags."); - assertAlignmentCue(subtitle, 10, Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_START); // {/an1} assertAlignmentCue(subtitle, 12, Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_MIDDLE); // {/an2} assertAlignmentCue(subtitle, 14, Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_END); // {/an3} @@ -215,6 +221,7 @@ public void decodeTypicalNoHoursAndMillis() throws IOException { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_NO_HOURS_AND_MILLIS); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); From 1a9b1c4d35183fa838dbd34f8e1e8d3faebbdb8a Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 1 Dec 2022 15:33:00 +0000 Subject: [PATCH 049/141] Reduce log output for failing bitmap loads Do not log the exception stack traces raised by the BitmapLoader when a bitmap fails to load, e.g. when the artwork's URI scheme is not supported by the SimpleBitmapLoader. The logs are kept in place but only a single line is printed. #minor-release PiperOrigin-RevId: 492191461 (cherry picked from commit f768ff970ca15483bcb02c1cf41746b67ec8c3ac) --- .../media3/session/DefaultMediaNotificationProvider.java | 8 ++++++-- .../androidx/media3/session/MediaSessionLegacyStub.java | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index 5cdc2630333..b0fd6bc36a7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -332,7 +332,7 @@ public final MediaNotification createNotification( try { builder.setLargeIcon(Futures.getDone(bitmapFuture)); } catch (ExecutionException e) { - Log.w(TAG, "Failed to load bitmap", e); + Log.w(TAG, getBitmapLoadErrorMessage(e)); } } else { pendingOnBitmapLoadedFutureCallback = @@ -634,7 +634,7 @@ public void onSuccess(Bitmap result) { @Override public void onFailure(Throwable t) { if (!discarded) { - Log.d(TAG, "Failed to load bitmap", t); + Log.w(TAG, getBitmapLoadErrorMessage(t)); } } } @@ -655,4 +655,8 @@ public static void createNotificationChannel( notificationManager.createNotificationChannel(channel); } } + + private static String getBitmapLoadErrorMessage(Throwable throwable) { + return "Failed to load bitmap: " + throwable.getMessage(); + } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index a80301d5098..2215b070711 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -1177,7 +1177,7 @@ private void updateMetadataIfChanged() { try { artworkBitmap = Futures.getDone(bitmapFuture); } catch (ExecutionException e) { - Log.w(TAG, "Failed to load bitmap", e); + Log.w(TAG, getBitmapLoadErrorMessage(e)); } } else { pendingBitmapLoadCallback = @@ -1199,7 +1199,7 @@ public void onFailure(Throwable t) { if (this != pendingBitmapLoadCallback) { return; } - Log.d(TAG, "Failed to load bitmap", t); + Log.w(TAG, getBitmapLoadErrorMessage(t)); } }; Futures.addCallback( @@ -1270,4 +1270,8 @@ public boolean hasPendingMediaPlayPauseKey() { return hasMessages(MSG_DOUBLE_TAP_TIMED_OUT); } } + + private static String getBitmapLoadErrorMessage(Throwable throwable) { + return "Failed to load bitmap: " + throwable.getMessage(); + } } From 868e86cd3f6e13d1ef033956f6deab9c4e46c8c1 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 1 Dec 2022 15:39:28 +0000 Subject: [PATCH 050/141] Stop service when app is terminated while the player is paused If the service ever has been started but is not in the foreground, the service would be terminated without calling onDestroy(). This is because when onStartCommand returns START_STICKY [1], the app takes the responsibility to stop the service. Note that this change interrupts the user journey when paused, because the notification is removed. Apps can implement playback resumption [2] to give the user an option to resume playback after the service has been terminated. [1] https://developer.android.com/reference/android/app/Service#START_STICKY [2] https://developer.android.com/guide/topics/media/media-controls#supporting_playback_resumption Issue: androidx/media#175 #minor-release PiperOrigin-RevId: 492192690 (cherry picked from commit 6a5ac19140253e7e78ea65745914b0746e527058) --- .../java/androidx/media3/demo/session/PlaybackService.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt index cc8291c27db..16ca1a25a5d 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt @@ -72,6 +72,12 @@ class PlaybackService : MediaLibraryService() { return mediaLibrarySession } + override fun onTaskRemoved(rootIntent: Intent?) { + if (!player.playWhenReady) { + stopSelf() + } + } + override fun onDestroy() { player.release() mediaLibrarySession.release() From 8618263b996d4462e782f58d5aa9dbfc55ebe737 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 2 Dec 2022 10:11:04 +0000 Subject: [PATCH 051/141] Write media type with a custom key to legacy components. This allows legacy media controllers and browsers to access this information and legacy sessions and browser services to set this information. PiperOrigin-RevId: 492414716 (cherry picked from commit ca4c6efdb7fdb50cef116d26360b79ed75a6401e) --- .../media3/session/MediaConstants.java | 10 ++++ .../androidx/media3/session/MediaUtils.java | 31 ++++++++-- .../media3/session/MediaUtilsTest.java | 58 +++++++++++++++++-- .../media3/session/MediaTestUtils.java | 6 +- 4 files changed, 95 insertions(+), 10 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java index d79385f5e46..8dda126ce46 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java @@ -19,6 +19,7 @@ import android.content.Intent; import android.os.Bundle; import android.os.Parcelable; +import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; @@ -455,6 +456,15 @@ public final class MediaConstants { androidx.media.utils.MediaConstants .BROWSER_SERVICE_EXTRAS_KEY_APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT; + /** + * {@link Bundle} key used to indicate the {@link MediaMetadata.MediaType} in the legacy {@link + * MediaDescriptionCompat} as a long {@link MediaDescriptionCompat#getExtras() extra} and as a + * long value in {@link android.support.v4.media.MediaMetadataCompat}. + */ + @UnstableApi + public static final String EXTRAS_KEY_MEDIA_TYPE_COMPAT = + "androidx.media3.session.EXTRAS_KEY_MEDIA_TYPE_COMPAT"; + /* package */ static final String SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED = "androidx.media3.session.SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED"; /* package */ static final String SESSION_COMMAND_REQUEST_SESSION3_TOKEN = diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 7caf715ea51..c805129dd48 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -339,15 +339,24 @@ public static MediaDescriptionCompat convertToMediaDescriptionCompat( builder.setIconBitmap(artworkBitmap); } @Nullable Bundle extras = metadata.extras; - if (metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE) { + boolean hasFolderType = + metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE; + boolean hasMediaType = metadata.mediaType != null; + if (hasFolderType || hasMediaType) { if (extras == null) { extras = new Bundle(); } else { extras = new Bundle(extras); } - extras.putLong( - MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE, - convertToExtraBtFolderType(metadata.folderType)); + if (hasFolderType) { + extras.putLong( + MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE, + convertToExtraBtFolderType(checkNotNull(metadata.folderType))); + } + if (hasMediaType) { + extras.putLong( + MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT, checkNotNull(metadata.mediaType)); + } } return builder .setTitle(metadata.title) @@ -420,6 +429,10 @@ private static MediaMetadata convertToMediaMetadata( builder.setFolderType(MediaMetadata.FOLDER_TYPE_NONE); } + if (extras != null && extras.containsKey(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT)) { + builder.setMediaType((int) extras.getLong(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT)); + } + builder.setIsPlayable(playable); return builder.build(); @@ -496,6 +509,11 @@ public static MediaMetadata convertToMediaMetadata( builder.setFolderType(MediaMetadata.FOLDER_TYPE_NONE); } + if (metadataCompat.containsKey(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT)) { + builder.setMediaType( + (int) metadataCompat.getLong(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT)); + } + builder.setIsPlayable(true); return builder.build(); @@ -610,6 +628,11 @@ public static MediaMetadataCompat convertToMediaMetadataCompat( builder.putRating(MediaMetadataCompat.METADATA_KEY_RATING, overallRatingCompat); } + if (mediaItem.mediaMetadata.mediaType != null) { + builder.putLong( + MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT, mediaItem.mediaMetadata.mediaType); + } + return builder.build(); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java index 83c5a4e3f8f..59209c334ad 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java @@ -142,20 +142,29 @@ public void convertToQueueItem_withArtworkData() throws Exception { } @Test - public void convertToMediaDescriptionCompat() { + public void convertToMediaDescriptionCompat_setsExpectedValues() { String mediaId = "testId"; String title = "testTitle"; String description = "testDesc"; MediaMetadata metadata = - new MediaMetadata.Builder().setTitle(title).setDescription(description).build(); + new MediaMetadata.Builder() + .setTitle(title) + .setDescription(description) + .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC) + .build(); MediaItem mediaItem = new MediaItem.Builder().setMediaId(mediaId).setMediaMetadata(metadata).build(); MediaDescriptionCompat descriptionCompat = - MediaUtils.convertToMediaDescriptionCompat(mediaItem); + MediaUtils.convertToMediaDescriptionCompat(mediaItem, /* artworkBitmap= */ null); assertThat(descriptionCompat.getMediaId()).isEqualTo(mediaId); assertThat(descriptionCompat.getTitle()).isEqualTo(title); assertThat(descriptionCompat.getDescription()).isEqualTo(description); + assertThat( + descriptionCompat + .getExtras() + .getLong(androidx.media3.session.MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT)) + .isEqualTo(MediaMetadata.MEDIA_TYPE_MUSIC); } @Test @@ -196,7 +205,8 @@ public void convertToMediaMetadata_withTitle() { } @Test - public void convertToMediaMetadata_roundTrip_returnsEqualMediaItem() throws Exception { + public void convertToMediaMetadata_roundTripViaMediaMetadataCompat_returnsEqualMediaItemMetadata() + throws Exception { MediaItem testMediaItem = MediaTestUtils.createMediaItemWithArtworkData("testZZZ"); MediaMetadata testMediaMetadata = testMediaItem.mediaMetadata; @Nullable Bitmap testArtworkBitmap = null; @@ -216,6 +226,46 @@ public void convertToMediaMetadata_roundTrip_returnsEqualMediaItem() throws Exce assertThat(mediaMetadata.artworkData).isNotNull(); } + @Test + public void + convertToMediaMetadata_roundTripViaMediaDescriptionCompat_returnsEqualMediaItemMetadata() + throws Exception { + MediaItem testMediaItem = MediaTestUtils.createMediaItemWithArtworkData("testZZZ"); + MediaMetadata testMediaMetadata = testMediaItem.mediaMetadata; + @Nullable Bitmap testArtworkBitmap = null; + @Nullable + ListenableFuture bitmapFuture = bitmapLoader.loadBitmapFromMetadata(testMediaMetadata); + if (bitmapFuture != null) { + testArtworkBitmap = bitmapFuture.get(10, SECONDS); + } + MediaDescriptionCompat mediaDescriptionCompat = + MediaUtils.convertToMediaDescriptionCompat(testMediaItem, testArtworkBitmap); + + MediaMetadata mediaMetadata = + MediaUtils.convertToMediaMetadata(mediaDescriptionCompat, RatingCompat.RATING_NONE); + + assertThat(mediaMetadata).isEqualTo(testMediaMetadata); + assertThat(mediaMetadata.artworkData).isNotNull(); + } + + @Test + public void convertToMediaMetadataCompat_withMediaType_setsMediaType() { + MediaItem mediaItem = + new MediaItem.Builder() + .setMediaMetadata( + new MediaMetadata.Builder().setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC).build()) + .build(); + + MediaMetadataCompat mediaMetadataCompat = + MediaUtils.convertToMediaMetadataCompat( + mediaItem, /* durotionsMs= */ C.TIME_UNSET, /* artworkBitmap= */ null); + + assertThat( + mediaMetadataCompat.getLong( + androidx.media3.session.MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT)) + .isEqualTo(MediaMetadata.MEDIA_TYPE_MUSIC); + } + @Test public void convertBetweenRatingAndRatingCompat() { assertRatingEquals(MediaUtils.convertToRating(null), MediaUtils.convertToRatingCompat(null)); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java index 5d9e56a7c70..2a6af52728b 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java @@ -56,7 +56,8 @@ public final class MediaTestUtils { public static MediaItem createMediaItem(String mediaId) { MediaMetadata mediaMetadata = new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_NONE) + .setFolderType(MediaMetadata.FOLDER_TYPE_TITLES) + .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) .setIsPlayable(true) .build(); return new MediaItem.Builder().setMediaId(mediaId).setMediaMetadata(mediaMetadata).build(); @@ -65,7 +66,8 @@ public static MediaItem createMediaItem(String mediaId) { public static MediaItem createMediaItemWithArtworkData(String mediaId) { MediaMetadata.Builder mediaMetadataBuilder = new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_NONE) + .setFolderType(MediaMetadata.FOLDER_TYPE_TITLES) + .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) .setIsPlayable(true); try { byte[] artworkData = From c4f1c047cac65073a34916c818923c70ed3d17d7 Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Fri, 2 Dec 2022 10:19:40 +0000 Subject: [PATCH 052/141] Added cancellation check for MediaBrowserFuture in demo session app When app is deployed with device's screen being off, MainActivity's onStart is called swiftly by its onStop. The onStop method cancels the browserFuture task which in turn "completes" the task. Upon task "completion", pushRoot() runs and then throws error as it calls get() a cancelled task. PiperOrigin-RevId: 492416445 (cherry picked from commit 64603cba8db9fbd9615e19701464c4d0734a86dc) --- .../src/main/java/androidx/media3/demo/session/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt b/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt index 0e7694dc037..810a6ac9b7b 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt @@ -38,7 +38,7 @@ import com.google.common.util.concurrent.ListenableFuture class MainActivity : AppCompatActivity() { private lateinit var browserFuture: ListenableFuture private val browser: MediaBrowser? - get() = if (browserFuture.isDone) browserFuture.get() else null + get() = if (browserFuture.isDone && !browserFuture.isCancelled) browserFuture.get() else null private lateinit var mediaListAdapter: FolderMediaItemArrayAdapter private lateinit var mediaListView: ListView From 8844b4f6466c3c1da769501d1b81f30cdff8e19a Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Fri, 2 Dec 2022 13:14:33 +0000 Subject: [PATCH 053/141] Removed ExoPlayer specific states from SimpleBasePlayer PiperOrigin-RevId: 492443147 (cherry picked from commit 2fd38e3912960c38d75bce32cc275c985a2722c1) --- .../media3/common/SimpleBasePlayer.java | 50 ------------------- .../media3/common/SimpleBasePlayerTest.java | 19 ++----- 2 files changed, 5 insertions(+), 64 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index f42e912fc57..893968b3b0d 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -118,8 +118,6 @@ public static final class Builder { private DeviceInfo deviceInfo; private int deviceVolume; private boolean isDeviceMuted; - private int audioSessionId; - private boolean skipSilenceEnabled; private Size surfaceSize; private boolean newlyRenderedFirstFrame; private Metadata timedMetadata; @@ -164,8 +162,6 @@ public Builder() { deviceInfo = DeviceInfo.UNKNOWN; deviceVolume = 0; isDeviceMuted = false; - audioSessionId = C.AUDIO_SESSION_ID_UNSET; - skipSilenceEnabled = false; surfaceSize = Size.UNKNOWN; newlyRenderedFirstFrame = false; timedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET); @@ -210,8 +206,6 @@ private Builder(State state) { this.deviceInfo = state.deviceInfo; this.deviceVolume = state.deviceVolume; this.isDeviceMuted = state.isDeviceMuted; - this.audioSessionId = state.audioSessionId; - this.skipSilenceEnabled = state.skipSilenceEnabled; this.surfaceSize = state.surfaceSize; this.newlyRenderedFirstFrame = state.newlyRenderedFirstFrame; this.timedMetadata = state.timedMetadata; @@ -497,30 +491,6 @@ public Builder setIsDeviceMuted(boolean isDeviceMuted) { return this; } - /** - * Sets the audio session id. - * - * @param audioSessionId The audio session id. - * @return This builder. - */ - @CanIgnoreReturnValue - public Builder setAudioSessionId(int audioSessionId) { - this.audioSessionId = audioSessionId; - return this; - } - - /** - * Sets whether skipping silences in the audio stream is enabled. - * - * @param skipSilenceEnabled Whether skipping silences in the audio stream is enabled. - * @return This builder. - */ - @CanIgnoreReturnValue - public Builder setSkipSilenceEnabled(boolean skipSilenceEnabled) { - this.skipSilenceEnabled = skipSilenceEnabled; - return this; - } - /** * Sets the size of the surface onto which the video is being rendered. * @@ -851,10 +821,6 @@ public State build() { public final int deviceVolume; /** Whether the device is muted. */ public final boolean isDeviceMuted; - /** The audio session id. */ - public final int audioSessionId; - /** Whether skipping silences in the audio stream is enabled. */ - public final boolean skipSilenceEnabled; /** The size of the surface onto which the video is being rendered. */ public final Size surfaceSize; /** @@ -995,8 +961,6 @@ private State(Builder builder) { this.deviceInfo = builder.deviceInfo; this.deviceVolume = builder.deviceVolume; this.isDeviceMuted = builder.isDeviceMuted; - this.audioSessionId = builder.audioSessionId; - this.skipSilenceEnabled = builder.skipSilenceEnabled; this.surfaceSize = builder.surfaceSize; this.newlyRenderedFirstFrame = builder.newlyRenderedFirstFrame; this.timedMetadata = builder.timedMetadata; @@ -1052,8 +1016,6 @@ public boolean equals(@Nullable Object o) { && deviceInfo.equals(state.deviceInfo) && deviceVolume == state.deviceVolume && isDeviceMuted == state.isDeviceMuted - && audioSessionId == state.audioSessionId - && skipSilenceEnabled == state.skipSilenceEnabled && surfaceSize.equals(state.surfaceSize) && newlyRenderedFirstFrame == state.newlyRenderedFirstFrame && timedMetadata.equals(state.timedMetadata) @@ -1098,8 +1060,6 @@ public int hashCode() { result = 31 * result + deviceInfo.hashCode(); result = 31 * result + deviceVolume; result = 31 * result + (isDeviceMuted ? 1 : 0); - result = 31 * result + audioSessionId; - result = 31 * result + (skipSilenceEnabled ? 1 : 0); result = 31 * result + surfaceSize.hashCode(); result = 31 * result + (newlyRenderedFirstFrame ? 1 : 0); result = 31 * result + timedMetadata.hashCode(); @@ -3006,11 +2966,6 @@ private void updateStateAndInformListeners(State newState) { Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, listener -> listener.onPlaybackParametersChanged(newState.playbackParameters)); } - if (previousState.skipSilenceEnabled != newState.skipSilenceEnabled) { - listeners.queueEvent( - Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED, - listener -> listener.onSkipSilenceEnabledChanged(newState.skipSilenceEnabled)); - } if (previousState.repeatMode != newState.repeatMode) { listeners.queueEvent( Player.EVENT_REPEAT_MODE_CHANGED, @@ -3057,11 +3012,6 @@ private void updateStateAndInformListeners(State newState) { Player.EVENT_PLAYLIST_METADATA_CHANGED, listener -> listener.onPlaylistMetadataChanged(newState.playlistMetadata)); } - if (previousState.audioSessionId != newState.audioSessionId) { - listeners.queueEvent( - Player.EVENT_AUDIO_SESSION_ID, - listener -> listener.onAudioSessionIdChanged(newState.audioSessionId)); - } if (newState.newlyRenderedFirstFrame) { listeners.queueEvent(Player.EVENT_RENDERED_FIRST_FRAME, Listener::onRenderedFirstFrame); } diff --git a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java index dc306838b5a..0ef67a9e7d9 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -107,8 +107,6 @@ public void stateBuildUpon_build_isEqual() { new DeviceInfo( DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 3, /* maxVolume= */ 7)) .setIsDeviceMuted(true) - .setAudioSessionId(78) - .setSkipSilenceEnabled(true) .setSurfaceSize(new Size(480, 360)) .setNewlyRenderedFirstFrame(true) .setTimedMetadata(new Metadata()) @@ -269,8 +267,6 @@ public void stateBuilderBuild_setsCorrectValues() { .setDeviceInfo(deviceInfo) .setDeviceVolume(5) .setIsDeviceMuted(true) - .setAudioSessionId(78) - .setSkipSilenceEnabled(true) .setSurfaceSize(surfaceSize) .setNewlyRenderedFirstFrame(true) .setTimedMetadata(timedMetadata) @@ -311,8 +307,6 @@ public void stateBuilderBuild_setsCorrectValues() { assertThat(state.deviceInfo).isEqualTo(deviceInfo); assertThat(state.deviceVolume).isEqualTo(5); assertThat(state.isDeviceMuted).isTrue(); - assertThat(state.audioSessionId).isEqualTo(78); - assertThat(state.skipSilenceEnabled).isTrue(); assertThat(state.surfaceSize).isEqualTo(surfaceSize); assertThat(state.newlyRenderedFirstFrame).isTrue(); assertThat(state.timedMetadata).isEqualTo(timedMetadata); @@ -865,8 +859,6 @@ public void getterMethods_noOtherMethodCalls_returnCurrentState() { .setDeviceInfo(deviceInfo) .setDeviceVolume(5) .setIsDeviceMuted(true) - .setAudioSessionId(78) - .setSkipSilenceEnabled(true) .setSurfaceSize(surfaceSize) .setPlaylist(playlist) .setPlaylistMetadata(playlistMetadata) @@ -1160,8 +1152,6 @@ public void invalidateState_updatesStateAndInformsListeners() throws Exception { .setDeviceInfo(deviceInfo) .setDeviceVolume(5) .setIsDeviceMuted(true) - .setAudioSessionId(78) - .setSkipSilenceEnabled(true) .setSurfaceSize(surfaceSize) .setNewlyRenderedFirstFrame(true) .setTimedMetadata(timedMetadata) @@ -1227,11 +1217,9 @@ protected State getState() { verify(listener).onMediaMetadataChanged(mediaMetadata); verify(listener).onTracksChanged(tracks); verify(listener).onPlaylistMetadataChanged(playlistMetadata); - verify(listener).onAudioSessionIdChanged(78); verify(listener).onRenderedFirstFrame(); verify(listener).onMetadata(timedMetadata); verify(listener).onSurfaceSizeChanged(surfaceSize.getWidth(), surfaceSize.getHeight()); - verify(listener).onSkipSilenceEnabledChanged(true); verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); verify(listener) .onPositionDiscontinuity( @@ -1284,9 +1272,7 @@ protected State getState() { Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED, Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, Player.EVENT_AUDIO_ATTRIBUTES_CHANGED, - Player.EVENT_AUDIO_SESSION_ID, Player.EVENT_VOLUME_CHANGED, - Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED, Player.EVENT_SURFACE_SIZE_CHANGED, Player.EVENT_VIDEO_SIZE_CHANGED, Player.EVENT_RENDERED_FIRST_FRAME, @@ -1301,6 +1287,11 @@ protected State getState() { if (method.getName().equals("onSeekProcessed")) { continue; } + if (method.getName().equals("onAudioSessionIdChanged") + || method.getName().equals("onSkipSilenceEnabledChanged")) { + // Skip listeners for ExoPlayer-specific states + continue; + } method.invoke(verify(listener), getAnyArguments(method)); } } From 5612f6924a7388e137c44e73751cd50c97cee0f6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 2 Dec 2022 15:29:19 +0000 Subject: [PATCH 054/141] Fix `TextRenderer` exception when a subtitle file contains no cues Discovered while investigating Issue: google/ExoPlayer#10823 Example stack trace with the previous code (I added the index value for debugging): ``` playerFailed [eventTime=44.07, mediaPos=44.01, window=0, period=0, errorCode=ERROR_CODE_FAILED_RUNTIME_CHECK androidx.media3.exoplayer.ExoPlaybackException: Unexpected runtime error at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:635) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loopOnce(Looper.java:202) at android.os.Looper.loop(Looper.java:291) at android.os.HandlerThread.run(HandlerThread.java:67) Caused by: java.lang.IllegalArgumentException: index=-1 at androidx.media3.common.util.Assertions.checkArgument(Assertions.java:55) at androidx.media3.extractor.text.webvtt.WebvttSubtitle.getEventTime(WebvttSubtitle.java:62) at androidx.media3.extractor.text.SubtitleOutputBuffer.getEventTime(SubtitleOutputBuffer.java:56) at androidx.media3.exoplayer.text.TextRenderer.getCurrentEventTimeUs(TextRenderer.java:435) at androidx.media3.exoplayer.text.TextRenderer.render(TextRenderer.java:268) at androidx.media3.exoplayer.ExoPlayerImplInternal.doSomeWork(ExoPlayerImplInternal.java:1008) at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:509) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loopOnce(Looper.java:202) at android.os.Looper.loop(Looper.java:291) at android.os.HandlerThread.run(HandlerThread.java:67) ] ``` #minor-release PiperOrigin-RevId: 492464180 (cherry picked from commit 33bbb9511a9ac6ad6495d4e264f8e248c4342763) --- RELEASENOTES.md | 2 ++ .../main/java/androidx/media3/exoplayer/text/TextRenderer.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ce0948036b7..f26f8fae064 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,8 @@ * Use the compressed audio format bitrate to calculate the min buffer size for `AudioTrack` in direct playbacks (passthrough). * Text: + * Fix `TextRenderer` passing an invalid (negative) index to + `Subtitle.getEventTime` if a subtitle file contains no cues. * SubRip: Add support for UTF-16 files if they start with a byte order mark. * Session: diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextRenderer.java index 2ddbd5908bd..8cd8bb0c373 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextRenderer.java @@ -427,7 +427,7 @@ private void handleDecoderError(SubtitleDecoderException e) { @SideEffectFree private long getCurrentEventTimeUs(long positionUs) { int nextEventTimeIndex = subtitle.getNextEventTimeIndex(positionUs); - if (nextEventTimeIndex == 0) { + if (nextEventTimeIndex == 0 || subtitle.getEventTimeCount() == 0) { return subtitle.timeUs; } From f43cc38ce189a096385190db79c2be8cd689cbb5 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 2 Dec 2022 16:05:02 +0000 Subject: [PATCH 055/141] Fix `ExoPlayerTest` to use `C.TIME_UNSET` instead of `C.POSITION_UNSET` This inconsistency was exposed by an upcoming change to deprecate `POSITION_UNSET` in favour of `INDEX_UNSET` because position is an ambiguous term between 'byte offset' and 'media position', as shown here. PiperOrigin-RevId: 492470241 (cherry picked from commit 2650654dd0d0654fc4cca67b0d3347d88431fa4e) --- .../test/java/androidx/media3/exoplayer/ExoPlayerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index eef9c43b4bf..8aafe98324b 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -2524,7 +2524,7 @@ public void sendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exceptio .build() .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET); + assertThat(target.positionMs).isEqualTo(C.TIME_UNSET); } @Test @@ -2546,7 +2546,7 @@ public void sendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception .build() .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET); + assertThat(target.positionMs).isEqualTo(C.TIME_UNSET); } @Test @@ -12292,7 +12292,7 @@ private static final class PositionGrabbingMessageTarget extends PlayerTarget { public PositionGrabbingMessageTarget() { mediaItemIndex = C.INDEX_UNSET; - positionMs = C.POSITION_UNSET; + positionMs = C.TIME_UNSET; } @Override From 515b6ac595af787b75e62ca8baa2459fefb3e23a Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 2 Dec 2022 16:24:37 +0000 Subject: [PATCH 056/141] Fix threading of onFallbackApplied callback The callback is currently triggered on the ExoPlayer playback thread instead of the app thread that added the listener. PiperOrigin-RevId: 492474405 (cherry picked from commit 634c6161f11f33b960023350d418bd3493f5a4b9) --- .../media3/transformer/FallbackListener.java | 28 +++++++++++------ .../media3/transformer/Transformer.java | 6 +++- .../transformer/FallbackListenerTest.java | 31 ++++++++++++++++--- .../transformer/VideoEncoderWrapperTest.java | 1 + 4 files changed, 51 insertions(+), 15 deletions(-) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java index bd9cf63426c..d601b74a18d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java @@ -20,6 +20,7 @@ import androidx.media3.common.C; import androidx.media3.common.MediaItem; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.ListenerSet; import androidx.media3.common.util.Util; @@ -32,6 +33,7 @@ private final MediaItem mediaItem; private final TransformationRequest originalTransformationRequest; private final ListenerSet transformerListeners; + private final HandlerWrapper transformerListenerHandler; private TransformationRequest fallbackTransformationRequest; private int trackCount; @@ -40,16 +42,20 @@ * Creates a new instance. * * @param mediaItem The {@link MediaItem} to transform. - * @param transformerListeners The {@linkplain Transformer.Listener listeners} to forward events - * to. + * @param transformerListeners The {@linkplain Transformer.Listener listeners} to call {@link + * Transformer.Listener#onFallbackApplied} on. + * @param transformerListenerHandler The {@link HandlerWrapper} to call {@link + * Transformer.Listener#onFallbackApplied} events on. * @param originalTransformationRequest The original {@link TransformationRequest}. */ public FallbackListener( MediaItem mediaItem, ListenerSet transformerListeners, + HandlerWrapper transformerListenerHandler, TransformationRequest originalTransformationRequest) { this.mediaItem = mediaItem; this.transformerListeners = transformerListeners; + this.transformerListenerHandler = transformerListenerHandler; this.originalTransformationRequest = originalTransformationRequest; this.fallbackTransformationRequest = originalTransformationRequest; } @@ -104,15 +110,19 @@ public void onTransformationRequestFinalized(TransformationRequest transformatio fallbackRequestBuilder.setEnableRequestSdrToneMapping( transformationRequest.enableRequestSdrToneMapping); } - fallbackTransformationRequest = fallbackRequestBuilder.build(); + TransformationRequest newFallbackTransformationRequest = fallbackRequestBuilder.build(); + fallbackTransformationRequest = newFallbackTransformationRequest; if (trackCount == 0 && !originalTransformationRequest.equals(fallbackTransformationRequest)) { - transformerListeners.queueEvent( - /* eventFlag= */ C.INDEX_UNSET, - listener -> - listener.onFallbackApplied( - mediaItem, originalTransformationRequest, fallbackTransformationRequest)); - transformerListeners.flushEvents(); + transformerListenerHandler.post( + () -> + transformerListeners.sendEvent( + /* eventFlag= */ C.INDEX_UNSET, + listener -> + listener.onFallbackApplied( + mediaItem, + originalTransformationRequest, + newFallbackTransformationRequest))); } } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index de50b10f564..c9627dd9ff9 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -729,7 +729,11 @@ private void startTransformationInternal(MediaItem mediaItem) { /* asyncErrorListener= */ componentListener); this.muxerWrapper = muxerWrapper; FallbackListener fallbackListener = - new FallbackListener(mediaItem, listeners, transformationRequest); + new FallbackListener( + mediaItem, + listeners, + clock.createHandler(looper, /* callback= */ null), + transformationRequest); exoPlayerAssetLoader.start( mediaItem, muxerWrapper, diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/FallbackListenerTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/FallbackListenerTest.java index e5dc534a8a7..d0b0e4c0f3f 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/FallbackListenerTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/FallbackListenerTest.java @@ -26,10 +26,12 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Clock; +import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.ListenerSet; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowLooper; /** Unit tests for {@link FallbackListener}. */ @RunWith(AndroidJUnit4.class) @@ -41,7 +43,8 @@ public class FallbackListenerTest { public void onTransformationRequestFinalized_withoutTrackRegistration_throwsException() { TransformationRequest transformationRequest = new TransformationRequest.Builder().build(); FallbackListener fallbackListener = - new FallbackListener(PLACEHOLDER_MEDIA_ITEM, createListenerSet(), transformationRequest); + new FallbackListener( + PLACEHOLDER_MEDIA_ITEM, createListenerSet(), createHandler(), transformationRequest); assertThrows( IllegalStateException.class, @@ -52,10 +55,12 @@ public void onTransformationRequestFinalized_withoutTrackRegistration_throwsExce public void onTransformationRequestFinalized_afterTrackRegistration_completesSuccessfully() { TransformationRequest transformationRequest = new TransformationRequest.Builder().build(); FallbackListener fallbackListener = - new FallbackListener(PLACEHOLDER_MEDIA_ITEM, createListenerSet(), transformationRequest); + new FallbackListener( + PLACEHOLDER_MEDIA_ITEM, createListenerSet(), createHandler(), transformationRequest); fallbackListener.registerTrack(); fallbackListener.onTransformationRequestFinalized(transformationRequest); + ShadowLooper.idleMainLooper(); } @Test @@ -66,10 +71,14 @@ public void onTransformationRequestFinalized_withUnchangedRequest_doesNotCallbac Transformer.Listener mockListener = mock(Transformer.Listener.class); FallbackListener fallbackListener = new FallbackListener( - PLACEHOLDER_MEDIA_ITEM, createListenerSet(mockListener), originalRequest); + PLACEHOLDER_MEDIA_ITEM, + createListenerSet(mockListener), + createHandler(), + originalRequest); fallbackListener.registerTrack(); fallbackListener.onTransformationRequestFinalized(unchangedRequest); + ShadowLooper.idleMainLooper(); verify(mockListener, never()).onFallbackApplied(any(), any(), any()); } @@ -83,10 +92,14 @@ public void onTransformationRequestFinalized_withDifferentRequest_callsCallback( Transformer.Listener mockListener = mock(Transformer.Listener.class); FallbackListener fallbackListener = new FallbackListener( - PLACEHOLDER_MEDIA_ITEM, createListenerSet(mockListener), originalRequest); + PLACEHOLDER_MEDIA_ITEM, + createListenerSet(mockListener), + createHandler(), + originalRequest); fallbackListener.registerTrack(); fallbackListener.onTransformationRequestFinalized(audioFallbackRequest); + ShadowLooper.idleMainLooper(); verify(mockListener) .onFallbackApplied(PLACEHOLDER_MEDIA_ITEM, originalRequest, audioFallbackRequest); @@ -109,12 +122,16 @@ public void onTransformationRequestFinalized_withDifferentRequest_callsCallback( Transformer.Listener mockListener = mock(Transformer.Listener.class); FallbackListener fallbackListener = new FallbackListener( - PLACEHOLDER_MEDIA_ITEM, createListenerSet(mockListener), originalRequest); + PLACEHOLDER_MEDIA_ITEM, + createListenerSet(mockListener), + createHandler(), + originalRequest); fallbackListener.registerTrack(); fallbackListener.registerTrack(); fallbackListener.onTransformationRequestFinalized(audioFallbackRequest); fallbackListener.onTransformationRequestFinalized(videoFallbackRequest); + ShadowLooper.idleMainLooper(); verify(mockListener) .onFallbackApplied(PLACEHOLDER_MEDIA_ITEM, originalRequest, mergedFallbackRequest); @@ -130,4 +147,8 @@ private static ListenerSet createListenerSet( private static ListenerSet createListenerSet() { return new ListenerSet<>(Looper.myLooper(), Clock.DEFAULT, (listener, flags) -> {}); } + + private static HandlerWrapper createHandler() { + return Clock.DEFAULT.createHandler(Looper.myLooper(), /* callback= */ null); + } } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/VideoEncoderWrapperTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/VideoEncoderWrapperTest.java index e92ed9db6a9..065741b6596 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/VideoEncoderWrapperTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/VideoEncoderWrapperTest.java @@ -45,6 +45,7 @@ public final class VideoEncoderWrapperTest { new FallbackListener( MediaItem.fromUri(Uri.EMPTY), new ListenerSet<>(Looper.myLooper(), Clock.DEFAULT, (listener, flags) -> {}), + Clock.DEFAULT.createHandler(Looper.myLooper(), /* callback= */ null), emptyTransformationRequest); private final VideoTranscodingSamplePipeline.EncoderWrapper encoderWrapper = new VideoTranscodingSamplePipeline.EncoderWrapper( From 3df6949c52d9413caf3b4dc4db514456de7484ba Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 6 Dec 2022 13:22:18 +0000 Subject: [PATCH 057/141] Add javadoc links to README files Fix some other link titles and destinations spotted along the way. #minor-release PiperOrigin-RevId: 493276172 (cherry picked from commit 636a4a8538ccfb235eeca7d9131d4b5d4d95e9aa) --- libraries/cast/README.md | 6 ++++++ libraries/common/README.md | 6 ++++++ libraries/database/README.md | 5 ++--- libraries/datasource/README.md | 6 ++++++ libraries/datasource_cronet/README.md | 6 ++++++ libraries/datasource_okhttp/README.md | 6 ++++++ libraries/datasource_rtmp/README.md | 6 ++++++ libraries/decoder/README.md | 6 ++++++ libraries/decoder_av1/README.md | 8 ++++++++ libraries/decoder_ffmpeg/README.md | 8 ++++++++ libraries/decoder_flac/README.md | 8 ++++++++ libraries/decoder_opus/README.md | 8 ++++++++ libraries/decoder_vp9/README.md | 8 ++++++++ libraries/effect/README.md | 6 ++++++ libraries/exoplayer/README.md | 6 ++++++ libraries/exoplayer_dash/README.md | 8 ++++++++ libraries/exoplayer_hls/README.md | 8 ++++++++ libraries/exoplayer_ima/README.md | 8 ++++++++ libraries/exoplayer_rtsp/README.md | 8 ++++++++ libraries/exoplayer_smoothstreaming/README.md | 8 ++++++++ libraries/exoplayer_workmanager/README.md | 6 ++++++ libraries/extractor/README.md | 6 ++++++ libraries/test_utils/README.md | 6 ++++++ libraries/test_utils_robolectric/README.md | 6 ++++++ libraries/transformer/README.md | 8 ++++++++ libraries/ui/README.md | 8 ++++++++ 26 files changed, 176 insertions(+), 3 deletions(-) diff --git a/libraries/cast/README.md b/libraries/cast/README.md index ccd919b9b7c..d8b25289b7c 100644 --- a/libraries/cast/README.md +++ b/libraries/cast/README.md @@ -27,3 +27,9 @@ Create a `CastPlayer` and use it to control a Cast receiver app. Since `CastPlayer` implements the `Player` interface, it can be passed to all media components that accept a `Player`, including the UI components provided by the UI module. + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/common/README.md b/libraries/common/README.md index 216342644a4..f2ef17bac60 100644 --- a/libraries/common/README.md +++ b/libraries/common/README.md @@ -2,3 +2,9 @@ Provides common code and utilities used by other media modules. Application code will not normally need to depend on this module directly. + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/database/README.md b/libraries/database/README.md index e0c51e762f7..793664d5adf 100644 --- a/libraries/database/README.md +++ b/libraries/database/README.md @@ -5,7 +5,6 @@ will not normally need to depend on this module directly. ## Links -* [Javadoc][]: Classes matching `androidx.media3.database.*` belong to this - module. +* [Javadoc][] -[Javadoc]: https://exoplayer.dev/doc/reference/index.html +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/datasource/README.md b/libraries/datasource/README.md index 42e17153e73..4e750828319 100644 --- a/libraries/datasource/README.md +++ b/libraries/datasource/README.md @@ -3,3 +3,9 @@ Provides a `DataSource` abstraction and a number of concrete implementations for reading data from different sources. Application code will not normally need to depend on this module directly. + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/datasource_cronet/README.md b/libraries/datasource_cronet/README.md index c64f8b3baea..4a5dbbd674f 100644 --- a/libraries/datasource_cronet/README.md +++ b/libraries/datasource_cronet/README.md @@ -119,3 +119,9 @@ whilst still using Cronet Fallback for other networking performed by your application. [Send a simple request]: https://developer.android.com/guide/topics/connectivity/cronet/start + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/datasource_okhttp/README.md b/libraries/datasource_okhttp/README.md index 6be5b521371..cb62baa0b40 100644 --- a/libraries/datasource_okhttp/README.md +++ b/libraries/datasource_okhttp/README.md @@ -48,3 +48,9 @@ new DefaultDataSourceFactory( ... /* baseDataSourceFactory= */ new OkHttpDataSource.Factory(...)); ``` + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/datasource_rtmp/README.md b/libraries/datasource_rtmp/README.md index 7f52a665b3d..27890cff867 100644 --- a/libraries/datasource_rtmp/README.md +++ b/libraries/datasource_rtmp/README.md @@ -45,3 +45,9 @@ application code are required. Alternatively, if you know that your application doesn't need to handle any other protocols, you can update any `DataSource.Factory` instantiations in your application code to use `RtmpDataSource.Factory` directly. + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/decoder/README.md b/libraries/decoder/README.md index 7d738f52309..150fcef72ac 100644 --- a/libraries/decoder/README.md +++ b/libraries/decoder/README.md @@ -2,3 +2,9 @@ Provides a decoder abstraction. Application code will not normally need to depend on this module directly. + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/decoder_av1/README.md b/libraries/decoder_av1/README.md index 75e89ad5f55..a4f741490ae 100644 --- a/libraries/decoder_av1/README.md +++ b/libraries/decoder_av1/README.md @@ -123,3 +123,11 @@ gets from the libgav1 decoder: Note: Although the default option uses `ANativeWindow`, based on our testing the GL rendering mode has better performance, so should be preferred + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/decoder_ffmpeg/README.md b/libraries/decoder_ffmpeg/README.md index 21127e65c9d..a819fc23adb 100644 --- a/libraries/decoder_ffmpeg/README.md +++ b/libraries/decoder_ffmpeg/README.md @@ -116,3 +116,11 @@ then implement your own logic to use the renderer for a given track. [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html [ExoPlayer issue 2781]: https://github.com/google/ExoPlayer/issues/2781 [Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/decoder_flac/README.md b/libraries/decoder_flac/README.md index 6d1046a0737..e381d2f8e1f 100644 --- a/libraries/decoder_flac/README.md +++ b/libraries/decoder_flac/README.md @@ -95,3 +95,11 @@ Note: These instructions assume you're using `DefaultTrackSelector`. If you have a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibflacAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/decoder_opus/README.md b/libraries/decoder_opus/README.md index 44845605c6a..26195664a89 100644 --- a/libraries/decoder_opus/README.md +++ b/libraries/decoder_opus/README.md @@ -99,3 +99,11 @@ Note: These instructions assume you're using `DefaultTrackSelector`. If you have a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibopusAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/decoder_vp9/README.md b/libraries/decoder_vp9/README.md index fc63129a9d9..e504c7a730a 100644 --- a/libraries/decoder_vp9/README.md +++ b/libraries/decoder_vp9/README.md @@ -136,3 +136,11 @@ gets from the libvpx decoder: Note: Although the default option uses `ANativeWindow`, based on our testing the GL rendering mode has better performance, so should be preferred. + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/effect/README.md b/libraries/effect/README.md index 50fc67fe3ba..532c8f61b86 100644 --- a/libraries/effect/README.md +++ b/libraries/effect/README.md @@ -17,3 +17,9 @@ Alternatively, you can clone this GitHub project and depend on the module locally. Instructions for doing this can be found in the [top level README][]. [top level README]: ../../README.md + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/exoplayer/README.md b/libraries/exoplayer/README.md index 6f6b0d3b6ad..0fca23f3661 100644 --- a/libraries/exoplayer/README.md +++ b/libraries/exoplayer/README.md @@ -18,3 +18,9 @@ Alternatively, you can clone this GitHub project and depend on the module locally. Instructions for doing this can be found in the [top level README][]. [top level README]: ../../README.md + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/exoplayer_dash/README.md b/libraries/exoplayer_dash/README.md index 8c52a839632..3f7ec5035fc 100644 --- a/libraries/exoplayer_dash/README.md +++ b/libraries/exoplayer_dash/README.md @@ -33,3 +33,11 @@ the module and build `DashDownloader` instances to download DASH content. For advanced playback use cases, applications can build `DashMediaSource` instances and pass them directly to the player. For advanced download use cases, `DashDownloader` can be used directly. + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/exoplayer_hls/README.md b/libraries/exoplayer_hls/README.md index 34f31c312d3..f89d324d06f 100644 --- a/libraries/exoplayer_hls/README.md +++ b/libraries/exoplayer_hls/README.md @@ -32,3 +32,11 @@ the module and build `HlsDownloader` instances to download HLS content. For advanced playback use cases, applications can build `HlsMediaSource` instances and pass them directly to the player. For advanced download use cases, `HlsDownloader` can be used directly. + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/exoplayer_ima/README.md b/libraries/exoplayer_ima/README.md index b2143d26e31..06ada682a74 100644 --- a/libraries/exoplayer_ima/README.md +++ b/libraries/exoplayer_ima/README.md @@ -49,3 +49,11 @@ You can try the IMA module in the ExoPlayer demo app, which has test content in the "IMA sample ad tags" section of the sample chooser. The demo app's `PlayerActivity` also shows how to persist the `ImaAdsLoader` instance and the player position when backgrounded during ad playback. + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/exoplayer_rtsp/README.md b/libraries/exoplayer_rtsp/README.md index 04bfa676623..f83220fe1d0 100644 --- a/libraries/exoplayer_rtsp/README.md +++ b/libraries/exoplayer_rtsp/README.md @@ -27,3 +27,11 @@ and convert a RTSP `MediaItem` into a `RtspMediaSource` for playback. For advanced playback use cases, applications can build `RtspMediaSource` instances and pass them directly to the player. + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/exoplayer_smoothstreaming/README.md b/libraries/exoplayer_smoothstreaming/README.md index 1650a22881e..076985bee71 100644 --- a/libraries/exoplayer_smoothstreaming/README.md +++ b/libraries/exoplayer_smoothstreaming/README.md @@ -32,3 +32,11 @@ content. For advanced playback use cases, applications can build `SsMediaSource` instances and pass them directly to the player. For advanced download use cases, `SsDownloader` can be used directly. + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/exoplayer_workmanager/README.md b/libraries/exoplayer_workmanager/README.md index ed3d5dd3a5d..7fa6c6d2671 100644 --- a/libraries/exoplayer_workmanager/README.md +++ b/libraries/exoplayer_workmanager/README.md @@ -19,3 +19,9 @@ Alternatively, you can clone this GitHub project and depend on the module locally. Instructions for doing this can be found in the [top level README][]. [top level README]: ../../README.md + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/extractor/README.md b/libraries/extractor/README.md index cd153181924..22b82fa5b5d 100644 --- a/libraries/extractor/README.md +++ b/libraries/extractor/README.md @@ -2,3 +2,9 @@ Provides media container extractors and related utilities. Application code will not normally need to depend on this module directly. + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/test_utils/README.md b/libraries/test_utils/README.md index b0fa26d687a..f8d78bb573b 100644 --- a/libraries/test_utils/README.md +++ b/libraries/test_utils/README.md @@ -1,3 +1,9 @@ # Test utils module Provides utility classes for media unit and instrumentation tests. + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/test_utils_robolectric/README.md b/libraries/test_utils_robolectric/README.md index 48d5e100367..4dbd123eeee 100644 --- a/libraries/test_utils_robolectric/README.md +++ b/libraries/test_utils_robolectric/README.md @@ -1,3 +1,9 @@ # Robolectric test utils module Provides test infrastructure for Robolectric-based media tests. + +## Links + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/transformer/README.md b/libraries/transformer/README.md index 0ff37e7aeae..2edafcd3869 100644 --- a/libraries/transformer/README.md +++ b/libraries/transformer/README.md @@ -17,3 +17,11 @@ Alternatively, you can clone this GitHub project and depend on the module locally. Instructions for doing this can be found in the [top level README][]. [top level README]: ../../README.md + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages diff --git a/libraries/ui/README.md b/libraries/ui/README.md index 0f09831abda..fe864c584d3 100644 --- a/libraries/ui/README.md +++ b/libraries/ui/README.md @@ -17,3 +17,11 @@ Alternatively, you can clone this GitHub project and depend on the module locally. Instructions for doing this can be found in the [top level README][]. [top level README]: ../../README.md + +## Links + + + +* [Javadoc][] + +[Javadoc]: https://developer.android.com/reference/androidx/media3/packages From 3b55ce2a603debb1b420ef08397698a4eb6ac812 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 7 Dec 2022 10:19:17 +0000 Subject: [PATCH 058/141] Support release in SimpleBasePlayer This adds support for the release handling. To align with the established behavior in ExoPlayer, the player can only call listeners from within the release methods (and not afterwards) and automatically enforces an IDLE state (without listener call) in case getters of the player are used after release. PiperOrigin-RevId: 493543958 (cherry picked from commit 4895bc42ff656ba77b604d8c7c93cba64733cc7a) --- .../media3/common/SimpleBasePlayer.java | 79 ++++++--- .../media3/common/SimpleBasePlayerTest.java | 151 ++++++++++++++++++ 2 files changed, 208 insertions(+), 22 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index 893968b3b0d..731dca56308 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -1937,6 +1937,7 @@ static PositionSupplier getExtrapolating(long currentPositionMs, float playbackS private final Timeline.Period period; private @MonotonicNonNull State state; + private boolean released; /** * Creates the base class. @@ -1999,7 +2000,7 @@ public final void setPlayWhenReady(boolean playWhenReady) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_PLAY_PAUSE)) { + if (!shouldHandleCommand(Player.COMMAND_PLAY_PAUSE)) { return; } updateStateForPendingOperation( @@ -2053,7 +2054,7 @@ public final void prepare() { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_PREPARE)) { + if (!shouldHandleCommand(Player.COMMAND_PREPARE)) { return; } updateStateForPendingOperation( @@ -2091,7 +2092,7 @@ public final void setRepeatMode(@Player.RepeatMode int repeatMode) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_REPEAT_MODE)) { + if (!shouldHandleCommand(Player.COMMAND_SET_REPEAT_MODE)) { return; } updateStateForPendingOperation( @@ -2111,7 +2112,7 @@ public final void setShuffleModeEnabled(boolean shuffleModeEnabled) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_SHUFFLE_MODE)) { + if (!shouldHandleCommand(Player.COMMAND_SET_SHUFFLE_MODE)) { return; } updateStateForPendingOperation( @@ -2167,7 +2168,7 @@ public final void setPlaybackParameters(PlaybackParameters playbackParameters) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_SPEED_AND_PITCH)) { + if (!shouldHandleCommand(Player.COMMAND_SET_SPEED_AND_PITCH)) { return; } updateStateForPendingOperation( @@ -2187,7 +2188,7 @@ public final void stop() { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_STOP)) { + if (!shouldHandleCommand(Player.COMMAND_STOP)) { return; } updateStateForPendingOperation( @@ -2212,8 +2213,25 @@ public final void stop(boolean reset) { @Override public final void release() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (released) { // TODO(b/261158047): Replace by !shouldHandleCommand(Player.COMMAND_RELEASE) + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleRelease(), /* placeholderStateSupplier= */ () -> state); + released = true; + listeners.release(); + // Enforce some final state values in case getters are called after release. + this.state = + this.state + .buildUpon() + .setPlaybackState(Player.STATE_IDLE) + .setTotalBufferedDurationMs(PositionSupplier.ZERO) + .setContentBufferedPositionMs(state.contentPositionMsSupplier) + .setAdBufferedPositionMs(state.adPositionMsSupplier) + .build(); } @Override @@ -2233,7 +2251,7 @@ public final void setTrackSelectionParameters(TrackSelectionParameters parameter verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { + if (!shouldHandleCommand(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { return; } updateStateForPendingOperation( @@ -2259,7 +2277,7 @@ public final void setPlaylistMetadata(MediaMetadata mediaMetadata) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_MEDIA_ITEMS_METADATA)) { + if (!shouldHandleCommand(Player.COMMAND_SET_MEDIA_ITEMS_METADATA)) { return; } updateStateForPendingOperation( @@ -2360,7 +2378,7 @@ public final void setVolume(float volume) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_VOLUME)) { + if (!shouldHandleCommand(Player.COMMAND_SET_VOLUME)) { return; } updateStateForPendingOperation( @@ -2379,7 +2397,7 @@ public final void setVideoSurface(@Nullable Surface surface) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + if (!shouldHandleCommand(Player.COMMAND_SET_VIDEO_SURFACE)) { return; } if (surface == null) { @@ -2397,7 +2415,7 @@ public final void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + if (!shouldHandleCommand(Player.COMMAND_SET_VIDEO_SURFACE)) { return; } if (surfaceHolder == null) { @@ -2415,7 +2433,7 @@ public final void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + if (!shouldHandleCommand(Player.COMMAND_SET_VIDEO_SURFACE)) { return; } if (surfaceView == null) { @@ -2436,7 +2454,7 @@ public final void setVideoTextureView(@Nullable TextureView textureView) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + if (!shouldHandleCommand(Player.COMMAND_SET_VIDEO_SURFACE)) { return; } if (textureView == null) { @@ -2484,7 +2502,7 @@ private void clearVideoOutput(@Nullable Object videoOutput) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + if (!shouldHandleCommand(Player.COMMAND_SET_VIDEO_SURFACE)) { return; } updateStateForPendingOperation( @@ -2533,7 +2551,7 @@ public final void setDeviceVolume(int volume) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_SET_DEVICE_VOLUME)) { + if (!shouldHandleCommand(Player.COMMAND_SET_DEVICE_VOLUME)) { return; } updateStateForPendingOperation( @@ -2546,7 +2564,7 @@ public final void increaseDeviceVolume() { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { + if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { return; } updateStateForPendingOperation( @@ -2560,7 +2578,7 @@ public final void decreaseDeviceVolume() { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { + if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { return; } updateStateForPendingOperation( @@ -2574,7 +2592,7 @@ public final void setDeviceMuted(boolean muted) { verifyApplicationThreadAndInitState(); // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; - if (!state.availableCommands.contains(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { + if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { return; } updateStateForPendingOperation( @@ -2593,7 +2611,7 @@ public final void setDeviceMuted(boolean muted) { */ protected final void invalidateState() { verifyApplicationThreadAndInitState(); - if (!pendingOperations.isEmpty()) { + if (!pendingOperations.isEmpty() || released) { return; } updateStateAndInformListeners(getState()); @@ -2672,6 +2690,18 @@ protected ListenableFuture handleStop() { throw new IllegalStateException(); } + /** + * Handles calls to {@link Player#release}. + * + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + // TODO(b/261158047): Add that this method will only be called if COMMAND_RELEASE is available. + @ForOverride + protected ListenableFuture handleRelease() { + throw new IllegalStateException(); + } + /** * Handles calls to {@link Player#setRepeatMode}. * @@ -2844,6 +2874,11 @@ protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutpu throw new IllegalStateException(); } + @RequiresNonNull("state") + private boolean shouldHandleCommand(@Player.Command int commandCode) { + return !released && state.availableCommands.contains(commandCode); + } + @SuppressWarnings("deprecation") // Calling deprecated listener methods. @RequiresNonNull("state") private void updateStateAndInformListeners(State newState) { @@ -3088,7 +3123,7 @@ private void updateStateForPendingOperation( () -> { castNonNull(state); // Already checked by method @RequiresNonNull pre-condition. pendingOperations.remove(pendingOperation); - if (pendingOperations.isEmpty()) { + if (pendingOperations.isEmpty() && !released) { updateStateAndInformListeners(getState()); } }, diff --git a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java index 0ef67a9e7d9..5c53e5e27b7 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -50,6 +51,7 @@ import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.shadows.ShadowLooper; @@ -2162,6 +2164,155 @@ protected ListenableFuture handleStop() { assertThat(callForwarded.get()).isFalse(); } + @Test + public void release_immediateHandling_updatesStateInformsListenersAndReturnsIdle() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaybackState(Player.STATE_READY) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .build(); + State updatedState = state.buildUpon().setRepeatMode(Player.REPEAT_MODE_ALL).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleRelease() { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.release(); + + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verify(listener).onEvents(eq(player), any()); + verifyNoMoreInteractions(listener); + } + + @Test + public void release_asyncHandling_returnsIdleAndIgnoredAsyncStateUpdate() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaybackState(Player.STATE_READY) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .build(); + // Additionally set the repeat mode to see a difference between the placeholder and new state. + State updatedState = state.buildUpon().setRepeatMode(Player.REPEAT_MODE_ALL).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleRelease() { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.release(); + + // Verify initial change to IDLE without listener call. + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify no further update happened. + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF); + verifyNoMoreInteractions(listener); + } + + @Ignore("b/261158047: Ignore test while Player.COMMAND_RELEASE doesn't exist.") + @Test + public void release_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + // TODO(b/261158047): Uncomment once test is no longer ignored. + // .setAvailableCommands( + // new Commands.Builder().addAllCommands().remove(Player.COMMAND_RELEASE).build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleRelease() { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.release(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void release_withSubsequentPlayerAction_ignoresSubsequentAction() { + AtomicBoolean releaseCalled = new AtomicBoolean(); + AtomicBoolean getStateCalledAfterRelease = new AtomicBoolean(); + AtomicBoolean handlePlayWhenReadyCalledAfterRelease = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + if (releaseCalled.get()) { + getStateCalledAfterRelease.set(true); + } + return new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + } + + @Override + protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { + if (releaseCalled.get()) { + handlePlayWhenReadyCalledAfterRelease.set(true); + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleRelease() { + return Futures.immediateVoidFuture(); + } + }; + + player.release(); + releaseCalled.set(true); + // Try triggering a regular player action and to invalidate the state manually. + player.setPlayWhenReady(true); + player.invalidateState(); + + assertThat(getStateCalledAfterRelease.get()).isFalse(); + assertThat(handlePlayWhenReadyCalledAfterRelease.get()).isFalse(); + } + @Test public void setRepeatMode_immediateHandling_updatesStateAndInformsListeners() { State state = From 71a1254514a4b08e5100f9fd6eca57aeb5513852 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 7 Dec 2022 10:22:45 +0000 Subject: [PATCH 059/141] Replace MediaMetadata folderType by isBrowsable The folder type has a mix of information about the item. It shows whether the item is browsable (type != FOLDER_TYPE_NONE) and which Bluetooth folder type to set for legacy session information. It's a lot clearer to split this into a boolean isBrowsable and use the existing mediaType to map back to the bluetooth folder type where required. folderType is not marked as deprecated yet as this would be an API change, which will be done later. PiperOrigin-RevId: 493544589 (cherry picked from commit ae8000aecaee725dea51a6ded06125884a5b8112) --- RELEASENOTES.md | 3 + .../media3/demo/session/MediaItemTree.kt | 37 ++--- .../androidx/media3/common/MediaMetadata.java | 132 +++++++++++++++++- .../media3/common/MediaMetadataTest.java | 57 ++++++++ .../media3/session/LibraryResult.java | 9 +- .../session/MediaBrowserImplLegacy.java | 3 +- .../media3/session/MediaConstants.java | 4 +- .../media3/session/MediaLibraryService.java | 5 +- .../androidx/media3/session/MediaUtils.java | 31 ++-- .../media3/session/LibraryResultTest.java | 10 +- .../session/MediaBrowserListenerTest.java | 3 +- ...iceCompatCallbackWithMediaBrowserTest.java | 2 +- .../MediaSessionServiceNotificationTest.java | 2 - .../media3/session/MediaTestUtils.java | 12 +- .../session/MockMediaLibraryService.java | 19 +-- 15 files changed, 251 insertions(+), 78 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f26f8fae064..e3b7ce1b0b2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,9 @@ ID3 v2.4. * Add `MediaMetadata.mediaType` to denote the type of content or the type of folder described by the metadata. + * Add `MediaMetadata.isBrowsable` as a replacement for + `MediaMetadata.folderType`. The folder type will be deprecated in the + next release. * Cast extension * Bump Cast SDK version to 21.2.0. diff --git a/demos/session/src/main/java/androidx/media3/demo/session/MediaItemTree.kt b/demos/session/src/main/java/androidx/media3/demo/session/MediaItemTree.kt index d1ece8ba12d..a1a6c6c187d 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/MediaItemTree.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/MediaItemTree.kt @@ -20,11 +20,6 @@ import android.net.Uri import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem.SubtitleConfiguration import androidx.media3.common.MediaMetadata -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_GENRES -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED -import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE import androidx.media3.common.util.Util import com.google.common.collect.ImmutableList import org.json.JSONObject @@ -67,7 +62,8 @@ object MediaItemTree { title: String, mediaId: String, isPlayable: Boolean, - @MediaMetadata.FolderType folderType: Int, + isBrowsable: Boolean, + mediaType: @MediaMetadata.MediaType Int, subtitleConfigurations: List = mutableListOf(), album: String? = null, artist: String? = null, @@ -81,9 +77,10 @@ object MediaItemTree { .setTitle(title) .setArtist(artist) .setGenre(genre) - .setFolderType(folderType) + .setIsBrowsable(isBrowsable) .setIsPlayable(isPlayable) .setArtworkUri(imageUri) + .setMediaType(mediaType) .build() return MediaItem.Builder() @@ -109,7 +106,8 @@ object MediaItemTree { title = "Root Folder", mediaId = ROOT_ID, isPlayable = false, - folderType = FOLDER_TYPE_MIXED + isBrowsable = true, + mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED ) ) treeNodes[ALBUM_ID] = @@ -118,7 +116,8 @@ object MediaItemTree { title = "Album Folder", mediaId = ALBUM_ID, isPlayable = false, - folderType = FOLDER_TYPE_MIXED + isBrowsable = true, + mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS ) ) treeNodes[ARTIST_ID] = @@ -127,7 +126,8 @@ object MediaItemTree { title = "Artist Folder", mediaId = ARTIST_ID, isPlayable = false, - folderType = FOLDER_TYPE_MIXED + isBrowsable = true, + mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS ) ) treeNodes[GENRE_ID] = @@ -136,7 +136,8 @@ object MediaItemTree { title = "Genre Folder", mediaId = GENRE_ID, isPlayable = false, - folderType = FOLDER_TYPE_MIXED + isBrowsable = true, + mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_GENRES ) ) treeNodes[ROOT_ID]!!.addChild(ALBUM_ID) @@ -188,7 +189,8 @@ object MediaItemTree { title = title, mediaId = idInTree, isPlayable = true, - folderType = FOLDER_TYPE_NONE, + isBrowsable = false, + mediaType = MediaMetadata.MEDIA_TYPE_MUSIC, subtitleConfigurations, album = album, artist = artist, @@ -207,7 +209,8 @@ object MediaItemTree { title = album, mediaId = albumFolderIdInTree, isPlayable = true, - folderType = FOLDER_TYPE_ALBUMS, + isBrowsable = true, + mediaType = MediaMetadata.MEDIA_TYPE_ALBUM, subtitleConfigurations ) ) @@ -223,7 +226,8 @@ object MediaItemTree { title = artist, mediaId = artistFolderIdInTree, isPlayable = true, - folderType = FOLDER_TYPE_ARTISTS, + isBrowsable = true, + mediaType = MediaMetadata.MEDIA_TYPE_ARTIST, subtitleConfigurations ) ) @@ -239,7 +243,8 @@ object MediaItemTree { title = genre, mediaId = genreFolderIdInTree, isPlayable = true, - folderType = FOLDER_TYPE_GENRES, + isBrowsable = true, + mediaType = MediaMetadata.MEDIA_TYPE_GENRE, subtitleConfigurations ) ) @@ -262,7 +267,7 @@ object MediaItemTree { fun getRandomItem(): MediaItem { var curRoot = getRootItem() - while (curRoot.mediaMetadata.folderType != FOLDER_TYPE_NONE) { + while (curRoot.mediaMetadata.isBrowsable == true) { val children = getChildren(curRoot.mediaId)!! curRoot = children.random() } diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java index b1d23866a06..470ed7a71c1 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java @@ -61,6 +61,7 @@ public static final class Builder { @Nullable private Integer trackNumber; @Nullable private Integer totalTrackCount; @Nullable private @FolderType Integer folderType; + @Nullable private Boolean isBrowsable; @Nullable private Boolean isPlayable; @Nullable private Integer recordingYear; @Nullable private Integer recordingMonth; @@ -97,6 +98,7 @@ private Builder(MediaMetadata mediaMetadata) { this.trackNumber = mediaMetadata.trackNumber; this.totalTrackCount = mediaMetadata.totalTrackCount; this.folderType = mediaMetadata.folderType; + this.isBrowsable = mediaMetadata.isBrowsable; this.isPlayable = mediaMetadata.isPlayable; this.recordingYear = mediaMetadata.recordingYear; this.recordingMonth = mediaMetadata.recordingMonth; @@ -246,13 +248,26 @@ public Builder setTotalTrackCount(@Nullable Integer totalTrackCount) { return this; } - /** Sets the {@link FolderType}. */ + /** + * Sets the {@link FolderType}. + * + *

    This method will be deprecated. Use {@link #setIsBrowsable} to indicate if an item is a + * browsable folder and use {@link #setMediaType} to indicate the type of the folder. + */ @CanIgnoreReturnValue public Builder setFolderType(@Nullable @FolderType Integer folderType) { this.folderType = folderType; return this; } + /** Sets whether the media is a browsable folder. */ + @UnstableApi + @CanIgnoreReturnValue + public Builder setIsBrowsable(@Nullable Boolean isBrowsable) { + this.isBrowsable = isBrowsable; + return this; + } + /** Sets whether the media is playable. */ @CanIgnoreReturnValue public Builder setIsPlayable(@Nullable Boolean isPlayable) { @@ -491,6 +506,9 @@ public Builder populate(@Nullable MediaMetadata mediaMetadata) { if (mediaMetadata.folderType != null) { setFolderType(mediaMetadata.folderType); } + if (mediaMetadata.isBrowsable != null) { + setIsBrowsable(mediaMetadata.isBrowsable); + } if (mediaMetadata.isPlayable != null) { setIsPlayable(mediaMetadata.isPlayable); } @@ -874,9 +892,16 @@ public MediaMetadata build() { @Nullable public final Integer trackNumber; /** Optional total number of tracks. */ @Nullable public final Integer totalTrackCount; - /** Optional {@link FolderType}. */ + /** + * Optional {@link FolderType}. + * + *

    This field will be deprecated. Use {@link #isBrowsable} to indicate if an item is a + * browsable folder and use {@link #mediaType} to indicate the type of the folder. + */ @Nullable public final @FolderType Integer folderType; - /** Optional boolean for media playability. */ + /** Optional boolean to indicate that the media is a browsable folder. */ + @UnstableApi @Nullable public final Boolean isBrowsable; + /** Optional boolean to indicate that the media is playable. */ @Nullable public final Boolean isPlayable; /** * @deprecated Use {@link #recordingYear} instead. @@ -939,6 +964,22 @@ public MediaMetadata build() { @Nullable public final Bundle extras; private MediaMetadata(Builder builder) { + // Handle compatibility for deprecated fields. + @Nullable Boolean isBrowsable = builder.isBrowsable; + @Nullable Integer folderType = builder.folderType; + @Nullable Integer mediaType = builder.mediaType; + if (isBrowsable != null) { + if (!isBrowsable) { + folderType = FOLDER_TYPE_NONE; + } else if (folderType == null || folderType == FOLDER_TYPE_NONE) { + folderType = mediaType != null ? getFolderTypeFromMediaType(mediaType) : FOLDER_TYPE_MIXED; + } + } else if (folderType != null) { + isBrowsable = folderType != FOLDER_TYPE_NONE; + if (isBrowsable && mediaType == null) { + mediaType = getMediaTypeFromFolderType(folderType); + } + } this.title = builder.title; this.artist = builder.artist; this.albumTitle = builder.albumTitle; @@ -953,7 +994,8 @@ private MediaMetadata(Builder builder) { this.artworkUri = builder.artworkUri; this.trackNumber = builder.trackNumber; this.totalTrackCount = builder.totalTrackCount; - this.folderType = builder.folderType; + this.folderType = folderType; + this.isBrowsable = isBrowsable; this.isPlayable = builder.isPlayable; this.year = builder.recordingYear; this.recordingYear = builder.recordingYear; @@ -970,7 +1012,7 @@ private MediaMetadata(Builder builder) { this.genre = builder.genre; this.compilation = builder.compilation; this.station = builder.station; - this.mediaType = builder.mediaType; + this.mediaType = mediaType; this.extras = builder.extras; } @@ -1003,6 +1045,7 @@ public boolean equals(@Nullable Object obj) { && Util.areEqual(trackNumber, that.trackNumber) && Util.areEqual(totalTrackCount, that.totalTrackCount) && Util.areEqual(folderType, that.folderType) + && Util.areEqual(isBrowsable, that.isBrowsable) && Util.areEqual(isPlayable, that.isPlayable) && Util.areEqual(recordingYear, that.recordingYear) && Util.areEqual(recordingMonth, that.recordingMonth) @@ -1039,6 +1082,7 @@ public int hashCode() { trackNumber, totalTrackCount, folderType, + isBrowsable, isPlayable, recordingYear, recordingMonth, @@ -1095,6 +1139,7 @@ public int hashCode() { FIELD_COMPILATION, FIELD_STATION, FIELD_MEDIA_TYPE, + FIELD_IS_BROWSABLE, FIELD_EXTRAS, }) private @interface FieldNumber {} @@ -1131,6 +1176,7 @@ public int hashCode() { private static final int FIELD_ARTWORK_DATA_TYPE = 29; private static final int FIELD_STATION = 30; private static final int FIELD_MEDIA_TYPE = 31; + private static final int FIELD_IS_BROWSABLE = 32; private static final int FIELD_EXTRAS = 1000; @UnstableApi @@ -1168,6 +1214,9 @@ public Bundle toBundle() { if (folderType != null) { bundle.putInt(keyForField(FIELD_FOLDER_TYPE), folderType); } + if (isBrowsable != null) { + bundle.putBoolean(keyForField(FIELD_IS_BROWSABLE), isBrowsable); + } if (isPlayable != null) { bundle.putBoolean(keyForField(FIELD_IS_PLAYABLE), isPlayable); } @@ -1255,6 +1304,9 @@ private static MediaMetadata fromBundle(Bundle bundle) { if (bundle.containsKey(keyForField(FIELD_FOLDER_TYPE))) { builder.setFolderType(bundle.getInt(keyForField(FIELD_FOLDER_TYPE))); } + if (bundle.containsKey(keyForField(FIELD_IS_BROWSABLE))) { + builder.setIsBrowsable(bundle.getBoolean(keyForField(FIELD_IS_BROWSABLE))); + } if (bundle.containsKey(keyForField(FIELD_IS_PLAYABLE))) { builder.setIsPlayable(bundle.getBoolean(keyForField(FIELD_IS_PLAYABLE))); } @@ -1292,4 +1344,74 @@ private static MediaMetadata fromBundle(Bundle bundle) { private static String keyForField(@FieldNumber int field) { return Integer.toString(field, Character.MAX_RADIX); } + + private static @FolderType int getFolderTypeFromMediaType(@MediaType int mediaType) { + switch (mediaType) { + case MEDIA_TYPE_ALBUM: + case MEDIA_TYPE_ARTIST: + case MEDIA_TYPE_AUDIO_BOOK: + case MEDIA_TYPE_AUDIO_BOOK_CHAPTER: + case MEDIA_TYPE_FOLDER_MOVIES: + case MEDIA_TYPE_FOLDER_NEWS: + case MEDIA_TYPE_FOLDER_RADIO_STATIONS: + case MEDIA_TYPE_FOLDER_TRAILERS: + case MEDIA_TYPE_FOLDER_VIDEOS: + case MEDIA_TYPE_GENRE: + case MEDIA_TYPE_MOVIE: + case MEDIA_TYPE_MUSIC: + case MEDIA_TYPE_NEWS: + case MEDIA_TYPE_PLAYLIST: + case MEDIA_TYPE_PODCAST: + case MEDIA_TYPE_PODCAST_EPISODE: + case MEDIA_TYPE_RADIO_STATION: + case MEDIA_TYPE_TRAILER: + case MEDIA_TYPE_TV_CHANNEL: + case MEDIA_TYPE_TV_SEASON: + case MEDIA_TYPE_TV_SERIES: + case MEDIA_TYPE_TV_SHOW: + case MEDIA_TYPE_VIDEO: + case MEDIA_TYPE_YEAR: + return FOLDER_TYPE_TITLES; + case MEDIA_TYPE_FOLDER_ALBUMS: + return FOLDER_TYPE_ALBUMS; + case MEDIA_TYPE_FOLDER_ARTISTS: + return FOLDER_TYPE_ARTISTS; + case MEDIA_TYPE_FOLDER_GENRES: + return FOLDER_TYPE_GENRES; + case MEDIA_TYPE_FOLDER_PLAYLISTS: + return FOLDER_TYPE_PLAYLISTS; + case MEDIA_TYPE_FOLDER_YEARS: + return FOLDER_TYPE_YEARS; + case MEDIA_TYPE_FOLDER_AUDIO_BOOKS: + case MEDIA_TYPE_FOLDER_MIXED: + case MEDIA_TYPE_FOLDER_TV_CHANNELS: + case MEDIA_TYPE_FOLDER_TV_SERIES: + case MEDIA_TYPE_FOLDER_TV_SHOWS: + case MEDIA_TYPE_FOLDER_PODCASTS: + case MEDIA_TYPE_MIXED: + default: + return FOLDER_TYPE_MIXED; + } + } + + private static @MediaType int getMediaTypeFromFolderType(@FolderType int folderType) { + switch (folderType) { + case FOLDER_TYPE_ALBUMS: + return MEDIA_TYPE_FOLDER_ALBUMS; + case FOLDER_TYPE_ARTISTS: + return MEDIA_TYPE_FOLDER_ARTISTS; + case FOLDER_TYPE_GENRES: + return MEDIA_TYPE_FOLDER_GENRES; + case FOLDER_TYPE_PLAYLISTS: + return MEDIA_TYPE_FOLDER_PLAYLISTS; + case FOLDER_TYPE_TITLES: + return MEDIA_TYPE_MIXED; + case FOLDER_TYPE_YEARS: + return MEDIA_TYPE_FOLDER_YEARS; + case FOLDER_TYPE_MIXED: + case FOLDER_TYPE_NONE: + default: + return MEDIA_TYPE_FOLDER_MIXED; + } + } } diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java index 4d66cd922a2..f3a7418fc79 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java @@ -49,6 +49,7 @@ public void builder_minimal_correctDefaults() { assertThat(mediaMetadata.trackNumber).isNull(); assertThat(mediaMetadata.totalTrackCount).isNull(); assertThat(mediaMetadata.folderType).isNull(); + assertThat(mediaMetadata.isBrowsable).isNull(); assertThat(mediaMetadata.isPlayable).isNull(); assertThat(mediaMetadata.recordingYear).isNull(); assertThat(mediaMetadata.recordingMonth).isNull(); @@ -115,6 +116,61 @@ public void roundTripViaBundle_yieldsEqualInstance() { assertThat(fromBundle.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE); } + @Test + public void builderSetFolderType_toNone_setsIsBrowsableToFalse() { + MediaMetadata mediaMetadata = + new MediaMetadata.Builder().setFolderType(MediaMetadata.FOLDER_TYPE_NONE).build(); + + assertThat(mediaMetadata.isBrowsable).isFalse(); + } + + @Test + public void builderSetFolderType_toNotNone_setsIsBrowsableToTrueAndMatchingMediaType() { + MediaMetadata mediaMetadata = + new MediaMetadata.Builder().setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS).build(); + + assertThat(mediaMetadata.isBrowsable).isTrue(); + assertThat(mediaMetadata.mediaType).isEqualTo(MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS); + } + + @Test + public void + builderSetFolderType_toNotNoneWithManualMediaType_setsIsBrowsableToTrueAndDoesNotOverrideMediaType() { + MediaMetadata mediaMetadata = + new MediaMetadata.Builder() + .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS) + .setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS) + .build(); + + assertThat(mediaMetadata.isBrowsable).isTrue(); + assertThat(mediaMetadata.mediaType).isEqualTo(MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS); + } + + @Test + public void builderSetIsBrowsable_toTrueWithoutMediaType_setsFolderTypeToMixed() { + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setIsBrowsable(true).build(); + + assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_MIXED); + } + + @Test + public void builderSetIsBrowsable_toTrueWithMediaType_setsFolderTypeToMatchMediaType() { + MediaMetadata mediaMetadata = + new MediaMetadata.Builder() + .setIsBrowsable(true) + .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS) + .build(); + + assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_ARTISTS); + } + + @Test + public void builderSetFolderType_toFalse_setsFolderTypeToNone() { + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setIsBrowsable(false).build(); + + assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_NONE); + } + private static MediaMetadata getFullyPopulatedMediaMetadata() { Bundle extras = new Bundle(); extras.putString(EXTRAS_KEY, EXTRAS_VALUE); @@ -135,6 +191,7 @@ private static MediaMetadata getFullyPopulatedMediaMetadata() { .setTrackNumber(4) .setTotalTrackCount(12) .setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS) + .setIsBrowsable(true) .setIsPlayable(true) .setRecordingYear(2000) .setRecordingMonth(11) diff --git a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java index 2984dbc604b..471d0200eb0 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java @@ -175,8 +175,8 @@ public static LibraryResult ofVoid(@Nullable LibraryParams params) { /** * Creates an instance with a media item and {@link #resultCode}{@code ==}{@link #RESULT_SUCCESS}. * - *

    The {@link MediaItem#mediaMetadata} must specify {@link MediaMetadata#folderType} and {@link - * MediaMetadata#isPlayable} fields. + *

    The {@link MediaItem#mediaMetadata} must specify {@link MediaMetadata#isBrowsable} (or + * {@link MediaMetadata#folderType}) and {@link MediaMetadata#isPlayable} fields. * * @param item The media item. * @param params The optional parameters to describe the media item. @@ -192,7 +192,8 @@ public static LibraryResult ofItem(MediaItem item, @Nullable LibraryP * #RESULT_SUCCESS}. * *

    The {@link MediaItem#mediaMetadata} of each item in the list must specify {@link - * MediaMetadata#folderType} and {@link MediaMetadata#isPlayable} fields. + * MediaMetadata#isBrowsable} (or {@link MediaMetadata#folderType}) and {@link + * MediaMetadata#isPlayable} fields. * * @param items The list of media items. * @param params The optional parameters to describe the list of media items. @@ -255,7 +256,7 @@ private LibraryResult( private static void verifyMediaItem(MediaItem item) { checkNotEmpty(item.mediaId, "mediaId must not be empty"); - checkArgument(item.mediaMetadata.folderType != null, "mediaMetadata must specify folderType"); + checkArgument(item.mediaMetadata.isBrowsable != null, "mediaMetadata must specify isBrowsable"); checkArgument(item.mediaMetadata.isPlayable != null, "mediaMetadata must specify isPlayable"); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java index 742c31e8797..fc924af3d92 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java @@ -311,7 +311,8 @@ private MediaItem createRootMediaItem(MediaBrowserCompat browserCompat) { String mediaId = browserCompat.getRoot(); MediaMetadata mediaMetadata = new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_MIXED) + .setIsBrowsable(true) + .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED) .setIsPlayable(false) .setExtras(browserCompat.getExtras()) .build(); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java index 8dda126ce46..5da473a821e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java @@ -213,7 +213,7 @@ public final class MediaConstants { * {@link MediaBrowser#getLibraryRoot}, the preference applies to all playable items within the * browse tree. * - *

    If exposed through {@link MediaMetadata#extras} of a {@linkplain MediaMetadata#folderType + *

    If exposed through {@link MediaMetadata#extras} of a {@linkplain MediaMetadata#isBrowsable * browsable media item}, the preference applies to only the immediate playable children. It takes * precedence over preferences received with {@link MediaBrowser#getLibraryRoot}. * @@ -238,7 +238,7 @@ public final class MediaConstants { * {@link MediaBrowser#getLibraryRoot}, the preference applies to all browsable items within the * browse tree. * - *

    If exposed through {@link MediaMetadata#extras} of a {@linkplain MediaMetadata#folderType + *

    If exposed through {@link MediaMetadata#extras} of a {@linkplain MediaMetadata#isBrowsable * browsable media item}, the preference applies to only the immediate browsable children. It * takes precedence over preferences received with {@link * MediaBrowser#getLibraryRoot(LibraryParams)}. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java index bb6b8ebfa9b..48442529ff7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java @@ -123,8 +123,9 @@ public static final class MediaLibrarySession extends MediaSession { * An extended {@link MediaSession.Callback} for the {@link MediaLibrarySession}. * *

    When you return {@link LibraryResult} with {@link MediaItem media items}, each item must - * have valid {@link MediaItem#mediaId} and specify {@link MediaMetadata#folderType} and {@link - * MediaMetadata#isPlayable} in its {@link MediaItem#mediaMetadata}. + * have valid {@link MediaItem#mediaId} and specify {@link MediaMetadata#isBrowsable} (or {@link + * MediaMetadata#folderType}) and {@link MediaMetadata#isPlayable} in its {@link + * MediaItem#mediaMetadata}. */ public interface Callback extends MediaSession.Callback { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index c805129dd48..919d5521787 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -145,7 +145,7 @@ public static MediaBrowserCompat.MediaItem convertToBrowserItem( MediaDescriptionCompat description = convertToMediaDescriptionCompat(item, artworkBitmap); MediaMetadata metadata = item.mediaMetadata; int flags = 0; - if (metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE) { + if (metadata.isBrowsable != null && metadata.isBrowsable) { flags |= MediaBrowserCompat.MediaItem.FLAG_BROWSABLE; } if (metadata.isPlayable != null && metadata.isPlayable) { @@ -375,11 +375,7 @@ public static MediaMetadata convertToMediaMetadata(@Nullable CharSequence queueT if (queueTitle == null) { return MediaMetadata.EMPTY; } - return new MediaMetadata.Builder() - .setTitle(queueTitle) - .setFolderType(MediaMetadata.FOLDER_TYPE_MIXED) - .setIsPlayable(true) - .build(); + return new MediaMetadata.Builder().setTitle(queueTitle).build(); } public static MediaMetadata convertToMediaMetadata( @@ -417,20 +413,22 @@ private static MediaMetadata convertToMediaMetadata( builder.setArtworkData(artworkData, MediaMetadata.PICTURE_TYPE_FRONT_COVER); } - @Nullable Bundle extras = descriptionCompat.getExtras(); - builder.setExtras(extras); + @Nullable Bundle compatExtras = descriptionCompat.getExtras(); + @Nullable Bundle extras = compatExtras == null ? null : new Bundle(compatExtras); if (extras != null && extras.containsKey(MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE)) { builder.setFolderType( convertToFolderType(extras.getLong(MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE))); - } else if (browsable) { - builder.setFolderType(MediaMetadata.FOLDER_TYPE_MIXED); - } else { - builder.setFolderType(MediaMetadata.FOLDER_TYPE_NONE); + extras.remove(MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE); } + builder.setIsBrowsable(browsable); if (extras != null && extras.containsKey(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT)) { builder.setMediaType((int) extras.getLong(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT)); + extras.remove(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT); + } + if (extras != null && !extras.isEmpty()) { + builder.setExtras(extras); } builder.setIsPlayable(playable); @@ -501,12 +499,13 @@ public static MediaMetadata convertToMediaMetadata( } } - if (metadataCompat.containsKey(MediaMetadataCompat.METADATA_KEY_BT_FOLDER_TYPE)) { + boolean isBrowsable = + metadataCompat.containsKey(MediaMetadataCompat.METADATA_KEY_BT_FOLDER_TYPE); + builder.setIsBrowsable(isBrowsable); + if (isBrowsable) { builder.setFolderType( convertToFolderType( metadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_BT_FOLDER_TYPE))); - } else { - builder.setFolderType(MediaMetadata.FOLDER_TYPE_NONE); } if (metadataCompat.containsKey(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT)) { @@ -653,7 +652,7 @@ private static int convertToFolderType(long extraBtFolderType) { } else if (extraBtFolderType == MediaDescriptionCompat.BT_FOLDER_TYPE_YEARS) { return MediaMetadata.FOLDER_TYPE_YEARS; } else { - return MediaMetadata.FOLDER_TYPE_NONE; + return MediaMetadata.FOLDER_TYPE_MIXED; } } diff --git a/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java b/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java index e1ab29d9468..a4d7afc33cb 100644 --- a/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java @@ -30,17 +30,14 @@ public class LibraryResultTest { @Test public void constructor_mediaItemWithoutMediaId_throwsIAE() { MediaMetadata metadata = - new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_MIXED) - .setIsPlayable(true) - .build(); + new MediaMetadata.Builder().setIsBrowsable(true).setIsPlayable(true).build(); MediaItem item = new MediaItem.Builder().setMediaMetadata(metadata).build(); assertThrows( IllegalArgumentException.class, () -> LibraryResult.ofItem(item, /* params= */ null)); } @Test - public void constructor_mediaItemWithoutFolderType_throwsIAE() { + public void constructor_mediaItemWithoutIsBrowsable_throwsIAE() { MediaMetadata metadata = new MediaMetadata.Builder().setIsPlayable(true).build(); MediaItem item = new MediaItem.Builder().setMediaId("id").setMediaMetadata(metadata).build(); assertThrows( @@ -49,8 +46,7 @@ public void constructor_mediaItemWithoutFolderType_throwsIAE() { @Test public void constructor_mediaItemWithoutIsPlayable_throwsIAE() { - MediaMetadata metadata = - new MediaMetadata.Builder().setFolderType(MediaMetadata.FOLDER_TYPE_MIXED).build(); + MediaMetadata metadata = new MediaMetadata.Builder().setIsBrowsable(true).build(); MediaItem item = new MediaItem.Builder().setMediaId("id").setMediaMetadata(metadata).build(); assertThrows( IllegalArgumentException.class, () -> LibraryResult.ofItem(item, /* params= */ null)); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java index 45c7171ccbc..5884f6c6ba9 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java @@ -42,7 +42,6 @@ import android.os.Bundle; import androidx.annotation.Nullable; import androidx.media3.common.MediaItem; -import androidx.media3.common.MediaMetadata; import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.test.session.common.MediaBrowserConstants; import androidx.media3.test.session.common.TestUtils; @@ -155,7 +154,7 @@ public void getItem_browsable() throws Exception { assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); assertThat(result.value.mediaId).isEqualTo(mediaId); - assertThat(result.value.mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_MIXED); + assertThat(result.value.mediaMetadata.isBrowsable).isTrue(); } @Test diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserServiceCompatCallbackWithMediaBrowserTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserServiceCompatCallbackWithMediaBrowserTest.java index 043b654f5c1..873d2a4441e 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserServiceCompatCallbackWithMediaBrowserTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserServiceCompatCallbackWithMediaBrowserTest.java @@ -142,7 +142,7 @@ public void onLoadItem(String itemId, Result resul assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(result.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS); assertItemEquals(testItem, result.value); - assertThat(result.value.mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_MIXED); + assertThat(result.value.mediaMetadata.isBrowsable).isTrue(); } @Test diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceNotificationTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceNotificationTest.java index 4180c3a3579..850cf318607 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceNotificationTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceNotificationTest.java @@ -138,7 +138,6 @@ private MediaMetadata createTestMediaMetadata() throws IOException { .setTitle("Test Song Name") .setArtist("Test Artist Name") .setArtworkData(artworkData) - .setFolderType(MediaMetadata.FOLDER_TYPE_NONE) .setIsPlayable(true) .build(); } @@ -147,7 +146,6 @@ private MediaMetadata createAnotherTestMediaMetadata() { return new MediaMetadata.Builder() .setTitle("New Song Name") .setArtist("New Artist Name") - .setFolderType(MediaMetadata.FOLDER_TYPE_NONE) .setIsPlayable(true) .build(); } diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java index 2a6af52728b..bd82b76e2a4 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java @@ -56,8 +56,8 @@ public final class MediaTestUtils { public static MediaItem createMediaItem(String mediaId) { MediaMetadata mediaMetadata = new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_TITLES) .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) + .setIsBrowsable(false) .setIsPlayable(true) .build(); return new MediaItem.Builder().setMediaId(mediaId).setMediaMetadata(mediaMetadata).build(); @@ -66,8 +66,8 @@ public static MediaItem createMediaItem(String mediaId) { public static MediaItem createMediaItemWithArtworkData(String mediaId) { MediaMetadata.Builder mediaMetadataBuilder = new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_TITLES) .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) + .setIsBrowsable(false) .setIsPlayable(true); try { byte[] artworkData = @@ -107,8 +107,8 @@ public static List createMediaItems(String... mediaIds) { public static MediaMetadata createMediaMetadata() { return new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_NONE) - .setIsPlayable(false) + .setIsBrowsable(false) + .setIsPlayable(true) .setTitle(METADATA_TITLE) .setSubtitle(METADATA_SUBTITLE) .setDescription(METADATA_DESCRIPTION) @@ -120,8 +120,8 @@ public static MediaMetadata createMediaMetadata() { public static MediaMetadata createMediaMetadataWithArtworkData() { MediaMetadata.Builder mediaMetadataBuilder = new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_NONE) - .setIsPlayable(false) + .setIsBrowsable(false) + .setIsPlayable(true) .setTitle(METADATA_TITLE) .setSubtitle(METADATA_SUBTITLE) .setDescription(METADATA_DESCRIPTION) diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java index d460bee20b9..f5a2da634e1 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java @@ -102,10 +102,7 @@ public class MockMediaLibraryService extends MediaLibraryService { new MediaItem.Builder() .setMediaId(ROOT_ID) .setMediaMetadata( - new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_MIXED) - .setIsPlayable(false) - .build()) + new MediaMetadata.Builder().setIsBrowsable(true).setIsPlayable(false).build()) .build(); public static final LibraryParams ROOT_PARAMS = new LibraryParams.Builder().setExtras(ROOT_EXTRAS).build(); @@ -228,10 +225,7 @@ public ListenableFuture> onGetLibraryRoot( new MediaItem.Builder() .setMediaId(customLibraryRoot) .setMediaMetadata( - new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_ALBUMS) - .setIsPlayable(false) - .build()) + new MediaMetadata.Builder().setIsBrowsable(true).setIsPlayable(false).build()) .build(); } if (params != null) { @@ -243,10 +237,7 @@ public ListenableFuture> onGetLibraryRoot( new MediaItem.Builder() .setMediaId(ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY) .setMediaMetadata( - new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_MIXED) - .setIsPlayable(false) - .build()) + new MediaMetadata.Builder().setIsBrowsable(true).setIsPlayable(false).build()) .build(); } } @@ -478,7 +469,7 @@ private List getPaginatedResult(List items, int page, int pag private MediaItem createBrowsableMediaItem(String mediaId) { MediaMetadata mediaMetadata = new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_MIXED) + .setIsBrowsable(true) .setIsPlayable(false) .setArtworkData(getArtworkData(), MediaMetadata.PICTURE_TYPE_FRONT_COVER) .build(); @@ -501,7 +492,7 @@ private static MediaItem createPlayableMediaItem(String mediaId) { extras.putInt(EXTRAS_KEY_COMPLETION_STATUS, EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED); MediaMetadata mediaMetadata = new MediaMetadata.Builder() - .setFolderType(MediaMetadata.FOLDER_TYPE_NONE) + .setIsBrowsable(false) .setIsPlayable(true) .setExtras(extras) .build(); From c32494a3e3e5488b558d2aa2c77da45c54f00c22 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 8 Dec 2022 10:09:55 +0000 Subject: [PATCH 060/141] Remove debug timeout multiplier. It looks like this was added accidentally in . PiperOrigin-RevId: 493834134 (cherry picked from commit 533f5288f4aec47a75357bf308907d1686ba493a) --- .../androidx/media3/test/utils/robolectric/RobolectricUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java index ef9c4e5a3f0..32f2c01b9ef 100644 --- a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java +++ b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/RobolectricUtil.java @@ -96,7 +96,7 @@ public static void runMainLooperUntil(Supplier condition, long timeoutM */ public static void runLooperUntil(Looper looper, Supplier condition) throws TimeoutException { - runLooperUntil(looper, condition, DEFAULT_TIMEOUT_MS * 1000000, Clock.DEFAULT); + runLooperUntil(looper, condition, DEFAULT_TIMEOUT_MS, Clock.DEFAULT); } /** From 80be30f5115491d097f281dbdcc65fd91d547e0a Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 8 Dec 2022 11:02:56 +0000 Subject: [PATCH 061/141] Clarify and correct allowed multi-threading for some Player methods Some Player methods like getting the Looper and adding listeners were always allowed to be called from any thread, but this is undocumented. This change makes the threading rules of these methods more explicit. Removing listeners was never meant to be called from another thread and we also don't support it safely because final callbacks may be triggered from the wrong thread. To find potential issues, we can assert the correct thread when releasing listeners. Finally, there is a potential race condition when calling addListener from a different thread at the same time as release, which may lead to a registered listener that could receive callbacks after the player is released. PiperOrigin-RevId: 493843981 (cherry picked from commit 927b2d6a435a236bb5db7646cf6402557db893f6) --- .../java/androidx/media3/common/Player.java | 8 + .../media3/common/SimpleBasePlayer.java | 3 +- .../media3/common/util/ListenerSet.java | 60 +++- .../androidx/media3/exoplayer/ExoPlayer.java | 34 ++- .../media3/exoplayer/ExoPlayerImpl.java | 16 +- .../analytics/DefaultAnalyticsCollector.java | 14 + .../media3/exoplayer/ExoPlayerTest.java | 14 +- .../media3/session/MediaController.java | 3 + .../session/MediaControllerSurfaceTest.java | 287 +++++++++--------- 9 files changed, 271 insertions(+), 168 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index 2fc70006a36..44464e39b99 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -49,6 +49,10 @@ * A media player interface defining traditional high-level functionality, such as the ability to * play, pause, seek and query properties of the currently playing media. * + *

    All methods must be called from a single {@linkplain #getApplicationLooper() application + * thread} unless indicated otherwise. Callbacks in registered listeners are called on the same + * thread. + * *

    This interface includes some convenience methods that can be implemented by calling other * methods in the interface. {@link BasePlayer} implements these convenience methods so inheriting * {@link BasePlayer} is recommended when implementing the interface so that only the minimal set of @@ -1543,6 +1547,8 @@ default void onMetadata(Metadata metadata) {} /** * Returns the {@link Looper} associated with the application thread that's used to access the * player and on which player events are received. + * + *

    This method can be called from any thread. */ Looper getApplicationLooper(); @@ -1552,6 +1558,8 @@ default void onMetadata(Metadata metadata) {} *

    The listener's methods will be called on the thread associated with {@link * #getApplicationLooper()}. * + *

    This method can be called from any thread. + * * @param listener The listener to register. */ void addListener(Listener listener); diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index 731dca56308..2a2a3ffc45a 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -1978,8 +1978,7 @@ public final void addListener(Listener listener) { @Override public final void removeListener(Listener listener) { - // Don't verify application thread. We allow calls to this method from any thread. - checkNotNull(listener); + verifyApplicationThreadAndInitState(); listeners.remove(listener); } diff --git a/libraries/common/src/main/java/androidx/media3/common/util/ListenerSet.java b/libraries/common/src/main/java/androidx/media3/common/util/ListenerSet.java index 78e529ae3a3..0ab3bab5416 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/ListenerSet.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/ListenerSet.java @@ -15,9 +15,12 @@ */ package androidx.media3.common.util; +import static androidx.media3.common.util.Assertions.checkState; + import android.os.Looper; import android.os.Message; import androidx.annotation.CheckResult; +import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.FlagSet; @@ -34,6 +37,9 @@ *

    Events are also guaranteed to be only sent to the listeners registered at the time the event * was enqueued and haven't been removed since. * + *

    All methods must be called on the {@link Looper} passed to the constructor unless indicated + * otherwise. + * * @param The listener type. */ @UnstableApi @@ -76,14 +82,18 @@ public interface IterationFinishedEvent { private final CopyOnWriteArraySet> listeners; private final ArrayDeque flushingEvents; private final ArrayDeque queuedEvents; + private final Object releasedLock; + @GuardedBy("releasedLock") private boolean released; + private boolean throwsWhenUsingWrongThread; + /** * Creates a new listener set. * * @param looper A {@link Looper} used to call listeners on. The same {@link Looper} must be used - * to call all other methods of this class. + * to call all other methods of this class unless indicated otherwise. * @param clock A {@link Clock}. * @param iterationFinishedEvent An {@link IterationFinishedEvent} sent when all other events sent * during one {@link Looper} message queue iteration were handled by the listeners. @@ -100,17 +110,21 @@ private ListenerSet( this.clock = clock; this.listeners = listeners; this.iterationFinishedEvent = iterationFinishedEvent; + releasedLock = new Object(); flushingEvents = new ArrayDeque<>(); queuedEvents = new ArrayDeque<>(); // It's safe to use "this" because we don't send a message before exiting the constructor. @SuppressWarnings("nullness:methodref.receiver.bound") HandlerWrapper handler = clock.createHandler(looper, this::handleMessage); this.handler = handler; + throwsWhenUsingWrongThread = true; } /** * Copies the listener set. * + *

    This method can be called from any thread. + * * @param looper The new {@link Looper} for the copied listener set. * @param iterationFinishedEvent The new {@link IterationFinishedEvent} sent when all other events * sent during one {@link Looper} message queue iteration were handled by the listeners. @@ -124,6 +138,8 @@ public ListenerSet copy(Looper looper, IterationFinishedEvent iterationFin /** * Copies the listener set. * + *

    This method can be called from any thread. + * * @param looper The new {@link Looper} for the copied listener set. * @param clock The new {@link Clock} for the copied listener set. * @param iterationFinishedEvent The new {@link IterationFinishedEvent} sent when all other events @@ -141,14 +157,18 @@ public ListenerSet copy( * *

    If a listener is already present, it will not be added again. * + *

    This method can be called from any thread. + * * @param listener The listener to be added. */ public void add(T listener) { - if (released) { - return; - } Assertions.checkNotNull(listener); - listeners.add(new ListenerHolder<>(listener)); + synchronized (releasedLock) { + if (released) { + return; + } + listeners.add(new ListenerHolder<>(listener)); + } } /** @@ -159,6 +179,7 @@ public void add(T listener) { * @param listener The listener to be removed. */ public void remove(T listener) { + verifyCurrentThread(); for (ListenerHolder listenerHolder : listeners) { if (listenerHolder.listener.equals(listener)) { listenerHolder.release(iterationFinishedEvent); @@ -169,11 +190,13 @@ public void remove(T listener) { /** Removes all listeners from the set. */ public void clear() { + verifyCurrentThread(); listeners.clear(); } /** Returns the number of added listeners. */ public int size() { + verifyCurrentThread(); return listeners.size(); } @@ -185,6 +208,7 @@ public int size() { * @param event The event. */ public void queueEvent(int eventFlag, Event event) { + verifyCurrentThread(); CopyOnWriteArraySet> listenerSnapshot = new CopyOnWriteArraySet<>(listeners); queuedEvents.add( () -> { @@ -196,6 +220,7 @@ public void queueEvent(int eventFlag, Event event) { /** Notifies listeners of events previously enqueued with {@link #queueEvent(int, Event)}. */ public void flushEvents() { + verifyCurrentThread(); if (queuedEvents.isEmpty()) { return; } @@ -234,11 +259,27 @@ public void sendEvent(int eventFlag, Event event) { *

    This will ensure no events are sent to any listener after this method has been called. */ public void release() { + verifyCurrentThread(); + synchronized (releasedLock) { + released = true; + } for (ListenerHolder listenerHolder : listeners) { listenerHolder.release(iterationFinishedEvent); } listeners.clear(); - released = true; + } + + /** + * Sets whether methods throw when using the wrong thread. + * + *

    Do not use this method unless to support legacy use cases. + * + * @param throwsWhenUsingWrongThread Whether to throw when using the wrong thread. + * @deprecated Do not use this method and ensure all calls are made from the correct thread. + */ + @Deprecated + public void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) { + this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread; } private boolean handleMessage(Message message) { @@ -254,6 +295,13 @@ private boolean handleMessage(Message message) { return true; } + private void verifyCurrentThread() { + if (!throwsWhenUsingWrongThread) { + return; + } + checkState(Thread.currentThread() == handler.getLooper().getThread()); + } + private static final class ListenerHolder { public final T listener; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index eae251688e7..e58db58847e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -132,15 +132,15 @@ * threading model"> * *

      - *
    • ExoPlayer instances must be accessed from a single application thread. For the vast - * majority of cases this should be the application's main thread. Using the application's - * main thread is also a requirement when using ExoPlayer's UI components or the IMA - * extension. The thread on which an ExoPlayer instance must be accessed can be explicitly - * specified by passing a `Looper` when creating the player. If no `Looper` is specified, then - * the `Looper` of the thread that the player is created on is used, or if that thread does - * not have a `Looper`, the `Looper` of the application's main thread is used. In all cases - * the `Looper` of the thread from which the player must be accessed can be queried using - * {@link #getApplicationLooper()}. + *
    • ExoPlayer instances must be accessed from a single application thread unless indicated + * otherwise. For the vast majority of cases this should be the application's main thread. + * Using the application's main thread is also a requirement when using ExoPlayer's UI + * components or the IMA extension. The thread on which an ExoPlayer instance must be accessed + * can be explicitly specified by passing a `Looper` when creating the player. If no `Looper` + * is specified, then the `Looper` of the thread that the player is created on is used, or if + * that thread does not have a `Looper`, the `Looper` of the application's main thread is + * used. In all cases the `Looper` of the thread from which the player must be accessed can be + * queried using {@link #getApplicationLooper()}. *
    • Registered listeners are called on the thread associated with {@link * #getApplicationLooper()}. Note that this means registered listeners are called on the same * thread which must be used to access the player. @@ -1229,6 +1229,8 @@ public ExoPlayer build() { /** * Adds a listener to receive audio offload events. * + *

      This method can be called from any thread. + * * @param listener The listener to register. */ @UnstableApi @@ -1249,6 +1251,8 @@ public ExoPlayer build() { /** * Adds an {@link AnalyticsListener} to receive analytics events. * + *

      This method can be called from any thread. + * * @param listener The listener to be added. */ void addAnalyticsListener(AnalyticsListener listener); @@ -1314,11 +1318,19 @@ public ExoPlayer build() { @Deprecated TrackSelectionArray getCurrentTrackSelections(); - /** Returns the {@link Looper} associated with the playback thread. */ + /** + * Returns the {@link Looper} associated with the playback thread. + * + *

      This method may be called from any thread. + */ @UnstableApi Looper getPlaybackLooper(); - /** Returns the {@link Clock} used for playback. */ + /** + * Returns the {@link Clock} used for playback. + * + *

      This method can be called from any thread. + */ @UnstableApi Clock getClock(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index d896bc76544..5f27dc546a7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -88,6 +88,7 @@ import androidx.media3.exoplayer.Renderer.MessageType; import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.AnalyticsListener; +import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector; import androidx.media3.exoplayer.analytics.MediaMetricsListener; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.audio.AudioRendererEventListener; @@ -479,7 +480,7 @@ public void addAudioOffloadListener(AudioOffloadListener listener) { @Override public void removeAudioOffloadListener(AudioOffloadListener listener) { - // Don't verify application thread. We allow calls to this method from any thread. + verifyApplicationThread(); audioOffloadListeners.remove(listener); } @@ -1487,7 +1488,7 @@ public void addAnalyticsListener(AnalyticsListener listener) { @Override public void removeAnalyticsListener(AnalyticsListener listener) { - // Don't verify application thread. We allow calls to this method from any thread. + verifyApplicationThread(); analyticsCollector.removeListener(checkNotNull(listener)); } @@ -1604,9 +1605,8 @@ public void addListener(Listener listener) { @Override public void removeListener(Listener listener) { - // Don't verify application thread. We allow calls to this method from any thread. - checkNotNull(listener); - listeners.remove(listener); + verifyApplicationThread(); + listeners.remove(checkNotNull(listener)); } @Override @@ -1689,8 +1689,14 @@ public boolean isTunnelingEnabled() { return false; } + @SuppressWarnings("deprecation") // Calling deprecated methods. /* package */ void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) { this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread; + listeners.setThrowsWhenUsingWrongThread(throwsWhenUsingWrongThread); + if (analyticsCollector instanceof DefaultAnalyticsCollector) { + ((DefaultAnalyticsCollector) analyticsCollector) + .setThrowsWhenUsingWrongThread(throwsWhenUsingWrongThread); + } } /** diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollector.java index 74062ab3e33..5044d8fa560 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollector.java @@ -96,6 +96,20 @@ public DefaultAnalyticsCollector(Clock clock) { eventTimes = new SparseArray<>(); } + /** + * Sets whether methods throw when using the wrong thread. + * + *

      Do not use this method unless to support legacy use cases. + * + * @param throwsWhenUsingWrongThread Whether to throw when using the wrong thread. + * @deprecated Do not use this method and ensure all calls are made from the correct thread. + */ + @SuppressWarnings("deprecation") // Calling deprecated method. + @Deprecated + public void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) { + listeners.setThrowsWhenUsingWrongThread(throwsWhenUsingWrongThread); + } + @Override @CallSuper public void addListener(AnalyticsListener listener) { diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 8aafe98324b..3fa73c69fc9 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -12006,10 +12006,20 @@ public void playingMedia_withNoMetadata_doesNotUpdateMediaMetadata() throws Exce @Test @Config(sdk = Config.ALL_SDKS) - public void builder_inBackgroundThread_doesNotThrow() throws Exception { + public void builder_inBackgroundThreadWithAllowedAnyThreadMethods_doesNotThrow() + throws Exception { Thread builderThread = new Thread( - () -> new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build()); + () -> { + ExoPlayer player = + new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(); + player.addListener(new Listener() {}); + player.addAnalyticsListener(new AnalyticsListener() {}); + player.addAudioOffloadListener(new ExoPlayer.AudioOffloadListener() {}); + player.getClock(); + player.getApplicationLooper(); + player.getPlaybackLooper(); + }); AtomicReference builderThrow = new AtomicReference<>(); builderThread.setUncaughtExceptionHandler((thread, throwable) -> builderThrow.set(throwable)); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 5affe5c6955..496e6ea946f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -1720,6 +1720,7 @@ public void setTrackSelectionParameters(TrackSelectionParameters parameters) { @Override public Looper getApplicationLooper() { + // Don't verify application thread. We allow calls to this method from any thread. return applicationHandler.getLooper(); } @@ -1744,12 +1745,14 @@ public Looper getApplicationLooper() { @Override public void addListener(Player.Listener listener) { + // Don't verify application thread. We allow calls to this method from any thread. checkNotNull(listener, "listener must not be null"); impl.addListener(listener); } @Override public void removeListener(Player.Listener listener) { + verifyApplicationThread(); checkNotNull(listener, "listener must not be null"); impl.removeListener(listener); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerSurfaceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerSurfaceTest.java index cc2567c0aa9..20aca9ee0f8 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerSurfaceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerSurfaceTest.java @@ -19,6 +19,7 @@ import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; +import android.os.Looper; import android.os.RemoteException; import android.view.Surface; import android.view.SurfaceHolder; @@ -26,7 +27,6 @@ import android.view.TextureView; import android.view.View; import android.view.ViewGroup; -import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.PollingCheck; import androidx.media3.test.session.common.SurfaceActivity; import androidx.test.core.app.ApplicationProvider; @@ -37,8 +37,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.RuleChain; -import org.junit.rules.TestRule; import org.junit.runner.RunWith; /** Tests for {@link MediaController#setVideoSurface(Surface)}. */ @@ -47,13 +45,6 @@ public class MediaControllerSurfaceTest { private static final String TAG = "MC_SurfaceTest"; - private final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); - private final MediaControllerTestRule controllerTestRule = - new MediaControllerTestRule(threadTestRule); - - @Rule - public final TestRule chain = RuleChain.outerRule(threadTestRule).around(controllerTestRule); - private SurfaceActivity activity; private RemoteMediaSession remoteSession; @@ -76,93 +67,97 @@ public void cleanUp() throws RemoteException { } @Test - public void setVideoSurface() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void setVideoSurface() throws Throwable { + MediaController controller = createController(); Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + activityRule.runOnUiThread(() -> controller.setVideoSurface(testSurface)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void setVideoSurface_withNull_clearsSurface() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void setVideoSurface_withNull_clearsSurface() throws Throwable { + MediaController controller = createController(); Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + activityRule.runOnUiThread(() -> controller.setVideoSurface(testSurface)); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(null)); + activityRule.runOnUiThread(() -> controller.setVideoSurface(null)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurface_withTheSameSurface() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurface_withTheSameSurface() throws Throwable { + MediaController controller = createController(); Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + activityRule.runOnUiThread(() -> controller.setVideoSurface(testSurface)); - threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurface(testSurface)); + activityRule.runOnUiThread(() -> controller.clearVideoSurface(testSurface)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurface_withDifferentSurface_doesNothing() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurface_withDifferentSurface_doesNothing() throws Throwable { + MediaController controller = createController(); Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + activityRule.runOnUiThread(() -> controller.setVideoSurface(testSurface)); Surface anotherSurface = activity.getSecondSurfaceHolder().getSurface(); - threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurface(anotherSurface)); + activityRule.runOnUiThread(() -> controller.clearVideoSurface(anotherSurface)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurface_withNull_doesNothing() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurface_withNull_doesNothing() throws Throwable { + MediaController controller = createController(); Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + activityRule.runOnUiThread(() -> controller.setVideoSurface(testSurface)); - threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurface(null)); + activityRule.runOnUiThread(() -> controller.clearVideoSurface(null)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void setVideoSurfaceHolder() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void setVideoSurfaceHolder() throws Throwable { + MediaController controller = createController(); SurfaceHolder testSurfaceHolder = activity.getFirstSurfaceHolder(); - threadTestRule - .getHandler() - .postAndSync(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); + activityRule.runOnUiThread( + () -> { + controller.setVideoSurfaceHolder(testSurfaceHolder); + }); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void setVideoSurfaceHolder_withNull_clearsSurfaceHolder() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void setVideoSurfaceHolder_withNull_clearsSurfaceHolder() throws Throwable { + MediaController controller = createController(); SurfaceHolder testSurfaceHolder = activity.getFirstSurfaceHolder(); - threadTestRule - .getHandler() - .postAndSync(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurfaceHolder(null)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceHolder(null)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test public void setVideoSurfaceHolder_whenSurfaceIsDestroyed_surfaceIsClearedFromPlayer() throws Throwable { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + MediaController controller = createController(); SurfaceHolder testSurfaceHolder = activity.getFirstSurfaceHolder(); - threadTestRule - .getHandler() - .postAndSync(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); activityRule.runOnUiThread( () -> { @@ -171,15 +166,14 @@ public void setVideoSurfaceHolder_whenSurfaceIsDestroyed_surfaceIsClearedFromPla }); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test public void setVideoSurfaceHolder_whenSurfaceIsCreated_surfaceIsSetToPlayer() throws Throwable { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + MediaController controller = createController(); SurfaceHolder testSurfaceHolder = activity.getFirstSurfaceHolder(); - threadTestRule - .getHandler() - .postAndSync(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); activityRule.runOnUiThread( () -> { SurfaceView firstSurfaceView = activity.getFirstSurfaceView(); @@ -193,79 +187,75 @@ public void setVideoSurfaceHolder_whenSurfaceIsCreated_surfaceIsSetToPlayer() th }); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurfaceHolder_withTheSameSurfaceHolder() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurfaceHolder_withTheSameSurfaceHolder() throws Throwable { + MediaController controller = createController(); SurfaceHolder testSurfaceHolder = activity.getFirstSurfaceHolder(); - threadTestRule - .getHandler() - .postAndSync(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); - threadTestRule - .getHandler() - .postAndSync(() -> controller.clearVideoSurfaceHolder(testSurfaceHolder)); + activityRule.runOnUiThread(() -> controller.clearVideoSurfaceHolder(testSurfaceHolder)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurfaceHolder_withDifferentSurfaceHolder_doesNothing() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurfaceHolder_withDifferentSurfaceHolder_doesNothing() throws Throwable { + MediaController controller = createController(); SurfaceHolder testSurfaceHolder = activity.getFirstSurfaceHolder(); - threadTestRule - .getHandler() - .postAndSync(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); SurfaceHolder anotherTestSurfaceHolder = activity.getSecondSurfaceHolder(); - threadTestRule - .getHandler() - .postAndSync(() -> controller.clearVideoSurfaceHolder(anotherTestSurfaceHolder)); + activityRule.runOnUiThread(() -> controller.clearVideoSurfaceHolder(anotherTestSurfaceHolder)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurfaceHolder_withNull_doesNothing() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurfaceHolder_withNull_doesNothing() throws Throwable { + MediaController controller = createController(); SurfaceHolder testSurfaceHolder = activity.getFirstSurfaceHolder(); - threadTestRule - .getHandler() - .postAndSync(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); - threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurfaceHolder(null)); + activityRule.runOnUiThread(() -> controller.clearVideoSurfaceHolder(null)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void setVideoSurfaceView() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void setVideoSurfaceView() throws Throwable { + MediaController controller = createController(); SurfaceView testSurfaceView = activity.getFirstSurfaceView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurfaceView(testSurfaceView)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceView(testSurfaceView)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void setVideoSurfaceView_withNull_clearsSurfaceView() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void setVideoSurfaceView_withNull_clearsSurfaceView() throws Throwable { + MediaController controller = createController(); SurfaceView testSurfaceView = activity.getFirstSurfaceView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurfaceView(testSurfaceView)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceView(testSurfaceView)); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurfaceView(null)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceView(null)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test public void setVideoSurfaceView_whenSurfaceIsDestroyed_surfaceIsClearedFromPlayer() throws Throwable { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + MediaController controller = createController(); SurfaceView testSurfaceView = activity.getFirstSurfaceView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurfaceView(testSurfaceView)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceView(testSurfaceView)); activityRule.runOnUiThread( () -> { @@ -273,13 +263,14 @@ public void setVideoSurfaceView_whenSurfaceIsDestroyed_surfaceIsClearedFromPlaye }); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test public void setVideoSurfaceView_whenSurfaceIsCreated_surfaceIsSetToPlayer() throws Throwable { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + MediaController controller = createController(); SurfaceView testSurfaceView = activity.getFirstSurfaceView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurfaceView(testSurfaceView)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceView(testSurfaceView)); activityRule.runOnUiThread( () -> { testSurfaceView.setVisibility(View.GONE); @@ -291,74 +282,76 @@ public void setVideoSurfaceView_whenSurfaceIsCreated_surfaceIsSetToPlayer() thro }); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurfaceView_withTheSameSurfaceView() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurfaceView_withTheSameSurfaceView() throws Throwable { + MediaController controller = createController(); SurfaceView testSurfaceView = activity.getFirstSurfaceView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurfaceView(testSurfaceView)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceView(testSurfaceView)); - threadTestRule - .getHandler() - .postAndSync(() -> controller.clearVideoSurfaceView(testSurfaceView)); + activityRule.runOnUiThread(() -> controller.clearVideoSurfaceView(testSurfaceView)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurfaceView_withDifferentSurfaceView_doesNothing() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurfaceView_withDifferentSurfaceView_doesNothing() throws Throwable { + MediaController controller = createController(); SurfaceView testSurfaceView = activity.getFirstSurfaceView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurfaceView(testSurfaceView)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceView(testSurfaceView)); SurfaceView anotherTestSurfaceView = activity.getSecondSurfaceView(); - threadTestRule - .getHandler() - .postAndSync(() -> controller.clearVideoSurfaceView(anotherTestSurfaceView)); + activityRule.runOnUiThread(() -> controller.clearVideoSurfaceView(anotherTestSurfaceView)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurfaceView_withNull_doesNothing() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurfaceView_withNull_doesNothing() throws Throwable { + MediaController controller = createController(); SurfaceView testSurfaceView = activity.getFirstSurfaceView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurfaceView(testSurfaceView)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceView(testSurfaceView)); SurfaceView anotherTestSurfaceView = activity.getSecondSurfaceView(); - threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurfaceView(null)); + activityRule.runOnUiThread(() -> controller.clearVideoSurfaceView(null)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void setVideoTextureView() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void setVideoTextureView() throws Throwable { + MediaController controller = createController(); TextureView testTextureView = activity.getFirstTextureView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoTextureView(testTextureView)); + activityRule.runOnUiThread(() -> controller.setVideoTextureView(testTextureView)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void setVideoTextureView_withNull_clearsTextureView() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void setVideoTextureView_withNull_clearsTextureView() throws Throwable { + MediaController controller = createController(); TextureView testTextureView = activity.getFirstTextureView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoTextureView(testTextureView)); + activityRule.runOnUiThread(() -> controller.setVideoTextureView(testTextureView)); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoTextureView(null)); + activityRule.runOnUiThread(() -> controller.setVideoTextureView(null)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test public void setVideoTextureView_whenSurfaceTextureIsDestroyed_surfaceIsClearedFromPlayer() throws Throwable { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + MediaController controller = createController(); TextureView testTextureView = activity.getFirstTextureView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoTextureView(testTextureView)); + activityRule.runOnUiThread(() -> controller.setVideoTextureView(testTextureView)); activityRule.runOnUiThread( () -> { @@ -368,14 +361,15 @@ public void setVideoTextureView_whenSurfaceTextureIsDestroyed_surfaceIsClearedFr }); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test public void setVideoTextureView_whenSurfaceTextureIsAvailable_surfaceIsSetToPlayer() throws Throwable { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + MediaController controller = createController(); TextureView testTextureView = activity.getFirstTextureView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoTextureView(testTextureView)); + activityRule.runOnUiThread(() -> controller.setVideoTextureView(testTextureView)); activityRule.runOnUiThread( () -> { ViewGroup rootViewGroup = activity.getRootViewGroup(); @@ -391,89 +385,98 @@ public void setVideoTextureView_whenSurfaceTextureIsAvailable_surfaceIsSetToPlay }); PollingCheck.waitFor(TIMEOUT_MS, () -> remoteSession.getMockPlayer().surfaceExists()); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoTextureView_withTheSameTextureView() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoTextureView_withTheSameTextureView() throws Throwable { + MediaController controller = createController(); TextureView testTextureView = activity.getFirstTextureView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoTextureView(testTextureView)); + activityRule.runOnUiThread(() -> controller.setVideoTextureView(testTextureView)); - threadTestRule - .getHandler() - .postAndSync(() -> controller.clearVideoTextureView(testTextureView)); + activityRule.runOnUiThread(() -> controller.clearVideoTextureView(testTextureView)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoTextureView_withDifferentTextureView_doesNothing() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoTextureView_withDifferentTextureView_doesNothing() throws Throwable { + MediaController controller = createController(); TextureView testTextureView = activity.getFirstTextureView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoTextureView(testTextureView)); + activityRule.runOnUiThread(() -> controller.setVideoTextureView(testTextureView)); TextureView anotherTestTextureView = activity.getSecondTextureView(); - threadTestRule - .getHandler() - .postAndSync(() -> controller.clearVideoTextureView(anotherTestTextureView)); + activityRule.runOnUiThread(() -> controller.clearVideoTextureView(anotherTestTextureView)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoTextureView_withNull_doesNothing() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoTextureView_withNull_doesNothing() throws Throwable { + MediaController controller = createController(); TextureView testTextureView = activity.getFirstTextureView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoTextureView(testTextureView)); + activityRule.runOnUiThread(() -> controller.setVideoTextureView(testTextureView)); - threadTestRule.getHandler().postAndSync(() -> controller.clearVideoTextureView(null)); + activityRule.runOnUiThread(() -> controller.clearVideoTextureView(null)); assertThat(remoteSession.getMockPlayer().surfaceExists()).isTrue(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurfaceWithNoArguments_afterSetVideoSurface() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurfaceWithNoArguments_afterSetVideoSurface() throws Throwable { + MediaController controller = createController(); Surface testSurface = activity.getFirstSurfaceHolder().getSurface(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurface(testSurface)); + activityRule.runOnUiThread(() -> controller.setVideoSurface(testSurface)); - threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurface()); + activityRule.runOnUiThread(() -> controller.clearVideoSurface()); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurfaceWithNoArguments_afterSetVideoSurfaceHolder() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurfaceWithNoArguments_afterSetVideoSurfaceHolder() throws Throwable { + MediaController controller = createController(); SurfaceHolder testSurfaceHolder = activity.getFirstSurfaceHolder(); - threadTestRule - .getHandler() - .postAndSync(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceHolder(testSurfaceHolder)); - threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurface()); + activityRule.runOnUiThread(() -> controller.clearVideoSurface()); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurfaceWithNoArguments_afterSetVideoSurfaceView() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurfaceWithNoArguments_afterSetVideoSurfaceView() throws Throwable { + MediaController controller = createController(); SurfaceView testSurfaceView = activity.getFirstSurfaceView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoSurfaceView(testSurfaceView)); + activityRule.runOnUiThread(() -> controller.setVideoSurfaceView(testSurfaceView)); - threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurface()); + activityRule.runOnUiThread(() -> controller.clearVideoSurface()); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); } @Test - public void clearVideoSurfaceWithNoArguments_afterSetVideoTextureView() throws Exception { - MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + public void clearVideoSurfaceWithNoArguments_afterSetVideoTextureView() throws Throwable { + MediaController controller = createController(); TextureView testTextureView = activity.getFirstTextureView(); - threadTestRule.getHandler().postAndSync(() -> controller.setVideoTextureView(testTextureView)); + activityRule.runOnUiThread(() -> controller.setVideoTextureView(testTextureView)); - threadTestRule.getHandler().postAndSync(() -> controller.clearVideoSurface()); + activityRule.runOnUiThread(() -> controller.clearVideoSurface()); assertThat(remoteSession.getMockPlayer().surfaceExists()).isFalse(); + activityRule.runOnUiThread(controller::release); + } + + private MediaController createController() throws Exception { + return new MediaController.Builder(activity, remoteSession.getToken()) + .setApplicationLooper(Looper.getMainLooper()) + .buildAsync() + .get(); } } From cdc07e21751273e46be5e18ef8b4fe7d3d2b7dd9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 13 Dec 2022 09:04:04 +0000 Subject: [PATCH 062/141] Forward seek command details to seekTo method in BasePlayer BasePlayer simplifies implementations by handling all the various seek methods and forwarding to a single method that can then be implemented by subclasses. However, this loses the information about the concrete entry point used for seeking, which is relevant when the subclass wants to verify or filter by Player.Command. This can be improved by adding the command as a new parameter. Since we have to change the method anyway, we can also incorporate the boolean flag about whether the current item is repeated to avoid the separate method. PiperOrigin-RevId: 494948094 (cherry picked from commit ab6fc6a08d0908afe59e7cd17fcaefa96acf1816) --- RELEASENOTES.md | 2 + .../java/androidx/media3/cast/CastPlayer.java | 8 +- .../androidx/media3/common/BasePlayer.java | 137 +++++--- .../media3/common/SimpleBasePlayer.java | 15 +- .../media3/common/BasePlayerTest.java | 318 ++++++++++++++++++ .../media3/exoplayer/ExoPlayerImpl.java | 95 +++--- .../media3/exoplayer/SimpleExoPlayer.java | 12 +- .../media3/test/utils/StubPlayer.java | 6 +- 8 files changed, 485 insertions(+), 108 deletions(-) create mode 100644 libraries/common/src/test/java/androidx/media3/common/BasePlayerTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e3b7ce1b0b2..4daafc12365 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,8 @@ playback thread for a new ExoPlayer instance. * Allow download manager helpers to be cleared ([#10776](https://github.com/google/ExoPlayer/issues/10776)). + * Add parameter to `BasePlayer.seekTo` to also indicate the command used + for seeking. * Audio: * Use the compressed audio format bitrate to calculate the min buffer size for `AudioTrack` in direct playbacks (passthrough). diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java index 57b81b3fa5c..b7c14438161 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java @@ -15,6 +15,7 @@ */ package androidx.media3.cast; +import static androidx.annotation.VisibleForTesting.PROTECTED; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Util.castNonNull; import static java.lang.Math.min; @@ -399,7 +400,12 @@ public boolean getPlayWhenReady() { // don't implement onPositionDiscontinuity(). @SuppressWarnings("deprecation") @Override - public void seekTo(int mediaItemIndex, long positionMs) { + @VisibleForTesting(otherwise = PROTECTED) + public void seekTo( + int mediaItemIndex, + long positionMs, + @Player.Command int seekCommand, + boolean isRepeatingCurrentItem) { MediaStatus mediaStatus = getMediaStatus(); // We assume the default position is 0. There is no support for seeking to the default position // in RemoteMediaClient. diff --git a/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java index 74b144baa38..b0f31e5d217 100644 --- a/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java @@ -15,14 +15,15 @@ */ package androidx.media3.common; +import static androidx.annotation.VisibleForTesting.PROTECTED; import static java.lang.Math.max; import static java.lang.Math.min; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; -import com.google.errorprone.annotations.ForOverride; import java.util.List; /** Abstract base {@link Player} which implements common implementation independent methods. */ @@ -121,27 +122,23 @@ && getPlayWhenReady() @Override public final void seekToDefaultPosition() { - seekToDefaultPosition(getCurrentMediaItemIndex()); + seekToDefaultPositionInternal( + getCurrentMediaItemIndex(), Player.COMMAND_SEEK_TO_DEFAULT_POSITION); } @Override public final void seekToDefaultPosition(int mediaItemIndex) { - seekTo(mediaItemIndex, /* positionMs= */ C.TIME_UNSET); - } - - @Override - public final void seekTo(long positionMs) { - seekTo(getCurrentMediaItemIndex(), positionMs); + seekToDefaultPositionInternal(mediaItemIndex, Player.COMMAND_SEEK_TO_MEDIA_ITEM); } @Override public final void seekBack() { - seekToOffset(-getSeekBackIncrement()); + seekToOffset(-getSeekBackIncrement(), Player.COMMAND_SEEK_BACK); } @Override public final void seekForward() { - seekToOffset(getSeekForwardIncrement()); + seekToOffset(getSeekForwardIncrement(), Player.COMMAND_SEEK_FORWARD); } /** @@ -187,15 +184,7 @@ public final void seekToPreviousWindow() { @Override public final void seekToPreviousMediaItem() { - int previousMediaItemIndex = getPreviousMediaItemIndex(); - if (previousMediaItemIndex == C.INDEX_UNSET) { - return; - } - if (previousMediaItemIndex == getCurrentMediaItemIndex()) { - repeatCurrentMediaItem(); - } else { - seekToDefaultPosition(previousMediaItemIndex); - } + seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM); } @Override @@ -207,12 +196,12 @@ public final void seekToPrevious() { boolean hasPreviousMediaItem = hasPreviousMediaItem(); if (isCurrentMediaItemLive() && !isCurrentMediaItemSeekable()) { if (hasPreviousMediaItem) { - seekToPreviousMediaItem(); + seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS); } } else if (hasPreviousMediaItem && getCurrentPosition() <= getMaxSeekToPreviousPosition()) { - seekToPreviousMediaItem(); + seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS); } else { - seekTo(/* positionMs= */ 0); + seekToCurrentItem(/* positionMs= */ 0, Player.COMMAND_SEEK_TO_PREVIOUS); } } @@ -259,15 +248,7 @@ public final void seekToNextWindow() { @Override public final void seekToNextMediaItem() { - int nextMediaItemIndex = getNextMediaItemIndex(); - if (nextMediaItemIndex == C.INDEX_UNSET) { - return; - } - if (nextMediaItemIndex == getCurrentMediaItemIndex()) { - repeatCurrentMediaItem(); - } else { - seekToDefaultPosition(nextMediaItemIndex); - } + seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM); } @Override @@ -277,12 +258,42 @@ public final void seekToNext() { return; } if (hasNextMediaItem()) { - seekToNextMediaItem(); + seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT); } else if (isCurrentMediaItemLive() && isCurrentMediaItemDynamic()) { - seekToDefaultPosition(); + seekToDefaultPositionInternal(getCurrentMediaItemIndex(), Player.COMMAND_SEEK_TO_NEXT); } } + @Override + public final void seekTo(long positionMs) { + seekToCurrentItem(positionMs, Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM); + } + + @Override + public final void seekTo(int mediaItemIndex, long positionMs) { + seekTo( + mediaItemIndex, + positionMs, + Player.COMMAND_SEEK_TO_MEDIA_ITEM, + /* isRepeatingCurrentItem= */ false); + } + + /** + * Seeks to a position in the specified {@link MediaItem}. + * + * @param mediaItemIndex The index of the {@link MediaItem}. + * @param positionMs The seek position in the specified {@link MediaItem} in milliseconds, or + * {@link C#TIME_UNSET} to seek to the media item's default position. + * @param seekCommand The {@link Player.Command} used to trigger the seek. + * @param isRepeatingCurrentItem Whether this seeks repeats the current item. + */ + @VisibleForTesting(otherwise = PROTECTED) + public abstract void seekTo( + int mediaItemIndex, + long positionMs, + @Player.Command int seekCommand, + boolean isRepeatingCurrentItem); + @Override public final void setPlaybackSpeed(float speed) { setPlaybackParameters(getPlaybackParameters().withSpeed(speed)); @@ -437,29 +448,63 @@ public final long getContentDuration() { : timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs(); } - /** - * Repeat the current media item. - * - *

      The default implementation seeks to the default position in the current item, which can be - * overridden for additional handling. - */ - @ForOverride - protected void repeatCurrentMediaItem() { - seekToDefaultPosition(); - } - private @RepeatMode int getRepeatModeForNavigation() { @RepeatMode int repeatMode = getRepeatMode(); return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode; } - private void seekToOffset(long offsetMs) { + private void seekToCurrentItem(long positionMs, @Player.Command int seekCommand) { + seekTo( + getCurrentMediaItemIndex(), positionMs, seekCommand, /* isRepeatingCurrentItem= */ false); + } + + private void seekToOffset(long offsetMs, @Player.Command int seekCommand) { long positionMs = getCurrentPosition() + offsetMs; long durationMs = getDuration(); if (durationMs != C.TIME_UNSET) { positionMs = min(positionMs, durationMs); } positionMs = max(positionMs, 0); - seekTo(positionMs); + seekToCurrentItem(positionMs, seekCommand); + } + + private void seekToDefaultPositionInternal(int mediaItemIndex, @Player.Command int seekCommand) { + seekTo( + mediaItemIndex, + /* positionMs= */ C.TIME_UNSET, + seekCommand, + /* isRepeatingCurrentItem= */ false); + } + + private void seekToNextMediaItemInternal(@Player.Command int seekCommand) { + int nextMediaItemIndex = getNextMediaItemIndex(); + if (nextMediaItemIndex == C.INDEX_UNSET) { + return; + } + if (nextMediaItemIndex == getCurrentMediaItemIndex()) { + repeatCurrentMediaItem(seekCommand); + } else { + seekToDefaultPositionInternal(nextMediaItemIndex, seekCommand); + } + } + + private void seekToPreviousMediaItemInternal(@Player.Command int seekCommand) { + int previousMediaItemIndex = getPreviousMediaItemIndex(); + if (previousMediaItemIndex == C.INDEX_UNSET) { + return; + } + if (previousMediaItemIndex == getCurrentMediaItemIndex()) { + repeatCurrentMediaItem(seekCommand); + } else { + seekToDefaultPositionInternal(previousMediaItemIndex, seekCommand); + } + } + + private void repeatCurrentMediaItem(@Player.Command int seekCommand) { + seekTo( + getCurrentMediaItemIndex(), + /* positionMs= */ C.TIME_UNSET, + seekCommand, + /* isRepeatingCurrentItem= */ true); } } diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index 2a2a3ffc45a..512b9d9311c 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -15,6 +15,7 @@ */ package androidx.media3.common; +import static androidx.annotation.VisibleForTesting.PROTECTED; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull; @@ -32,6 +33,7 @@ import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Clock; import androidx.media3.common.util.HandlerWrapper; @@ -2133,13 +2135,12 @@ public final boolean isLoading() { } @Override - public final void seekTo(int mediaItemIndex, long positionMs) { - // TODO: implement. - throw new IllegalStateException(); - } - - @Override - protected final void repeatCurrentMediaItem() { + @VisibleForTesting(otherwise = PROTECTED) + public final void seekTo( + int mediaItemIndex, + long positionMs, + @Player.Command int seekCommand, + boolean isRepeatingCurrentItem) { // TODO: implement. throw new IllegalStateException(); } diff --git a/libraries/common/src/test/java/androidx/media3/common/BasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/BasePlayerTest.java new file mode 100644 index 00000000000..4f3c677f66f --- /dev/null +++ b/libraries/common/src/test/java/androidx/media3/common/BasePlayerTest.java @@ -0,0 +1,318 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.common; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import androidx.media3.test.utils.FakeTimeline; +import androidx.media3.test.utils.StubPlayer; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link BasePlayer}. */ +@RunWith(AndroidJUnit4.class) +public class BasePlayerTest { + + @Test + public void seekTo_withIndexAndPosition_usesCommandSeekToMediaItem() { + BasePlayer player = spy(new TestBasePlayer()); + + player.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 4000); + + verify(player) + .seekTo( + /* mediaItemIndex= */ 2, + /* positionMs= */ 4000, + Player.COMMAND_SEEK_TO_MEDIA_ITEM, + /* isRepeatingCurrentItem= */ false); + } + + @Test + public void seekTo_withPosition_usesCommandSeekInCurrentMediaItem() { + BasePlayer player = + spy( + new TestBasePlayer() { + @Override + public int getCurrentMediaItemIndex() { + return 1; + } + }); + + player.seekTo(/* positionMs= */ 4000); + + verify(player) + .seekTo( + /* mediaItemIndex= */ 1, + /* positionMs= */ 4000, + Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, + /* isRepeatingCurrentItem= */ false); + } + + @Test + public void seekToDefaultPosition_withIndex_usesCommandSeekToMediaItem() { + BasePlayer player = spy(new TestBasePlayer()); + + player.seekToDefaultPosition(/* mediaItemIndex= */ 2); + + verify(player) + .seekTo( + /* mediaItemIndex= */ 2, + /* positionMs= */ C.TIME_UNSET, + Player.COMMAND_SEEK_TO_MEDIA_ITEM, + /* isRepeatingCurrentItem= */ false); + } + + @Test + public void seekToDefaultPosition_withoutIndex_usesCommandSeekToDefaultPosition() { + BasePlayer player = + spy( + new TestBasePlayer() { + @Override + public int getCurrentMediaItemIndex() { + return 1; + } + }); + + player.seekToDefaultPosition(); + + verify(player) + .seekTo( + /* mediaItemIndex= */ 1, + /* positionMs= */ C.TIME_UNSET, + Player.COMMAND_SEEK_TO_DEFAULT_POSITION, + /* isRepeatingCurrentItem= */ false); + } + + @Test + public void seekToNext_usesCommandSeekToNext() { + BasePlayer player = + spy( + new TestBasePlayer() { + @Override + public int getCurrentMediaItemIndex() { + return 1; + } + }); + + player.seekToNext(); + + verify(player) + .seekTo( + /* mediaItemIndex= */ 2, + /* positionMs= */ C.TIME_UNSET, + Player.COMMAND_SEEK_TO_NEXT, + /* isRepeatingCurrentItem= */ false); + } + + @Test + public void seekToNextMediaItem_usesCommandSeekToNextMediaItem() { + BasePlayer player = + spy( + new TestBasePlayer() { + @Override + public int getCurrentMediaItemIndex() { + return 1; + } + }); + + player.seekToNextMediaItem(); + + verify(player) + .seekTo( + /* mediaItemIndex= */ 2, + /* positionMs= */ C.TIME_UNSET, + Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, + /* isRepeatingCurrentItem= */ false); + } + + @Test + public void seekForward_usesCommandSeekForward() { + BasePlayer player = + spy( + new TestBasePlayer() { + @Override + public long getSeekForwardIncrement() { + return 2000; + } + + @Override + public int getCurrentMediaItemIndex() { + return 1; + } + + @Override + public long getCurrentPosition() { + return 5000; + } + }); + + player.seekForward(); + + verify(player) + .seekTo( + /* mediaItemIndex= */ 1, + /* positionMs= */ 7000, + Player.COMMAND_SEEK_FORWARD, + /* isRepeatingCurrentItem= */ false); + } + + @Test + public void seekToPrevious_usesCommandSeekToPrevious() { + BasePlayer player = + spy( + new TestBasePlayer() { + @Override + public int getCurrentMediaItemIndex() { + return 1; + } + + @Override + public long getMaxSeekToPreviousPosition() { + return 4000; + } + + @Override + public long getCurrentPosition() { + return 2000; + } + }); + + player.seekToPrevious(); + + verify(player) + .seekTo( + /* mediaItemIndex= */ 0, + /* positionMs= */ C.TIME_UNSET, + Player.COMMAND_SEEK_TO_PREVIOUS, + /* isRepeatingCurrentItem= */ false); + } + + @Test + public void seekToPreviousMediaItem_usesCommandSeekToPreviousMediaItem() { + BasePlayer player = + spy( + new TestBasePlayer() { + @Override + public int getCurrentMediaItemIndex() { + return 1; + } + }); + + player.seekToPreviousMediaItem(); + + verify(player) + .seekTo( + /* mediaItemIndex= */ 0, + /* positionMs= */ C.TIME_UNSET, + Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, + /* isRepeatingCurrentItem= */ false); + } + + @Test + public void seekBack_usesCommandSeekBack() { + BasePlayer player = + spy( + new TestBasePlayer() { + @Override + public long getSeekBackIncrement() { + return 2000; + } + + @Override + public int getCurrentMediaItemIndex() { + return 1; + } + + @Override + public long getCurrentPosition() { + return 5000; + } + }); + + player.seekBack(); + + verify(player) + .seekTo( + /* mediaItemIndex= */ 1, + /* positionMs= */ 3000, + Player.COMMAND_SEEK_BACK, + /* isRepeatingCurrentItem= */ false); + } + + private static class TestBasePlayer extends StubPlayer { + + @Override + public void seekTo( + int mediaItemIndex, + long positionMs, + @Player.Command int seekCommand, + boolean isRepeatingCurrentItem) { + // Do nothing. + } + + @Override + public long getSeekBackIncrement() { + return 2000; + } + + @Override + public long getSeekForwardIncrement() { + return 2000; + } + + @Override + public long getMaxSeekToPreviousPosition() { + return 2000; + } + + @Override + public Timeline getCurrentTimeline() { + return new FakeTimeline(/* windowCount= */ 3); + } + + @Override + public int getCurrentMediaItemIndex() { + return 1; + } + + @Override + public long getCurrentPosition() { + return 5000; + } + + @Override + public long getDuration() { + return 20000; + } + + @Override + public boolean isPlayingAd() { + return false; + } + + @Override + public int getRepeatMode() { + return Player.REPEAT_MODE_OFF; + } + + @Override + public boolean getShuffleModeEnabled() { + return false; + } + } +} diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 5f27dc546a7..4e6ebf0c325 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -823,16 +823,51 @@ public boolean isLoading() { } @Override - protected void repeatCurrentMediaItem() { + public void seekTo( + int mediaItemIndex, + long positionMs, + @Player.Command int seekCommand, + boolean isRepeatingCurrentItem) { verifyApplicationThread(); - seekToInternal( - getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET, /* repeatMediaItem= */ true); - } - - @Override - public void seekTo(int mediaItemIndex, long positionMs) { - verifyApplicationThread(); - seekToInternal(mediaItemIndex, positionMs, /* repeatMediaItem= */ false); + analyticsCollector.notifySeekStarted(); + Timeline timeline = playbackInfo.timeline; + if (mediaItemIndex < 0 + || (!timeline.isEmpty() && mediaItemIndex >= timeline.getWindowCount())) { + throw new IllegalSeekPositionException(timeline, mediaItemIndex, positionMs); + } + pendingOperationAcks++; + if (isPlayingAd()) { + // TODO: Investigate adding support for seeking during ads. This is complicated to do in + // general because the midroll ad preceding the seek destination must be played before the + // content position can be played, if a different ad is playing at the moment. + Log.w(TAG, "seekTo ignored because an ad is playing"); + ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate = + new ExoPlayerImplInternal.PlaybackInfoUpdate(this.playbackInfo); + playbackInfoUpdate.incrementPendingOperationAcks(1); + playbackInfoUpdateListener.onPlaybackInfoUpdate(playbackInfoUpdate); + return; + } + @Player.State + int newPlaybackState = + getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : STATE_BUFFERING; + int oldMaskingMediaItemIndex = getCurrentMediaItemIndex(); + PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackState(newPlaybackState); + newPlaybackInfo = + maskTimelineAndPosition( + newPlaybackInfo, + timeline, + maskWindowPositionMsOrGetPeriodPositionUs(timeline, mediaItemIndex, positionMs)); + internalPlayer.seekTo(timeline, mediaItemIndex, Util.msToUs(positionMs)); + updatePlaybackInfo( + newPlaybackInfo, + /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + /* seekProcessed= */ true, + /* positionDiscontinuity= */ true, + /* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK, + /* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo), + oldMaskingMediaItemIndex, + isRepeatingCurrentItem); } @Override @@ -2696,48 +2731,6 @@ private void updatePriorityTaskManagerForIsLoadingChange(boolean isLoading) { } } - private void seekToInternal(int mediaItemIndex, long positionMs, boolean repeatMediaItem) { - analyticsCollector.notifySeekStarted(); - Timeline timeline = playbackInfo.timeline; - if (mediaItemIndex < 0 - || (!timeline.isEmpty() && mediaItemIndex >= timeline.getWindowCount())) { - throw new IllegalSeekPositionException(timeline, mediaItemIndex, positionMs); - } - pendingOperationAcks++; - if (isPlayingAd()) { - // TODO: Investigate adding support for seeking during ads. This is complicated to do in - // general because the midroll ad preceding the seek destination must be played before the - // content position can be played, if a different ad is playing at the moment. - Log.w(TAG, "seekTo ignored because an ad is playing"); - ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate = - new ExoPlayerImplInternal.PlaybackInfoUpdate(this.playbackInfo); - playbackInfoUpdate.incrementPendingOperationAcks(1); - playbackInfoUpdateListener.onPlaybackInfoUpdate(playbackInfoUpdate); - return; - } - @Player.State - int newPlaybackState = - getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : STATE_BUFFERING; - int oldMaskingMediaItemIndex = getCurrentMediaItemIndex(); - PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackState(newPlaybackState); - newPlaybackInfo = - maskTimelineAndPosition( - newPlaybackInfo, - timeline, - maskWindowPositionMsOrGetPeriodPositionUs(timeline, mediaItemIndex, positionMs)); - internalPlayer.seekTo(timeline, mediaItemIndex, Util.msToUs(positionMs)); - updatePlaybackInfo( - newPlaybackInfo, - /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, - /* seekProcessed= */ true, - /* positionDiscontinuity= */ true, - /* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK, - /* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo), - oldMaskingMediaItemIndex, - repeatMediaItem); - } - private static DeviceInfo createDeviceInfo(StreamVolumeManager streamVolumeManager) { return new DeviceInfo( DeviceInfo.PLAYBACK_TYPE_LOCAL, diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java index c5150e5c7f4..5676ce75541 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java @@ -15,6 +15,8 @@ */ package androidx.media3.exoplayer; +import static androidx.annotation.VisibleForTesting.PROTECTED; + import android.content.Context; import android.media.AudioDeviceInfo; import android.os.Looper; @@ -1004,10 +1006,16 @@ public boolean isLoading() { return player.isLoading(); } + @SuppressWarnings("ForOverride") // Forwarding to ForOverride method in ExoPlayerImpl. @Override - public void seekTo(int mediaItemIndex, long positionMs) { + @VisibleForTesting(otherwise = PROTECTED) + public void seekTo( + int mediaItemIndex, + long positionMs, + @Player.Command int seekCommand, + boolean isRepeatingCurrentItem) { blockUntilConstructorFinished(); - player.seekTo(mediaItemIndex, positionMs); + player.seekTo(mediaItemIndex, positionMs, seekCommand, isRepeatingCurrentItem); } @Override diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java index 8638745a564..5845cdaceb4 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java @@ -147,7 +147,11 @@ public boolean isLoading() { } @Override - public void seekTo(int mediaItemIndex, long positionMs) { + public void seekTo( + int mediaItemIndex, + long positionMs, + @Player.Command int seekCommand, + boolean isRepeatingCurrentItem) { throw new UnsupportedOperationException(); } From 11bd727ac50bcfbdd6d58475239ccb5d0f744be0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 13 Dec 2022 11:37:20 +0000 Subject: [PATCH 063/141] Reset isLoading when calling SimpleBasePlayer.stop/release isLoading is not allowed to be true when IDLE, so we have to set to false when stopping in case it was set to true before. PiperOrigin-RevId: 494975405 (cherry picked from commit 6e7de583bb42871267899776966575512152b111) --- .../java/androidx/media3/common/SimpleBasePlayer.java | 2 ++ .../androidx/media3/common/SimpleBasePlayerTest.java | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index 512b9d9311c..0d6f24d98e9 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -2200,6 +2200,7 @@ public final void stop() { .setTotalBufferedDurationMs(PositionSupplier.ZERO) .setContentBufferedPositionMs(state.contentPositionMsSupplier) .setAdBufferedPositionMs(state.adPositionMsSupplier) + .setIsLoading(false) .build()); } @@ -2231,6 +2232,7 @@ public final void release() { .setTotalBufferedDurationMs(PositionSupplier.ZERO) .setContentBufferedPositionMs(state.contentPositionMsSupplier) .setAdBufferedPositionMs(state.adPositionMsSupplier) + .setIsLoading(false) .build(); } diff --git a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java index 5c53e5e27b7..498fb600683 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -2095,6 +2095,7 @@ public void stop_asyncHandling_usesPlaceholderStateAndInformsListeners() { .setPlaylist( ImmutableList.of( new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .setIsLoading(true) .build(); // Additionally set the repeat mode to see a difference between the placeholder and new state. State updatedState = @@ -2102,6 +2103,7 @@ public void stop_asyncHandling_usesPlaceholderStateAndInformsListeners() { .buildUpon() .setPlaybackState(Player.STATE_IDLE) .setRepeatMode(Player.REPEAT_MODE_ALL) + .setIsLoading(false) .build(); SettableFuture future = SettableFuture.create(); SimpleBasePlayer player = @@ -2124,9 +2126,12 @@ protected ListenableFuture handleStop() { // Verify placeholder state and listener calls. assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF); + assertThat(player.isLoading()).isFalse(); verify(listener).onPlaybackStateChanged(Player.STATE_IDLE); verify(listener) .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + verify(listener).onIsLoadingChanged(false); + verify(listener).onLoadingChanged(false); verifyNoMoreInteractions(listener); future.set(null); @@ -2211,6 +2216,7 @@ public void release_asyncHandling_returnsIdleAndIgnoredAsyncStateUpdate() { .setPlaylist( ImmutableList.of( new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .setIsLoading(true) .build(); // Additionally set the repeat mode to see a difference between the placeholder and new state. State updatedState = state.buildUpon().setRepeatMode(Player.REPEAT_MODE_ALL).build(); @@ -2232,8 +2238,9 @@ protected ListenableFuture handleRelease() { player.release(); - // Verify initial change to IDLE without listener call. + // Verify initial change to IDLE and !isLoading without listener call. assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + assertThat(player.isLoading()).isFalse(); verifyNoMoreInteractions(listener); future.set(null); From 1e7480d78aa4d845b69704b4f62092691be3daa7 Mon Sep 17 00:00:00 2001 From: rohks Date: Tue, 13 Dec 2022 14:27:54 +0000 Subject: [PATCH 064/141] Document the reason for defining private method `defaultIfNull` PiperOrigin-RevId: 495004732 (cherry picked from commit 610e431c906d71fd684c5c7c8ff8a9aa171a55ef) --- .../src/main/java/androidx/media3/common/Format.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libraries/common/src/main/java/androidx/media3/common/Format.java b/libraries/common/src/main/java/androidx/media3/common/Format.java index bb712e24723..8e08993f1aa 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Format.java +++ b/libraries/common/src/main/java/androidx/media3/common/Format.java @@ -1690,6 +1690,14 @@ private static String keyForInitializationData(int initialisationDataIndex) { + Integer.toString(initialisationDataIndex, Character.MAX_RADIX); } + /** + * Utility method to get {@code defaultValue} if {@code value} is {@code null}. {@code + * defaultValue} can be {@code null}. + * + *

      Note: Current implementations of getters in {@link Bundle}, for example {@link + * Bundle#getString(String, String)} does not allow the defaultValue to be {@code null}, hence the + * need for this method. + */ @Nullable private static T defaultIfNull(@Nullable T value, @Nullable T defaultValue) { return value != null ? value : defaultValue; From d91c005a2144c13459be08a53be0c9debc81b93e Mon Sep 17 00:00:00 2001 From: rohks Date: Tue, 13 Dec 2022 18:09:51 +0000 Subject: [PATCH 065/141] Remove parameters with default values from bundle in `MediaItem` This improves the time taken to construct PlayerInfo from bundle from ~600ms to ~450ms. PiperOrigin-RevId: 495055355 (cherry picked from commit 395cf4debc52c9209377ea85a319d2e27c6533ce) --- .../androidx/media3/common/MediaItem.java | 97 ++++++++++---- .../androidx/media3/common/MediaItemTest.java | 119 +++++++++++++++++- 2 files changed, 188 insertions(+), 28 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java index 7770a8c2765..68f3a828828 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java @@ -1122,7 +1122,7 @@ public static final class Builder { private float minPlaybackSpeed; private float maxPlaybackSpeed; - /** Constructs an instance. */ + /** Creates a new instance with default values. */ public Builder() { this.targetOffsetMs = C.TIME_UNSET; this.minOffsetMs = C.TIME_UNSET; @@ -1326,11 +1326,21 @@ public int hashCode() { @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putLong(keyForField(FIELD_TARGET_OFFSET_MS), targetOffsetMs); - bundle.putLong(keyForField(FIELD_MIN_OFFSET_MS), minOffsetMs); - bundle.putLong(keyForField(FIELD_MAX_OFFSET_MS), maxOffsetMs); - bundle.putFloat(keyForField(FIELD_MIN_PLAYBACK_SPEED), minPlaybackSpeed); - bundle.putFloat(keyForField(FIELD_MAX_PLAYBACK_SPEED), maxPlaybackSpeed); + if (targetOffsetMs != UNSET.targetOffsetMs) { + bundle.putLong(keyForField(FIELD_TARGET_OFFSET_MS), targetOffsetMs); + } + if (minOffsetMs != UNSET.minOffsetMs) { + bundle.putLong(keyForField(FIELD_MIN_OFFSET_MS), minOffsetMs); + } + if (maxOffsetMs != UNSET.maxOffsetMs) { + bundle.putLong(keyForField(FIELD_MAX_OFFSET_MS), maxOffsetMs); + } + if (minPlaybackSpeed != UNSET.minPlaybackSpeed) { + bundle.putFloat(keyForField(FIELD_MIN_PLAYBACK_SPEED), minPlaybackSpeed); + } + if (maxPlaybackSpeed != UNSET.maxPlaybackSpeed) { + bundle.putFloat(keyForField(FIELD_MAX_PLAYBACK_SPEED), maxPlaybackSpeed); + } return bundle; } @@ -1340,13 +1350,17 @@ public Bundle toBundle() { bundle -> new LiveConfiguration( bundle.getLong( - keyForField(FIELD_TARGET_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET), - bundle.getLong(keyForField(FIELD_MIN_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET), - bundle.getLong(keyForField(FIELD_MAX_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET), + keyForField(FIELD_TARGET_OFFSET_MS), /* defaultValue= */ UNSET.targetOffsetMs), + bundle.getLong( + keyForField(FIELD_MIN_OFFSET_MS), /* defaultValue= */ UNSET.minOffsetMs), + bundle.getLong( + keyForField(FIELD_MAX_OFFSET_MS), /* defaultValue= */ UNSET.maxOffsetMs), bundle.getFloat( - keyForField(FIELD_MIN_PLAYBACK_SPEED), /* defaultValue= */ C.RATE_UNSET), + keyForField(FIELD_MIN_PLAYBACK_SPEED), + /* defaultValue= */ UNSET.minPlaybackSpeed), bundle.getFloat( - keyForField(FIELD_MAX_PLAYBACK_SPEED), /* defaultValue= */ C.RATE_UNSET)); + keyForField(FIELD_MAX_PLAYBACK_SPEED), + /* defaultValue= */ UNSET.maxPlaybackSpeed)); private static String keyForField(@LiveConfiguration.FieldNumber int field) { return Integer.toString(field, Character.MAX_RADIX); @@ -1589,7 +1603,7 @@ public static final class Builder { private boolean relativeToDefaultPosition; private boolean startsAtKeyFrame; - /** Constructs an instance. */ + /** Creates a new instance with default values. */ public Builder() { endPositionMs = C.TIME_END_OF_SOURCE; } @@ -1764,11 +1778,22 @@ public int hashCode() { @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putLong(keyForField(FIELD_START_POSITION_MS), startPositionMs); - bundle.putLong(keyForField(FIELD_END_POSITION_MS), endPositionMs); - bundle.putBoolean(keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), relativeToLiveWindow); - bundle.putBoolean(keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), relativeToDefaultPosition); - bundle.putBoolean(keyForField(FIELD_STARTS_AT_KEY_FRAME), startsAtKeyFrame); + if (startPositionMs != UNSET.startPositionMs) { + bundle.putLong(keyForField(FIELD_START_POSITION_MS), startPositionMs); + } + if (endPositionMs != UNSET.endPositionMs) { + bundle.putLong(keyForField(FIELD_END_POSITION_MS), endPositionMs); + } + if (relativeToLiveWindow != UNSET.relativeToLiveWindow) { + bundle.putBoolean(keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), relativeToLiveWindow); + } + if (relativeToDefaultPosition != UNSET.relativeToDefaultPosition) { + bundle.putBoolean( + keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), relativeToDefaultPosition); + } + if (startsAtKeyFrame != UNSET.startsAtKeyFrame) { + bundle.putBoolean(keyForField(FIELD_STARTS_AT_KEY_FRAME), startsAtKeyFrame); + } return bundle; } @@ -1778,17 +1803,25 @@ public Bundle toBundle() { bundle -> new ClippingConfiguration.Builder() .setStartPositionMs( - bundle.getLong(keyForField(FIELD_START_POSITION_MS), /* defaultValue= */ 0)) + bundle.getLong( + keyForField(FIELD_START_POSITION_MS), + /* defaultValue= */ UNSET.startPositionMs)) .setEndPositionMs( bundle.getLong( keyForField(FIELD_END_POSITION_MS), - /* defaultValue= */ C.TIME_END_OF_SOURCE)) + /* defaultValue= */ UNSET.endPositionMs)) .setRelativeToLiveWindow( - bundle.getBoolean(keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), false)) + bundle.getBoolean( + keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), + /* defaultValue= */ UNSET.relativeToLiveWindow)) .setRelativeToDefaultPosition( - bundle.getBoolean(keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), false)) + bundle.getBoolean( + keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), + /* defaultValue= */ UNSET.relativeToDefaultPosition)) .setStartsAtKeyFrame( - bundle.getBoolean(keyForField(FIELD_STARTS_AT_KEY_FRAME), false)) + bundle.getBoolean( + keyForField(FIELD_STARTS_AT_KEY_FRAME), + /* defaultValue= */ UNSET.startsAtKeyFrame)) .buildClippingProperties(); private static String keyForField(@ClippingConfiguration.FieldNumber int field) { @@ -2075,11 +2108,21 @@ public int hashCode() { @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putString(keyForField(FIELD_MEDIA_ID), mediaId); - bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle()); - bundle.putBundle(keyForField(FIELD_MEDIA_METADATA), mediaMetadata.toBundle()); - bundle.putBundle(keyForField(FIELD_CLIPPING_PROPERTIES), clippingConfiguration.toBundle()); - bundle.putBundle(keyForField(FIELD_REQUEST_METADATA), requestMetadata.toBundle()); + if (!mediaId.equals(DEFAULT_MEDIA_ID)) { + bundle.putString(keyForField(FIELD_MEDIA_ID), mediaId); + } + if (!liveConfiguration.equals(LiveConfiguration.UNSET)) { + bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle()); + } + if (!mediaMetadata.equals(MediaMetadata.EMPTY)) { + bundle.putBundle(keyForField(FIELD_MEDIA_METADATA), mediaMetadata.toBundle()); + } + if (!clippingConfiguration.equals(ClippingConfiguration.UNSET)) { + bundle.putBundle(keyForField(FIELD_CLIPPING_PROPERTIES), clippingConfiguration.toBundle()); + } + if (!requestMetadata.equals(RequestMetadata.EMPTY)) { + bundle.putBundle(keyForField(FIELD_REQUEST_METADATA), requestMetadata.toBundle()); + } return bundle; } diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java index f861a701ef7..30b5853e5f8 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java @@ -360,10 +360,12 @@ public void builderSetClippingConfiguration() { } @Test - public void clippingConfigurationDefaults() { + public void createDefaultClippingConfigurationInstance_checksDefaultValues() { MediaItem.ClippingConfiguration clippingConfiguration = new MediaItem.ClippingConfiguration.Builder().build(); + // Please refrain from altering default values since doing so would cause issues with backwards + // compatibility. assertThat(clippingConfiguration.startPositionMs).isEqualTo(0L); assertThat(clippingConfiguration.endPositionMs).isEqualTo(C.TIME_END_OF_SOURCE); assertThat(clippingConfiguration.relativeToLiveWindow).isFalse(); @@ -372,6 +374,32 @@ public void clippingConfigurationDefaults() { assertThat(clippingConfiguration).isEqualTo(MediaItem.ClippingConfiguration.UNSET); } + @Test + public void createDefaultClippingConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() { + MediaItem.ClippingConfiguration clippingConfiguration = + new MediaItem.ClippingConfiguration.Builder().build(); + + MediaItem.ClippingConfiguration clippingConfigurationFromBundle = + MediaItem.ClippingConfiguration.CREATOR.fromBundle(clippingConfiguration.toBundle()); + + assertThat(clippingConfigurationFromBundle).isEqualTo(clippingConfiguration); + } + + @Test + public void createClippingConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() { + // Creates instance by setting some non-default values + MediaItem.ClippingConfiguration clippingConfiguration = + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(1000L) + .setStartsAtKeyFrame(true) + .build(); + + MediaItem.ClippingConfiguration clippingConfigurationFromBundle = + MediaItem.ClippingConfiguration.CREATOR.fromBundle(clippingConfiguration.toBundle()); + + assertThat(clippingConfigurationFromBundle).isEqualTo(clippingConfiguration); + } + @Test public void clippingConfigurationBuilder_throwsOnInvalidValues() { MediaItem.ClippingConfiguration.Builder clippingConfigurationBuilder = @@ -514,6 +542,47 @@ public void builderSetMediaMetadata_setsMetadata() { assertThat(mediaItem.mediaMetadata).isEqualTo(mediaMetadata); } + @Test + public void createDefaultLiveConfigurationInstance_checksDefaultValues() { + MediaItem.LiveConfiguration liveConfiguration = + new MediaItem.LiveConfiguration.Builder().build(); + + // Please refrain from altering default values since doing so would cause issues with backwards + // compatibility. + assertThat(liveConfiguration.targetOffsetMs).isEqualTo(C.TIME_UNSET); + assertThat(liveConfiguration.minOffsetMs).isEqualTo(C.TIME_UNSET); + assertThat(liveConfiguration.maxOffsetMs).isEqualTo(C.TIME_UNSET); + assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(liveConfiguration).isEqualTo(MediaItem.LiveConfiguration.UNSET); + } + + @Test + public void createDefaultLiveConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() { + MediaItem.LiveConfiguration liveConfiguration = + new MediaItem.LiveConfiguration.Builder().build(); + + MediaItem.LiveConfiguration liveConfigurationFromBundle = + MediaItem.LiveConfiguration.CREATOR.fromBundle(liveConfiguration.toBundle()); + + assertThat(liveConfigurationFromBundle).isEqualTo(liveConfiguration); + } + + @Test + public void createLiveConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() { + // Creates instance by setting some non-default values + MediaItem.LiveConfiguration liveConfiguration = + new MediaItem.LiveConfiguration.Builder() + .setTargetOffsetMs(10_000) + .setMaxPlaybackSpeed(2f) + .build(); + + MediaItem.LiveConfiguration liveConfigurationFromBundle = + MediaItem.LiveConfiguration.CREATOR.fromBundle(liveConfiguration.toBundle()); + + assertThat(liveConfigurationFromBundle).isEqualTo(liveConfiguration); + } + @Test public void builderSetLiveConfiguration() { MediaItem mediaItem = @@ -747,4 +816,52 @@ public void roundTripViaBundle_withPlaybackProperties_dropsPlaybackProperties() assertThat(mediaItem.localConfiguration).isNotNull(); assertThat(MediaItem.CREATOR.fromBundle(mediaItem.toBundle()).localConfiguration).isNull(); } + + @Test + public void createDefaultMediaItemInstance_checksDefaultValues() { + MediaItem mediaItem = new MediaItem.Builder().build(); + + // Please refrain from altering default values since doing so would cause issues with backwards + // compatibility. + assertThat(mediaItem.mediaId).isEqualTo(MediaItem.DEFAULT_MEDIA_ID); + assertThat(mediaItem.liveConfiguration).isEqualTo(MediaItem.LiveConfiguration.UNSET); + assertThat(mediaItem.mediaMetadata).isEqualTo(MediaMetadata.EMPTY); + assertThat(mediaItem.clippingConfiguration).isEqualTo(MediaItem.ClippingConfiguration.UNSET); + assertThat(mediaItem.requestMetadata).isEqualTo(RequestMetadata.EMPTY); + assertThat(mediaItem).isEqualTo(MediaItem.EMPTY); + } + + @Test + public void createDefaultMediaItemInstance_roundTripViaBundle_yieldsEqualInstance() { + MediaItem mediaItem = new MediaItem.Builder().build(); + + MediaItem mediaItemFromBundle = MediaItem.CREATOR.fromBundle(mediaItem.toBundle()); + + assertThat(mediaItemFromBundle).isEqualTo(mediaItem); + } + + @Test + public void createMediaItemInstance_roundTripViaBundle_yieldsEqualInstance() { + // Creates instance by setting some non-default values + MediaItem mediaItem = + new MediaItem.Builder() + .setLiveConfiguration( + new MediaItem.LiveConfiguration.Builder() + .setTargetOffsetMs(20_000) + .setMinOffsetMs(2_222) + .setMaxOffsetMs(4_444) + .setMinPlaybackSpeed(.9f) + .setMaxPlaybackSpeed(1.1f) + .build()) + .setRequestMetadata( + new RequestMetadata.Builder() + .setMediaUri(Uri.parse("http://test.test")) + .setSearchQuery("search") + .build()) + .build(); + + MediaItem mediaItemFromBundle = MediaItem.CREATOR.fromBundle(mediaItem.toBundle()); + + assertThat(mediaItemFromBundle).isEqualTo(mediaItem); + } } From 7ebab0e1823eeda06c6162da40b330dcc74187b5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 14 Dec 2022 10:52:33 +0000 Subject: [PATCH 066/141] Fix some release notes typos PiperOrigin-RevId: 495262344 (cherry picked from commit c9e87f050303a78e39aa0c96eab48e30714f3351) --- RELEASENOTES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4daafc12365..91e690245ef 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -298,15 +298,15 @@ This release corresponds to the * Query the platform (API 29+) or assume the audio encoding channel count for audio passthrough when the format audio channel count is unset, which occurs with HLS chunkless preparation - ([10204](https://github.com/google/ExoPlayer/issues/10204)). + ([#10204](https://github.com/google/ExoPlayer/issues/10204)). * Configure `AudioTrack` with channel mask `AudioFormat.CHANNEL_OUT_7POINT1POINT4` if the decoder outputs 12 channel PCM audio - ([#10322](#https://github.com/google/ExoPlayer/pull/10322). + ([#10322](#https://github.com/google/ExoPlayer/pull/10322)). * DRM * Ensure the DRM session is always correctly updated when seeking immediately after a format change - ([10274](https://github.com/google/ExoPlayer/issues/10274)). + ([#10274](https://github.com/google/ExoPlayer/issues/10274)). * Text: * Change `Player.getCurrentCues()` to return `CueGroup` instead of `List`. From 8e8abdaead22e55a6474f974badce8f083889b63 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 14 Dec 2022 12:42:43 +0000 Subject: [PATCH 067/141] Clear one-off events from state as soon as they are triggered. This ensures they are not accidentally triggered again when the state is rebuilt with a buildUpon method. PiperOrigin-RevId: 495280711 (cherry picked from commit a1231348926b4a88a2a8cb059204c083e304f23f) --- .../media3/common/SimpleBasePlayer.java | 15 +++- .../media3/common/SimpleBasePlayerTest.java | 76 +++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index 0d6f24d98e9..c32260fc23d 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -2888,6 +2888,15 @@ private void updateStateAndInformListeners(State newState) { // Assign new state immediately such that all getters return the right values, but use a // snapshot of the previous and new state so that listener invocations are triggered correctly. this.state = newState; + if (newState.hasPositionDiscontinuity || newState.newlyRenderedFirstFrame) { + // Clear one-time events to avoid signalling them again later. + this.state = + this.state + .buildUpon() + .clearPositionDiscontinuity() + .setNewlyRenderedFirstFrame(false) + .build(); + } boolean playWhenReadyChanged = previousState.playWhenReady != newState.playWhenReady; boolean playbackStateChanged = previousState.playbackState != newState.playbackState; @@ -2914,7 +2923,7 @@ private void updateStateAndInformListeners(State newState) { PositionInfo positionInfo = getPositionInfo( newState, - /* useDiscontinuityPosition= */ state.hasPositionDiscontinuity, + /* useDiscontinuityPosition= */ newState.hasPositionDiscontinuity, window, period); listeners.queueEvent( @@ -2928,9 +2937,9 @@ private void updateStateAndInformListeners(State newState) { if (mediaItemTransitionReason != C.INDEX_UNSET) { @Nullable MediaItem mediaItem = - state.timeline.isEmpty() + newState.timeline.isEmpty() ? null - : state.playlist.get(state.currentMediaItemIndex).mediaItem; + : newState.playlist.get(state.currentMediaItemIndex).mediaItem; listeners.queueEvent( Player.EVENT_MEDIA_ITEM_TRANSITION, listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason)); diff --git a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java index 498fb600683..b6b65b62c8a 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -1697,6 +1697,82 @@ protected State getState() { verify(listener, never()).onMediaItemTransition(any(), anyInt()); } + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void invalidateStateAndOtherOperation_withDiscontinuity_reportsDiscontinuityOnlyOnce() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 0).build())) + .setPositionDiscontinuity( + Player.DISCONTINUITY_REASON_INTERNAL, /* discontinuityPositionMs= */ 2000) + .build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handlePrepare() { + // We just care about the placeholder state, so return an unfulfilled future. + return SettableFuture.create(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.invalidateState(); + player.prepare(); + + // Assert listener calls (in particular getting only a single discontinuity). + verify(listener) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_INTERNAL)); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); + verify(listener).onPlaybackStateChanged(Player.STATE_BUFFERING); + verify(listener).onPlayerStateChanged(/* playWhenReady= */ false, Player.STATE_BUFFERING); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void + invalidateStateAndOtherOperation_withRenderedFirstFrame_reportsRenderedFirstFrameOnlyOnce() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 0).build())) + .setNewlyRenderedFirstFrame(true) + .build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handlePrepare() { + // We just care about the placeholder state, so return an unfulfilled future. + return SettableFuture.create(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.invalidateState(); + player.prepare(); + + // Assert listener calls (in particular getting only a single rendered first frame). + verify(listener).onRenderedFirstFrame(); + verify(listener).onPlaybackStateChanged(Player.STATE_BUFFERING); + verify(listener).onPlayerStateChanged(/* playWhenReady= */ false, Player.STATE_BUFFERING); + verifyNoMoreInteractions(listener); + } + @Test public void invalidateState_duringAsyncMethodHandling_isIgnored() { State state1 = From b1e4ac446fceddf7da371196d7f265bfb73ea5f3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 14 Dec 2022 18:30:49 +0000 Subject: [PATCH 068/141] Allow unset index and position values + remove period index This simplifies some position tracking needs for an app implementing SimpleBasePlayer. - The period index can always be derived from the media item index and the position. So there is no need to set it separately. - The media item index can be left unset in the State in case the app doesn't care about the value or wants to set it the default start index (e.g. while the playlist is still empty where UNSET is different from zero). - Similarly, we should allow to set the content position (and buffered position) to C.TIME_UNSET to let the app ignore it or indicate the default position explictly. PiperOrigin-RevId: 495352633 (cherry picked from commit 545fa5946268908562370c29bd3e3e1598c28453) --- .../media3/common/SimpleBasePlayer.java | 216 ++++++++++-------- .../media3/common/SimpleBasePlayerTest.java | 208 ++++++++++++++--- 2 files changed, 303 insertions(+), 121 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index c32260fc23d..aa677d85c8a 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -19,6 +19,7 @@ import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull; +import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.common.util.Util.usToMs; import static java.lang.Math.max; @@ -127,12 +128,11 @@ public static final class Builder { private Timeline timeline; private MediaMetadata playlistMetadata; private int currentMediaItemIndex; - private int currentPeriodIndex; private int currentAdGroupIndex; private int currentAdIndexInAdGroup; - private long contentPositionMs; + @Nullable private Long contentPositionMs; private PositionSupplier contentPositionMsSupplier; - private long adPositionMs; + @Nullable private Long adPositionMs; private PositionSupplier adPositionMsSupplier; private PositionSupplier contentBufferedPositionMsSupplier; private PositionSupplier adBufferedPositionMsSupplier; @@ -170,15 +170,14 @@ public Builder() { playlist = ImmutableList.of(); timeline = Timeline.EMPTY; playlistMetadata = MediaMetadata.EMPTY; - currentMediaItemIndex = 0; - currentPeriodIndex = C.INDEX_UNSET; + currentMediaItemIndex = C.INDEX_UNSET; currentAdGroupIndex = C.INDEX_UNSET; currentAdIndexInAdGroup = C.INDEX_UNSET; - contentPositionMs = C.TIME_UNSET; - contentPositionMsSupplier = PositionSupplier.ZERO; - adPositionMs = C.TIME_UNSET; + contentPositionMs = null; + contentPositionMsSupplier = PositionSupplier.getConstant(C.TIME_UNSET); + adPositionMs = null; adPositionMsSupplier = PositionSupplier.ZERO; - contentBufferedPositionMsSupplier = PositionSupplier.ZERO; + contentBufferedPositionMsSupplier = PositionSupplier.getConstant(C.TIME_UNSET); adBufferedPositionMsSupplier = PositionSupplier.ZERO; totalBufferedDurationMsSupplier = PositionSupplier.ZERO; hasPositionDiscontinuity = false; @@ -215,12 +214,11 @@ private Builder(State state) { this.timeline = state.timeline; this.playlistMetadata = state.playlistMetadata; this.currentMediaItemIndex = state.currentMediaItemIndex; - this.currentPeriodIndex = state.currentPeriodIndex; this.currentAdGroupIndex = state.currentAdGroupIndex; this.currentAdIndexInAdGroup = state.currentAdIndexInAdGroup; - this.contentPositionMs = C.TIME_UNSET; + this.contentPositionMs = null; this.contentPositionMsSupplier = state.contentPositionMsSupplier; - this.adPositionMs = C.TIME_UNSET; + this.adPositionMs = null; this.adPositionMsSupplier = state.adPositionMsSupplier; this.contentBufferedPositionMsSupplier = state.contentBufferedPositionMsSupplier; this.adBufferedPositionMsSupplier = state.adBufferedPositionMsSupplier; @@ -574,7 +572,8 @@ public Builder setPlaylistMetadata(MediaMetadata playlistMetadata) { *

      The media item index must be less than the number of {@linkplain #setPlaylist media * items in the playlist}, if set. * - * @param currentMediaItemIndex The current media item index. + * @param currentMediaItemIndex The current media item index, or {@link C#INDEX_UNSET} to + * assume the default first item in the playlist. * @return This builder. */ @CanIgnoreReturnValue @@ -583,26 +582,6 @@ public Builder setCurrentMediaItemIndex(int currentMediaItemIndex) { return this; } - /** - * Sets the current period index, or {@link C#INDEX_UNSET} to assume the first period of the - * current media item is played. - * - *

      The period index must be less than the total number of {@linkplain - * MediaItemData.Builder#setPeriods periods} in the media item, if set, and the period at the - * specified index must be part of the {@linkplain #setCurrentMediaItemIndex current media - * item}. - * - * @param currentPeriodIndex The current period index, or {@link C#INDEX_UNSET} to assume the - * first period of the current media item is played. - * @return This builder. - */ - @CanIgnoreReturnValue - public Builder setCurrentPeriodIndex(int currentPeriodIndex) { - checkArgument(currentPeriodIndex == C.INDEX_UNSET || currentPeriodIndex >= 0); - this.currentPeriodIndex = currentPeriodIndex; - return this; - } - /** * Sets the current ad indices, or {@link C#INDEX_UNSET} if no ad is playing. * @@ -632,7 +611,11 @@ public Builder setCurrentAd(int adGroupIndex, int adIndexInAdGroup) { *

      This position will be converted to an advancing {@link PositionSupplier} if the overall * state indicates an advancing playback position. * - * @param positionMs The current content playback position in milliseconds. + *

      This method overrides any other {@link PositionSupplier} set via {@link + * #setContentPositionMs(PositionSupplier)}. + * + * @param positionMs The current content playback position in milliseconds, or {@link + * C#TIME_UNSET} to indicate the default start position. * @return This builder. */ @CanIgnoreReturnValue @@ -648,24 +631,30 @@ public Builder setContentPositionMs(long positionMs) { *

      The supplier is expected to return the updated position on every call if the playback is * advancing, for example by using {@link PositionSupplier#getExtrapolating}. * + *

      This method overrides any other position set via {@link #setContentPositionMs(long)}. + * * @param contentPositionMsSupplier The {@link PositionSupplier} for the current content - * playback position in milliseconds. + * playback position in milliseconds, or {@link C#TIME_UNSET} to indicate the default + * start position. * @return This builder. */ @CanIgnoreReturnValue public Builder setContentPositionMs(PositionSupplier contentPositionMsSupplier) { - this.contentPositionMs = C.TIME_UNSET; + this.contentPositionMs = null; this.contentPositionMsSupplier = contentPositionMsSupplier; return this; } /** - * Sets the current ad playback position in milliseconds. The * value is unused if no ad is + * Sets the current ad playback position in milliseconds. The value is unused if no ad is * playing. * *

      This position will be converted to an advancing {@link PositionSupplier} if the overall * state indicates an advancing ad playback position. * + *

      This method overrides any other {@link PositionSupplier} set via {@link + * #setAdPositionMs(PositionSupplier)}. + * * @param positionMs The current ad playback position in milliseconds. * @return This builder. */ @@ -682,13 +671,15 @@ public Builder setAdPositionMs(long positionMs) { *

      The supplier is expected to return the updated position on every call if the playback is * advancing, for example by using {@link PositionSupplier#getExtrapolating}. * + *

      This method overrides any other position set via {@link #setAdPositionMs(long)}. + * * @param adPositionMsSupplier The {@link PositionSupplier} for the current ad playback * position in milliseconds. The value is unused if no ad is playing. * @return This builder. */ @CanIgnoreReturnValue public Builder setAdPositionMs(PositionSupplier adPositionMsSupplier) { - this.adPositionMs = C.TIME_UNSET; + this.adPositionMs = null; this.adPositionMsSupplier = adPositionMsSupplier; return this; } @@ -698,7 +689,8 @@ public Builder setAdPositionMs(PositionSupplier adPositionMsSupplier) { * playing content is buffered, in milliseconds. * * @param contentBufferedPositionMsSupplier The {@link PositionSupplier} for the estimated - * position up to which the currently playing content is buffered, in milliseconds. + * position up to which the currently playing content is buffered, in milliseconds, or + * {@link C#TIME_UNSET} to indicate the default start position. * @return This builder. */ @CanIgnoreReturnValue @@ -838,18 +830,19 @@ public State build() { public final Timeline timeline; /** The playlist {@link MediaMetadata}. */ public final MediaMetadata playlistMetadata; - /** The current media item index. */ - public final int currentMediaItemIndex; /** - * The current period index, or {@link C#INDEX_UNSET} to assume the first period of the current - * media item is played. + * The current media item index, or {@link C#INDEX_UNSET} to assume the default first item of + * the playlist is played. */ - public final int currentPeriodIndex; + public final int currentMediaItemIndex; /** The current ad group index, or {@link C#INDEX_UNSET} if no ad is playing. */ public final int currentAdGroupIndex; /** The current ad index in the ad group, or {@link C#INDEX_UNSET} if no ad is playing. */ public final int currentAdIndexInAdGroup; - /** The {@link PositionSupplier} for the current content playback position in milliseconds. */ + /** + * The {@link PositionSupplier} for the current content playback position in milliseconds, or + * {@link C#TIME_UNSET} to indicate the default start position. + */ public final PositionSupplier contentPositionMsSupplier; /** * The {@link PositionSupplier} for the current ad playback position in milliseconds. The value @@ -858,7 +851,8 @@ public State build() { public final PositionSupplier adPositionMsSupplier; /** * The {@link PositionSupplier} for the estimated position up to which the currently playing - * content is buffered, in milliseconds. + * content is buffered, in milliseconds, or {@link C#TIME_UNSET} to indicate the default start + * position. */ public final PositionSupplier contentBufferedPositionMsSupplier; /** @@ -887,22 +881,27 @@ private State(Builder builder) { checkArgument( builder.playbackState == Player.STATE_IDLE || builder.playbackState == Player.STATE_ENDED); + checkArgument( + builder.currentAdGroupIndex == C.INDEX_UNSET + && builder.currentAdIndexInAdGroup == C.INDEX_UNSET); } else { - checkArgument(builder.currentMediaItemIndex < builder.timeline.getWindowCount()); - if (builder.currentPeriodIndex != C.INDEX_UNSET) { - checkArgument(builder.currentPeriodIndex < builder.timeline.getPeriodCount()); - checkArgument( - builder.timeline.getPeriod(builder.currentPeriodIndex, new Timeline.Period()) - .windowIndex - == builder.currentMediaItemIndex); + int mediaItemIndex = builder.currentMediaItemIndex; + if (mediaItemIndex == C.INDEX_UNSET) { + mediaItemIndex = 0; // TODO: Use shuffle order to find first index. + } else { + checkArgument(builder.currentMediaItemIndex < builder.timeline.getWindowCount()); } if (builder.currentAdGroupIndex != C.INDEX_UNSET) { + Timeline.Period period = new Timeline.Period(); + Timeline.Window window = new Timeline.Window(); + long contentPositionMs = + builder.contentPositionMs != null + ? builder.contentPositionMs + : builder.contentPositionMsSupplier.get(); int periodIndex = - builder.currentPeriodIndex != C.INDEX_UNSET - ? builder.currentPeriodIndex - : builder.timeline.getWindow(builder.currentMediaItemIndex, new Timeline.Window()) - .firstPeriodIndex; - Timeline.Period period = builder.timeline.getPeriod(periodIndex, new Timeline.Period()); + getPeriodIndexFromWindowPosition( + builder.timeline, mediaItemIndex, contentPositionMs, window, period); + builder.timeline.getPeriod(periodIndex, period); checkArgument(builder.currentAdGroupIndex < period.getAdGroupCount()); int adCountInGroup = period.getAdCountInAdGroup(builder.currentAdGroupIndex); if (adCountInGroup != C.LENGTH_UNSET) { @@ -918,11 +917,12 @@ private State(Builder builder) { checkArgument(!builder.isLoading); } PositionSupplier contentPositionMsSupplier = builder.contentPositionMsSupplier; - if (builder.contentPositionMs != C.TIME_UNSET) { + if (builder.contentPositionMs != null) { if (builder.currentAdGroupIndex == C.INDEX_UNSET && builder.playWhenReady && builder.playbackState == Player.STATE_READY - && builder.playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE) { + && builder.playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE + && builder.contentPositionMs != C.TIME_UNSET) { contentPositionMsSupplier = PositionSupplier.getExtrapolating( builder.contentPositionMs, builder.playbackParameters.speed); @@ -931,7 +931,7 @@ private State(Builder builder) { } } PositionSupplier adPositionMsSupplier = builder.adPositionMsSupplier; - if (builder.adPositionMs != C.TIME_UNSET) { + if (builder.adPositionMs != null) { if (builder.currentAdGroupIndex != C.INDEX_UNSET && builder.playWhenReady && builder.playbackState == Player.STATE_READY @@ -970,7 +970,6 @@ private State(Builder builder) { this.timeline = builder.timeline; this.playlistMetadata = builder.playlistMetadata; this.currentMediaItemIndex = builder.currentMediaItemIndex; - this.currentPeriodIndex = builder.currentPeriodIndex; this.currentAdGroupIndex = builder.currentAdGroupIndex; this.currentAdIndexInAdGroup = builder.currentAdIndexInAdGroup; this.contentPositionMsSupplier = contentPositionMsSupplier; @@ -1024,7 +1023,6 @@ public boolean equals(@Nullable Object o) { && playlist.equals(state.playlist) && playlistMetadata.equals(state.playlistMetadata) && currentMediaItemIndex == state.currentMediaItemIndex - && currentPeriodIndex == state.currentPeriodIndex && currentAdGroupIndex == state.currentAdGroupIndex && currentAdIndexInAdGroup == state.currentAdIndexInAdGroup && contentPositionMsSupplier.equals(state.contentPositionMsSupplier) @@ -1068,7 +1066,6 @@ public int hashCode() { result = 31 * result + playlist.hashCode(); result = 31 * result + playlistMetadata.hashCode(); result = 31 * result + currentMediaItemIndex; - result = 31 * result + currentPeriodIndex; result = 31 * result + currentAdGroupIndex; result = 31 * result + currentAdIndexInAdGroup; result = 31 * result + contentPositionMsSupplier.hashCode(); @@ -2198,7 +2195,8 @@ public final void stop() { .buildUpon() .setPlaybackState(Player.STATE_IDLE) .setTotalBufferedDurationMs(PositionSupplier.ZERO) - .setContentBufferedPositionMs(state.contentPositionMsSupplier) + .setContentBufferedPositionMs( + PositionSupplier.getConstant(getContentPositionMsInternal(state))) .setAdBufferedPositionMs(state.adPositionMsSupplier) .setIsLoading(false) .build()); @@ -2230,7 +2228,8 @@ public final void release() { .buildUpon() .setPlaybackState(Player.STATE_IDLE) .setTotalBufferedDurationMs(PositionSupplier.ZERO) - .setContentBufferedPositionMs(state.contentPositionMsSupplier) + .setContentBufferedPositionMs( + PositionSupplier.getConstant(getContentPositionMsInternal(state))) .setAdBufferedPositionMs(state.adPositionMsSupplier) .setIsLoading(false) .build(); @@ -2297,13 +2296,13 @@ public final Timeline getCurrentTimeline() { @Override public final int getCurrentPeriodIndex() { verifyApplicationThreadAndInitState(); - return getCurrentPeriodIndexInternal(state, window); + return getCurrentPeriodIndexInternal(state, window, period); } @Override public final int getCurrentMediaItemIndex() { verifyApplicationThreadAndInitState(); - return state.currentMediaItemIndex; + return getCurrentMediaItemIndexInternal(state); } @Override @@ -2359,14 +2358,13 @@ public final int getCurrentAdIndexInAdGroup() { @Override public final long getContentPosition() { verifyApplicationThreadAndInitState(); - return state.contentPositionMsSupplier.get(); + return getContentPositionMsInternal(state); } @Override public final long getContentBufferedPosition() { verifyApplicationThreadAndInitState(); - return max( - state.contentBufferedPositionMsSupplier.get(), state.contentPositionMsSupplier.get()); + return max(getContentBufferedPositionMsInternal(state), getContentPositionMsInternal(state)); } @Override @@ -2939,7 +2937,7 @@ private void updateStateAndInformListeners(State newState) { MediaItem mediaItem = newState.timeline.isEmpty() ? null - : newState.playlist.get(state.currentMediaItemIndex).mediaItem; + : newState.playlist.get(getCurrentMediaItemIndexInternal(newState)).mediaItem; listeners.queueEvent( Player.EVENT_MEDIA_ITEM_TRANSITION, listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason)); @@ -3159,23 +3157,59 @@ private static boolean isPlaying(State state) { private static Tracks getCurrentTracksInternal(State state) { return state.playlist.isEmpty() ? Tracks.EMPTY - : state.playlist.get(state.currentMediaItemIndex).tracks; + : state.playlist.get(getCurrentMediaItemIndexInternal(state)).tracks; } private static MediaMetadata getMediaMetadataInternal(State state) { return state.playlist.isEmpty() ? MediaMetadata.EMPTY - : state.playlist.get(state.currentMediaItemIndex).combinedMediaMetadata; + : state.playlist.get(getCurrentMediaItemIndexInternal(state)).combinedMediaMetadata; + } + + private static int getCurrentMediaItemIndexInternal(State state) { + if (state.currentMediaItemIndex != C.INDEX_UNSET) { + return state.currentMediaItemIndex; + } + return 0; // TODO: Use shuffle order to get first item if playlist is not empty. + } + + private static long getContentPositionMsInternal(State state) { + return getPositionOrDefaultInMediaItem(state.contentPositionMsSupplier.get(), state); + } + + private static long getContentBufferedPositionMsInternal(State state) { + return getPositionOrDefaultInMediaItem(state.contentBufferedPositionMsSupplier.get(), state); } - private static int getCurrentPeriodIndexInternal(State state, Timeline.Window window) { - if (state.currentPeriodIndex != C.INDEX_UNSET) { - return state.currentPeriodIndex; + private static long getPositionOrDefaultInMediaItem(long positionMs, State state) { + if (positionMs != C.TIME_UNSET) { + return positionMs; + } + if (state.playlist.isEmpty()) { + return 0; } + return usToMs(state.playlist.get(getCurrentMediaItemIndexInternal(state)).defaultPositionUs); + } + + private static int getCurrentPeriodIndexInternal( + State state, Timeline.Window window, Timeline.Period period) { + int currentMediaItemIndex = getCurrentMediaItemIndexInternal(state); if (state.timeline.isEmpty()) { - return state.currentMediaItemIndex; + return currentMediaItemIndex; } - return state.timeline.getWindow(state.currentMediaItemIndex, window).firstPeriodIndex; + return getPeriodIndexFromWindowPosition( + state.timeline, currentMediaItemIndex, getContentPositionMsInternal(state), window, period); + } + + private static int getPeriodIndexFromWindowPosition( + Timeline timeline, + int windowIndex, + long windowPositionMs, + Timeline.Window window, + Timeline.Period period) { + Object periodUid = + timeline.getPeriodPositionUs(window, period, windowIndex, msToUs(windowPositionMs)).first; + return timeline.getIndexOfPeriod(periodUid); } private static @Player.TimelineChangeReason int getTimelineChangeReason( @@ -3206,9 +3240,10 @@ private static int getPositionDiscontinuityReason( return Player.DISCONTINUITY_REASON_REMOVE; } Object previousPeriodUid = - previousState.timeline.getUidOfPeriod(getCurrentPeriodIndexInternal(previousState, window)); + previousState.timeline.getUidOfPeriod( + getCurrentPeriodIndexInternal(previousState, window, period)); Object newPeriodUid = - newState.timeline.getUidOfPeriod(getCurrentPeriodIndexInternal(newState, window)); + newState.timeline.getUidOfPeriod(getCurrentPeriodIndexInternal(newState, window, period)); if (!newPeriodUid.equals(previousPeriodUid) || previousState.currentAdGroupIndex != newState.currentAdGroupIndex || previousState.currentAdIndexInAdGroup != newState.currentAdIndexInAdGroup) { @@ -3244,7 +3279,7 @@ private static long getCurrentPeriodOrAdPositionMs( State state, Object currentPeriodUid, Timeline.Period period) { return state.currentAdGroupIndex != C.INDEX_UNSET ? state.adPositionMsSupplier.get() - : state.contentPositionMsSupplier.get() + : getContentPositionMsInternal(state) - state.timeline.getPeriodByUid(currentPeriodUid, period).getPositionInWindowMs(); } @@ -3265,11 +3300,11 @@ private static PositionInfo getPositionInfo( Timeline.Period period) { @Nullable Object windowUid = null; @Nullable Object periodUid = null; - int mediaItemIndex = state.currentMediaItemIndex; + int mediaItemIndex = getCurrentMediaItemIndexInternal(state); int periodIndex = C.INDEX_UNSET; @Nullable MediaItem mediaItem = null; if (!state.timeline.isEmpty()) { - periodIndex = getCurrentPeriodIndexInternal(state, window); + periodIndex = getCurrentPeriodIndexInternal(state, window, period); periodUid = state.timeline.getPeriod(periodIndex, period, /* setIds= */ true).uid; windowUid = state.timeline.getWindow(mediaItemIndex, window).uid; mediaItem = window.mediaItem; @@ -3281,9 +3316,9 @@ private static PositionInfo getPositionInfo( contentPositionMs = state.currentAdGroupIndex == C.INDEX_UNSET ? positionMs - : state.contentPositionMsSupplier.get(); + : getContentPositionMsInternal(state); } else { - contentPositionMs = state.contentPositionMsSupplier.get(); + contentPositionMs = getContentPositionMsInternal(state); positionMs = state.currentAdGroupIndex != C.INDEX_UNSET ? state.adPositionMsSupplier.get() @@ -3314,8 +3349,10 @@ private static int getMediaItemTransitionReason( return MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED; } Object previousWindowUid = - previousState.timeline.getWindow(previousState.currentMediaItemIndex, window).uid; - Object newWindowUid = newState.timeline.getWindow(newState.currentMediaItemIndex, window).uid; + previousState.timeline.getWindow(getCurrentMediaItemIndexInternal(previousState), window) + .uid; + Object newWindowUid = + newState.timeline.getWindow(getCurrentMediaItemIndexInternal(newState), window).uid; if (!previousWindowUid.equals(newWindowUid)) { if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) { return MEDIA_ITEM_TRANSITION_REASON_AUTO; @@ -3328,8 +3365,7 @@ private static int getMediaItemTransitionReason( // Only mark changes within the current item as a transition if we are repeating automatically // or via a seek to next/previous. if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION - && previousState.contentPositionMsSupplier.get() - > newState.contentPositionMsSupplier.get()) { + && getContentPositionMsInternal(previousState) > getContentPositionMsInternal(newState)) { return MEDIA_ITEM_TRANSITION_REASON_REPEAT; } if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK diff --git a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java index b6b65b62c8a..2b2d6fb4ac1 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -128,7 +128,6 @@ public void stateBuildUpon_build_isEqual() { .build())) .setPlaylistMetadata(new MediaMetadata.Builder().setArtist("artist").build()) .setCurrentMediaItemIndex(1) - .setCurrentPeriodIndex(1) .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2) .setContentPositionMs(() -> 456) .setAdPositionMs(() -> 6678) @@ -275,7 +274,6 @@ public void stateBuilderBuild_setsCorrectValues() { .setPlaylist(playlist) .setPlaylistMetadata(playlistMetadata) .setCurrentMediaItemIndex(1) - .setCurrentPeriodIndex(1) .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2) .setContentPositionMs(contentPositionSupplier) .setAdPositionMs(adPositionSupplier) @@ -315,7 +313,6 @@ public void stateBuilderBuild_setsCorrectValues() { assertThat(state.playlist).isEqualTo(playlist); assertThat(state.playlistMetadata).isEqualTo(playlistMetadata); assertThat(state.currentMediaItemIndex).isEqualTo(1); - assertThat(state.currentPeriodIndex).isEqualTo(1); assertThat(state.currentAdGroupIndex).isEqualTo(1); assertThat(state.currentAdIndexInAdGroup).isEqualTo(2); assertThat(state.contentPositionMsSupplier).isEqualTo(contentPositionSupplier); @@ -362,37 +359,32 @@ public void stateBuilderBuild_idleStateWithIsLoading_throwsException() { } @Test - public void stateBuilderBuild_currentWindowIndexExceedsPlaylistLength_throwsException() { - assertThrows( - IllegalArgumentException.class, - () -> - new SimpleBasePlayer.State.Builder() - .setPlaylist( - ImmutableList.of( - new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), - new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) - .build())) - .setCurrentMediaItemIndex(2) - .build()); + public void stateBuilderBuild_currentMediaItemIndexUnset_doesNotThrow() { + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .setCurrentMediaItemIndex(C.INDEX_UNSET) + .build(); + + assertThat(state.currentMediaItemIndex).isEqualTo(C.INDEX_UNSET); } @Test - public void stateBuilderBuild_currentPeriodIndexExceedsPlaylistLength_throwsException() { - assertThrows( - IllegalArgumentException.class, - () -> - new SimpleBasePlayer.State.Builder() - .setPlaylist( - ImmutableList.of( - new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), - new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) - .build())) - .setCurrentPeriodIndex(2) - .build()); + public void stateBuilderBuild_currentMediaItemIndexSetForEmptyPlaylist_doesNotThrow() { + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist(ImmutableList.of()) + .setCurrentMediaItemIndex(20) + .build(); + + assertThat(state.currentMediaItemIndex).isEqualTo(20); } @Test - public void stateBuilderBuild_currentPeriodIndexInOtherMediaItem_throwsException() { + public void stateBuilderBuild_currentMediaItemIndexExceedsPlaylistLength_throwsException() { assertThrows( IllegalArgumentException.class, () -> @@ -402,8 +394,7 @@ public void stateBuilderBuild_currentPeriodIndexInOtherMediaItem_throwsException new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()) .build())) - .setCurrentMediaItemIndex(0) - .setCurrentPeriodIndex(1) + .setCurrentMediaItemIndex(2) .build()); } @@ -453,6 +444,16 @@ public void stateBuilderBuild_currentAdIndexExceedsAdCountInAdGroup_throwsExcept .build()); } + @Test + public void stateBuilderBuild_setAdAndEmptyPlaylist_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3) + .build()); + } + @Test public void stateBuilderBuild_playerErrorInNonIdleState_throwsException() { assertThrows( @@ -534,6 +535,27 @@ public void stateBuilderBuild_returnsAdvancingContentPositionWhenPlaying() { assertThat(position2).isEqualTo(8000); } + @Test + public void stateBuilderBuild_withUnsetPositionAndPlaying_returnsConstantContentPosition() { + SystemClock.setCurrentTimeMillis(10000); + + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .setContentPositionMs(C.TIME_UNSET) + .setPlayWhenReady(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setPlaybackState(Player.STATE_READY) + .build(); + long position1 = state.contentPositionMsSupplier.get(); + SystemClock.setCurrentTimeMillis(12000); + long position2 = state.contentPositionMsSupplier.get(); + + assertThat(position1).isEqualTo(C.TIME_UNSET); + assertThat(position2).isEqualTo(C.TIME_UNSET); + } + @Test public void stateBuilderBuild_returnsConstantContentPositionWhenNotPlaying() { SystemClock.setCurrentTimeMillis(10000); @@ -865,7 +887,6 @@ public void getterMethods_noOtherMethodCalls_returnCurrentState() { .setPlaylist(playlist) .setPlaylistMetadata(playlistMetadata) .setCurrentMediaItemIndex(1) - .setCurrentPeriodIndex(1) .setContentPositionMs(contentPositionSupplier) .setContentBufferedPositionMs(contentBufferedPositionSupplier) .setTotalBufferedDurationMs(totalBufferedPositionSupplier) @@ -1049,6 +1070,131 @@ protected State getState() { assertThat(player.getCurrentMediaItemIndex()).isEqualTo(4); } + @Test + public void getCurrentMediaItemIndex_withUnsetIndexInState_returnsDefaultIndex() { + State state = new State.Builder().setCurrentMediaItemIndex(C.INDEX_UNSET).build(); + + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + }; + + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + } + + @Test + public void getCurrentPeriodIndex_withUnsetIndexInState_returnsPeriodForCurrentPosition() { + State state = + new State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 0).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ "period0") + .setDurationUs(60_000_000) + .build(), + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ "period1") + .setDurationUs(5_000_000) + .build(), + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ "period2") + .setDurationUs(5_000_000) + .build())) + .setPositionInFirstPeriodUs(50_000_000) + .build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(12_000) + .build(); + + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + }; + + assertThat(player.getCurrentPeriodIndex()).isEqualTo(2); + } + + @Test + public void getCurrentPosition_withUnsetPositionInState_returnsDefaultPosition() { + State state = + new State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 0) + .setDefaultPositionUs(5_000_000) + .build())) + .setContentPositionMs(C.TIME_UNSET) + .build(); + + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + }; + + assertThat(player.getCurrentPosition()).isEqualTo(5000); + } + + @Test + public void getBufferedPosition_withUnsetBufferedPositionInState_returnsDefaultPosition() { + State state = + new State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 0) + .setDefaultPositionUs(5_000_000) + .build())) + .setContentBufferedPositionMs( + SimpleBasePlayer.PositionSupplier.getConstant(C.TIME_UNSET)) + .build(); + + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + }; + + assertThat(player.getBufferedPosition()).isEqualTo(5000); + } + + @Test + public void + getBufferedPosition_withUnsetBufferedPositionAndPositionInState_returnsDefaultPosition() { + State state = + new State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 0) + .setDefaultPositionUs(5_000_000) + .build())) + .setContentPositionMs(C.TIME_UNSET) + .setContentBufferedPositionMs( + SimpleBasePlayer.PositionSupplier.getConstant(C.TIME_UNSET)) + .build(); + + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + }; + + assertThat(player.getBufferedPosition()).isEqualTo(5000); + } + @SuppressWarnings("deprecation") // Verifying deprecated listener call. @Test public void invalidateState_updatesStateAndInformsListeners() throws Exception { From 4f8d71e87248570cf3cc24b3d13cf58523772a3c Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 15 Dec 2022 17:23:27 +0000 Subject: [PATCH 069/141] Remove parameters with `null` values from bundle in `MediaMetadata` Improves the time taken to construct `playerInfo` from its bundle from ~450 ms to ~400 ms. Each `MediaItem` inside `Timeline.Window` contains `MediaMetadata` and hence is a good candidate for bundling optimisations. There already exists a test to check all parameters for null values when unset. PiperOrigin-RevId: 495614719 (cherry picked from commit d11e0a35c114225261a8fe472b0b93d4a8a6b727) --- .../androidx/media3/common/MediaMetadata.java | 61 ++++++++++++++----- .../media3/common/MediaMetadataTest.java | 22 +++++-- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java index 470ed7a71c1..9f6b0f2035f 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java @@ -1183,22 +1183,51 @@ public int hashCode() { @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putCharSequence(keyForField(FIELD_TITLE), title); - bundle.putCharSequence(keyForField(FIELD_ARTIST), artist); - bundle.putCharSequence(keyForField(FIELD_ALBUM_TITLE), albumTitle); - bundle.putCharSequence(keyForField(FIELD_ALBUM_ARTIST), albumArtist); - bundle.putCharSequence(keyForField(FIELD_DISPLAY_TITLE), displayTitle); - bundle.putCharSequence(keyForField(FIELD_SUBTITLE), subtitle); - bundle.putCharSequence(keyForField(FIELD_DESCRIPTION), description); - bundle.putByteArray(keyForField(FIELD_ARTWORK_DATA), artworkData); - bundle.putParcelable(keyForField(FIELD_ARTWORK_URI), artworkUri); - bundle.putCharSequence(keyForField(FIELD_WRITER), writer); - bundle.putCharSequence(keyForField(FIELD_COMPOSER), composer); - bundle.putCharSequence(keyForField(FIELD_CONDUCTOR), conductor); - bundle.putCharSequence(keyForField(FIELD_GENRE), genre); - bundle.putCharSequence(keyForField(FIELD_COMPILATION), compilation); - bundle.putCharSequence(keyForField(FIELD_STATION), station); - + if (title != null) { + bundle.putCharSequence(keyForField(FIELD_TITLE), title); + } + if (artist != null) { + bundle.putCharSequence(keyForField(FIELD_ARTIST), artist); + } + if (albumTitle != null) { + bundle.putCharSequence(keyForField(FIELD_ALBUM_TITLE), albumTitle); + } + if (albumArtist != null) { + bundle.putCharSequence(keyForField(FIELD_ALBUM_ARTIST), albumArtist); + } + if (displayTitle != null) { + bundle.putCharSequence(keyForField(FIELD_DISPLAY_TITLE), displayTitle); + } + if (subtitle != null) { + bundle.putCharSequence(keyForField(FIELD_SUBTITLE), subtitle); + } + if (description != null) { + bundle.putCharSequence(keyForField(FIELD_DESCRIPTION), description); + } + if (artworkData != null) { + bundle.putByteArray(keyForField(FIELD_ARTWORK_DATA), artworkData); + } + if (artworkUri != null) { + bundle.putParcelable(keyForField(FIELD_ARTWORK_URI), artworkUri); + } + if (writer != null) { + bundle.putCharSequence(keyForField(FIELD_WRITER), writer); + } + if (composer != null) { + bundle.putCharSequence(keyForField(FIELD_COMPOSER), composer); + } + if (conductor != null) { + bundle.putCharSequence(keyForField(FIELD_CONDUCTOR), conductor); + } + if (genre != null) { + bundle.putCharSequence(keyForField(FIELD_GENRE), genre); + } + if (compilation != null) { + bundle.putCharSequence(keyForField(FIELD_COMPILATION), compilation); + } + if (station != null) { + bundle.putCharSequence(keyForField(FIELD_STATION), station); + } if (userRating != null) { bundle.putBundle(keyForField(FIELD_USER_RATING), userRating.toBundle()); } diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java index f3a7418fc79..bde20bc6037 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java @@ -107,13 +107,27 @@ public void populate_populatesEveryField() { } @Test - public void roundTripViaBundle_yieldsEqualInstance() { + public void createMinimalMediaMetadata_roundTripViaBundle_yieldsEqualInstance() { + MediaMetadata mediaMetadata = new MediaMetadata.Builder().build(); + + MediaMetadata mediaMetadataFromBundle = + MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle()); + + assertThat(mediaMetadataFromBundle).isEqualTo(mediaMetadata); + // Extras is not implemented in MediaMetadata.equals(Object o). + assertThat(mediaMetadataFromBundle.extras).isNull(); + } + + @Test + public void createFullyPopulatedMediaMetadata_roundTripViaBundle_yieldsEqualInstance() { MediaMetadata mediaMetadata = getFullyPopulatedMediaMetadata(); - MediaMetadata fromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle()); - assertThat(fromBundle).isEqualTo(mediaMetadata); + MediaMetadata mediaMetadataFromBundle = + MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle()); + + assertThat(mediaMetadataFromBundle).isEqualTo(mediaMetadata); // Extras is not implemented in MediaMetadata.equals(Object o). - assertThat(fromBundle.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE); + assertThat(mediaMetadataFromBundle.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE); } @Test From 9817c46923cffbbdf459afcf553d80936fb5bf1b Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 15 Dec 2022 19:00:04 +0000 Subject: [PATCH 070/141] Use theme when loading drawables on API 21+ Issue: androidx/media#220 PiperOrigin-RevId: 495642588 (cherry picked from commit 22dfd4cb32fdb76ba10047f555c983490c27eb13) --- RELEASENOTES.md | 2 + .../androidx/media3/common/util/Util.java | 28 +++++++++ .../media3/ui/LegacyPlayerControlView.java | 16 +++-- .../androidx/media3/ui/PlayerControlView.java | 58 ++++++++++++------- .../java/androidx/media3/ui/PlayerView.java | 14 +++-- 5 files changed, 85 insertions(+), 33 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 91e690245ef..3b4ca8610d8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,8 @@ ([#10776](https://github.com/google/ExoPlayer/issues/10776)). * Add parameter to `BasePlayer.seekTo` to also indicate the command used for seeking. + * Use theme when loading drawables on API 21+ + ([#220](https://github.com/androidx/media/issues/220)). * Audio: * Use the compressed audio format bitrate to calculate the min buffer size for `AudioTrack` in direct playbacks (passthrough). diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index 30d94ef39e0..b1669556d2c 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -47,6 +47,7 @@ import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.graphics.Point; +import android.graphics.drawable.Drawable; import android.hardware.display.DisplayManager; import android.media.AudioFormat; import android.media.AudioManager; @@ -66,6 +67,8 @@ import android.view.Display; import android.view.SurfaceView; import android.view.WindowManager; +import androidx.annotation.DoNotInline; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.media3.common.C; @@ -2864,6 +2867,23 @@ public static long sum(long... summands) { return sum; } + /** + * Returns a {@link Drawable} for the given resource or throws a {@link + * Resources.NotFoundException} if not found. + * + * @param context The context to get the theme from starting with API 21. + * @param resources The resources to load the drawable from. + * @param drawableRes The drawable resource int. + * @return The loaded {@link Drawable}. + */ + @UnstableApi + public static Drawable getDrawable( + Context context, Resources resources, @DrawableRes int drawableRes) { + return SDK_INT >= 21 + ? Api21.getDrawable(context, resources, drawableRes) + : resources.getDrawable(drawableRes); + } + @Nullable private static String getSystemProperty(String name) { try { @@ -3100,4 +3120,12 @@ private static String maybeReplaceLegacyLanguageTags(String languageTag) { 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3 }; + + @RequiresApi(21) + private static final class Api21 { + @DoNotInline + public static Drawable getDrawable(Context context, Resources resources, @DrawableRes int res) { + return resources.getDrawable(res, context.getTheme()); + } + } } diff --git a/libraries/ui/src/main/java/androidx/media3/ui/LegacyPlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/LegacyPlayerControlView.java index 67197ade22b..b0be016e636 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/LegacyPlayerControlView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/LegacyPlayerControlView.java @@ -28,6 +28,7 @@ import static androidx.media3.common.Player.EVENT_REPEAT_MODE_CHANGED; import static androidx.media3.common.Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED; import static androidx.media3.common.Player.EVENT_TIMELINE_CHANGED; +import static androidx.media3.common.util.Util.getDrawable; import android.annotation.SuppressLint; import android.content.Context; @@ -498,11 +499,16 @@ public LegacyPlayerControlView( buttonAlphaDisabled = (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_disabled) / 100; - repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_legacy_controls_repeat_off); - repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_legacy_controls_repeat_one); - repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_legacy_controls_repeat_all); - shuffleOnButtonDrawable = resources.getDrawable(R.drawable.exo_legacy_controls_shuffle_on); - shuffleOffButtonDrawable = resources.getDrawable(R.drawable.exo_legacy_controls_shuffle_off); + repeatOffButtonDrawable = + getDrawable(context, resources, R.drawable.exo_legacy_controls_repeat_off); + repeatOneButtonDrawable = + getDrawable(context, resources, R.drawable.exo_legacy_controls_repeat_one); + repeatAllButtonDrawable = + getDrawable(context, resources, R.drawable.exo_legacy_controls_repeat_all); + shuffleOnButtonDrawable = + getDrawable(context, resources, R.drawable.exo_legacy_controls_shuffle_on); + shuffleOffButtonDrawable = + getDrawable(context, resources, R.drawable.exo_legacy_controls_shuffle_off); repeatOffButtonContentDescription = resources.getString(R.string.exo_controls_repeat_off_description); repeatOneButtonContentDescription = diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java index 8c56f78298a..461dbd2dd0d 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java @@ -34,6 +34,7 @@ import static androidx.media3.common.Player.EVENT_TRACKS_CHANGED; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull; +import static androidx.media3.common.util.Util.getDrawable; import android.annotation.SuppressLint; import android.content.Context; @@ -53,7 +54,9 @@ import android.widget.ImageView; import android.widget.PopupWindow; import android.widget.TextView; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.core.content.res.ResourcesCompat; import androidx.media3.common.C; import androidx.media3.common.Format; @@ -529,11 +532,11 @@ public PlayerControlView( settingTexts[SETTINGS_PLAYBACK_SPEED_POSITION] = resources.getString(R.string.exo_controls_playback_speed); settingIcons[SETTINGS_PLAYBACK_SPEED_POSITION] = - resources.getDrawable(R.drawable.exo_styled_controls_speed); + getDrawable(context, resources, R.drawable.exo_styled_controls_speed); settingTexts[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] = resources.getString(R.string.exo_track_selection_title_audio); settingIcons[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] = - resources.getDrawable(R.drawable.exo_styled_controls_audiotrack); + getDrawable(context, resources, R.drawable.exo_styled_controls_audiotrack); settingsAdapter = new SettingsAdapter(settingTexts, settingIcons); settingsWindowMargin = resources.getDimensionPixelSize(R.dimen.exo_settings_offset); settingsView = @@ -553,8 +556,10 @@ public PlayerControlView( needToHideBars = true; trackNameProvider = new DefaultTrackNameProvider(getResources()); - subtitleOnButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_subtitle_on); - subtitleOffButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_subtitle_off); + subtitleOnButtonDrawable = + getDrawable(context, resources, R.drawable.exo_styled_controls_subtitle_on); + subtitleOffButtonDrawable = + getDrawable(context, resources, R.drawable.exo_styled_controls_subtitle_off); subtitleOnContentDescription = resources.getString(R.string.exo_controls_cc_enabled_description); subtitleOffContentDescription = @@ -565,14 +570,20 @@ public PlayerControlView( new PlaybackSpeedAdapter( resources.getStringArray(R.array.exo_controls_playback_speeds), PLAYBACK_SPEEDS); - fullScreenExitDrawable = resources.getDrawable(R.drawable.exo_styled_controls_fullscreen_exit); + fullScreenExitDrawable = + getDrawable(context, resources, R.drawable.exo_styled_controls_fullscreen_exit); fullScreenEnterDrawable = - resources.getDrawable(R.drawable.exo_styled_controls_fullscreen_enter); - repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_repeat_off); - repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_repeat_one); - repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_repeat_all); - shuffleOnButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_shuffle_on); - shuffleOffButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_shuffle_off); + getDrawable(context, resources, R.drawable.exo_styled_controls_fullscreen_enter); + repeatOffButtonDrawable = + getDrawable(context, resources, R.drawable.exo_styled_controls_repeat_off); + repeatOneButtonDrawable = + getDrawable(context, resources, R.drawable.exo_styled_controls_repeat_one); + repeatAllButtonDrawable = + getDrawable(context, resources, R.drawable.exo_styled_controls_repeat_all); + shuffleOnButtonDrawable = + getDrawable(context, resources, R.drawable.exo_styled_controls_shuffle_on); + shuffleOffButtonDrawable = + getDrawable(context, resources, R.drawable.exo_styled_controls_shuffle_off); fullScreenExitContentDescription = resources.getString(R.string.exo_controls_fullscreen_exit_description); fullScreenEnterContentDescription = @@ -955,17 +966,20 @@ private void updatePlayPauseButton() { return; } if (playPauseButton != null) { - if (shouldShowPauseButton()) { - ((ImageView) playPauseButton) - .setImageDrawable(resources.getDrawable(R.drawable.exo_styled_controls_pause)); - playPauseButton.setContentDescription( - resources.getString(R.string.exo_controls_pause_description)); - } else { - ((ImageView) playPauseButton) - .setImageDrawable(resources.getDrawable(R.drawable.exo_styled_controls_play)); - playPauseButton.setContentDescription( - resources.getString(R.string.exo_controls_play_description)); - } + boolean shouldShowPauseButton = shouldShowPauseButton(); + @DrawableRes + int drawableRes = + shouldShowPauseButton + ? R.drawable.exo_styled_controls_pause + : R.drawable.exo_styled_controls_play; + @StringRes + int stringRes = + shouldShowPauseButton + ? R.string.exo_controls_pause_description + : R.string.exo_controls_play_description; + ((ImageView) playPauseButton) + .setImageDrawable(getDrawable(getContext(), resources, drawableRes)); + playPauseButton.setContentDescription(resources.getString(stringRes)); } } diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java index 998bd845ba5..6731d040a3e 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java @@ -18,6 +18,7 @@ import static androidx.media3.common.Player.COMMAND_GET_TEXT; import static androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Util.getDrawable; import static java.lang.annotation.ElementType.TYPE_USE; import android.annotation.SuppressLint; @@ -291,9 +292,9 @@ public PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAtt overlayFrameLayout = null; ImageView logo = new ImageView(context); if (Util.SDK_INT >= 23) { - configureEditModeLogoV23(getResources(), logo); + configureEditModeLogoV23(context, getResources(), logo); } else { - configureEditModeLogo(getResources(), logo); + configureEditModeLogo(context, getResources(), logo); } addView(logo); return; @@ -1450,13 +1451,14 @@ private void updateAspectRatio() { } @RequiresApi(23) - private static void configureEditModeLogoV23(Resources resources, ImageView logo) { - logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null)); + private static void configureEditModeLogoV23( + Context context, Resources resources, ImageView logo) { + logo.setImageDrawable(getDrawable(context, resources, R.drawable.exo_edit_mode_logo)); logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null)); } - private static void configureEditModeLogo(Resources resources, ImageView logo) { - logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo)); + private static void configureEditModeLogo(Context context, Resources resources, ImageView logo) { + logo.setImageDrawable(getDrawable(context, resources, R.drawable.exo_edit_mode_logo)); logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color)); } From 44dbeb808513a0e563ce3e26f892436fe4834bd4 Mon Sep 17 00:00:00 2001 From: rohks Date: Fri, 16 Dec 2022 12:41:29 +0000 Subject: [PATCH 071/141] Rename `EMPTY_MEDIA_ITEM` to `PLACEHOLDER_MEDIA_ITEM` The `MediaItem` instances in the following cases are not actually empty but acts as a placeholder. `EMPTY_MEDIA_ITEM` can also be confused with `MediaItem.EMPTY`. PiperOrigin-RevId: 495843012 (cherry picked from commit 3e7f53fda77048731d22de0221b0520a069eb582) --- .../src/main/java/androidx/media3/common/Timeline.java | 6 +++--- .../media3/exoplayer/source/ConcatenatingMediaSource.java | 6 +++--- .../media3/exoplayer/source/MergingMediaSource.java | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java index 43dc1aed117..679df19aae3 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java +++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java @@ -158,7 +158,7 @@ public static final class Window implements Bundleable { private static final Object FAKE_WINDOW_UID = new Object(); - private static final MediaItem EMPTY_MEDIA_ITEM = + private static final MediaItem PLACEHOLDER_MEDIA_ITEM = new MediaItem.Builder() .setMediaId("androidx.media3.common.Timeline") .setUri(Uri.EMPTY) @@ -258,7 +258,7 @@ public static final class Window implements Bundleable { /** Creates window. */ public Window() { uid = SINGLE_WINDOW_UID; - mediaItem = EMPTY_MEDIA_ITEM; + mediaItem = PLACEHOLDER_MEDIA_ITEM; } /** Sets the data held by this window. */ @@ -281,7 +281,7 @@ public Window set( int lastPeriodIndex, long positionInFirstPeriodUs) { this.uid = uid; - this.mediaItem = mediaItem != null ? mediaItem : EMPTY_MEDIA_ITEM; + this.mediaItem = mediaItem != null ? mediaItem : PLACEHOLDER_MEDIA_ITEM; this.tag = mediaItem != null && mediaItem.localConfiguration != null ? mediaItem.localConfiguration.tag diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource.java index b2f005c47dd..8ad14b9cdaa 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource.java @@ -61,7 +61,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource 0 ? mediaSources[0].getMediaItem() : EMPTY_MEDIA_ITEM; + return mediaSources.length > 0 ? mediaSources[0].getMediaItem() : PLACEHOLDER_MEDIA_ITEM; } @Override From 097cdded3f77d24355e5a2b3b81ebf555844a25a Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 16 Dec 2022 14:10:28 +0000 Subject: [PATCH 072/141] Clarify behavior for out-of-bounds indices and align implementations Some Player methods operate relative to existing indices in the playlist (add,remove,move,seek). As these operations may be issued from a place with a stale playlist (e.g. a controller that sends a command while the playlist is changing), we have to handle out- of-bounds indices gracefully. In most cases this is already documented and implemented correctly. However, some cases are not documented and the existing player implementations don't handle these cases consistently (or in some cases not even correctly). PiperOrigin-RevId: 495856295 (cherry picked from commit a1954f7e0a334492ffa35cf535d2e6c4e4c9ca91) --- .../java/androidx/media3/cast/CastPlayer.java | 27 +-- .../java/androidx/media3/common/Player.java | 27 +-- .../media3/exoplayer/ExoPlayerImpl.java | 38 +++-- .../media3/exoplayer/ExoPlayerTest.java | 120 ++++++++++--- .../session/MediaControllerImplBase.java | 74 ++++----- .../session/MediaControllerImplLegacy.java | 23 +-- .../MediaControllerStateMaskingTest.java | 157 ++++++++++++++++++ ...tateMaskingWithMediaSessionCompatTest.java | 157 ++++++++++++++++++ 8 files changed, 503 insertions(+), 120 deletions(-) diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java index b7c14438161..8d2a0cbde1b 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java @@ -44,7 +44,6 @@ import androidx.media3.common.Tracks; import androidx.media3.common.VideoSize; import androidx.media3.common.text.CueGroup; -import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Clock; import androidx.media3.common.util.ListenerSet; import androidx.media3.common.util.Log; @@ -296,7 +295,7 @@ public void setMediaItems(List mediaItems, int startIndex, long start @Override public void addMediaItems(int index, List mediaItems) { - Assertions.checkArgument(index >= 0); + checkArgument(index >= 0); int uid = MediaQueueItem.INVALID_ITEM_ID; if (index < currentTimeline.getWindowCount()) { uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid; @@ -306,14 +305,11 @@ public void addMediaItems(int index, List mediaItems) { @Override public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { - Assertions.checkArgument( - fromIndex >= 0 - && fromIndex <= toIndex - && toIndex <= currentTimeline.getWindowCount() - && newIndex >= 0 - && newIndex < currentTimeline.getWindowCount()); - newIndex = min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex)); - if (fromIndex == toIndex || fromIndex == newIndex) { + checkArgument(fromIndex >= 0 && fromIndex <= toIndex && newIndex >= 0); + int playlistSize = currentTimeline.getWindowCount(); + toIndex = min(toIndex, playlistSize); + newIndex = min(newIndex, playlistSize - (toIndex - fromIndex)); + if (fromIndex >= playlistSize || fromIndex == toIndex || fromIndex == newIndex) { // Do nothing. return; } @@ -326,9 +322,10 @@ public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { @Override public void removeMediaItems(int fromIndex, int toIndex) { - Assertions.checkArgument(fromIndex >= 0 && toIndex >= fromIndex); - toIndex = min(toIndex, currentTimeline.getWindowCount()); - if (fromIndex == toIndex) { + checkArgument(fromIndex >= 0 && toIndex >= fromIndex); + int playlistSize = currentTimeline.getWindowCount(); + toIndex = min(toIndex, playlistSize); + if (fromIndex >= playlistSize || fromIndex == toIndex) { // Do nothing. return; } @@ -406,6 +403,10 @@ public void seekTo( long positionMs, @Player.Command int seekCommand, boolean isRepeatingCurrentItem) { + checkArgument(mediaItemIndex >= 0); + if (!currentTimeline.isEmpty() && mediaItemIndex >= currentTimeline.getWindowCount()) { + return; + } MediaStatus mediaStatus = getMediaStatus(); // We assume the default position is 0. There is no support for seeking to the default position // in RemoteMediaClient. diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index 44464e39b99..be64212d219 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -1666,7 +1666,8 @@ default void onMetadata(Metadata metadata) {} /** * Moves the media item at the current index to the new index. * - * @param currentIndex The current index of the media item to move. + * @param currentIndex The current index of the media item to move. If the index is larger than + * the size of the playlist, the request is ignored. * @param newIndex The new index of the media item. If the new index is larger than the size of * the playlist the item is moved to the end of the playlist. */ @@ -1675,8 +1676,10 @@ default void onMetadata(Metadata metadata) {} /** * Moves the media item range to the new index. * - * @param fromIndex The start of the range to move. - * @param toIndex The first item not to be included in the range (exclusive). + * @param fromIndex The start of the range to move. If the index is larger than the size of the + * playlist, the request is ignored. + * @param toIndex The first item not to be included in the range (exclusive). If the index is + * larger than the size of the playlist, items up to the end of the playlist are moved. * @param newIndex The new index of the first media item of the range. If the new index is larger * than the size of the remaining playlist after removing the range, the range is moved to the * end of the playlist. @@ -1686,16 +1689,18 @@ default void onMetadata(Metadata metadata) {} /** * Removes the media item at the given index of the playlist. * - * @param index The index at which to remove the media item. + * @param index The index at which to remove the media item. If the index is larger than the size + * of the playlist, the request is ignored. */ void removeMediaItem(int index); /** * Removes a range of media items from the playlist. * - * @param fromIndex The index at which to start removing media items. + * @param fromIndex The index at which to start removing media items. If the index is larger than + * the size of the playlist, the request is ignored. * @param toIndex The index of the first item to be kept (exclusive). If the index is larger than - * the size of the playlist, media items to the end of the playlist are removed. + * the size of the playlist, media items up to the end of the playlist are removed. */ void removeMediaItems(int fromIndex, int toIndex); @@ -1876,9 +1881,8 @@ default void onMetadata(Metadata metadata) {} * For other streams it will typically be the start. * * @param mediaItemIndex The index of the {@link MediaItem} whose associated default position - * should be seeked to. - * @throws IllegalSeekPositionException If the player has a non-empty timeline and the provided - * {@code mediaItemIndex} is not within the bounds of the current timeline. + * should be seeked to. If the index is larger than the size of the playlist, the request is + * ignored. */ void seekToDefaultPosition(int mediaItemIndex); @@ -1893,11 +1897,10 @@ default void onMetadata(Metadata metadata) {} /** * Seeks to a position specified in milliseconds in the specified {@link MediaItem}. * - * @param mediaItemIndex The index of the {@link MediaItem}. + * @param mediaItemIndex The index of the {@link MediaItem}. If the index is larger than the size + * of the playlist, the request is ignored. * @param positionMs The seek position in the specified {@link MediaItem}, or {@link C#TIME_UNSET} * to seek to the media item's default position. - * @throws IllegalSeekPositionException If the player has a non-empty timeline and the provided - * {@code mediaItemIndex} is not within the bounds of the current timeline. */ void seekTo(int mediaItemIndex, long positionMs); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 4e6ebf0c325..525b2df9c43 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -18,6 +18,7 @@ import static androidx.media3.common.C.TRACK_TYPE_AUDIO; import static androidx.media3.common.C.TRACK_TYPE_CAMERA_MOTION; import static androidx.media3.common.C.TRACK_TYPE_VIDEO; +import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.castNonNull; @@ -76,7 +77,6 @@ import androidx.media3.common.VideoSize; import androidx.media3.common.text.Cue; import androidx.media3.common.text.CueGroup; -import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Clock; import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.HandlerWrapper; @@ -623,7 +623,6 @@ public void setMediaSources( @Override public void addMediaItems(int index, List mediaItems) { verifyApplicationThread(); - index = min(index, mediaSourceHolderSnapshots.size()); addMediaSources(index, createMediaSources(mediaItems)); } @@ -648,7 +647,8 @@ public void addMediaSources(List mediaSources) { @Override public void addMediaSources(int index, List mediaSources) { verifyApplicationThread(); - Assertions.checkArgument(index >= 0); + checkArgument(index >= 0); + index = min(index, mediaSourceHolderSnapshots.size()); Timeline oldTimeline = getCurrentTimeline(); pendingOperationAcks++; List holders = addMediaSourceHolders(index, mediaSources); @@ -674,7 +674,13 @@ public void addMediaSources(int index, List mediaSources) { @Override public void removeMediaItems(int fromIndex, int toIndex) { verifyApplicationThread(); - toIndex = min(toIndex, mediaSourceHolderSnapshots.size()); + checkArgument(fromIndex >= 0 && toIndex >= fromIndex); + int playlistSize = mediaSourceHolderSnapshots.size(); + toIndex = min(toIndex, playlistSize); + if (fromIndex >= playlistSize || fromIndex == toIndex) { + // Do nothing. + return; + } PlaybackInfo newPlaybackInfo = removeMediaItemsInternal(fromIndex, toIndex); boolean positionDiscontinuity = !newPlaybackInfo.periodId.periodUid.equals(playbackInfo.periodId.periodUid); @@ -693,14 +699,16 @@ public void removeMediaItems(int fromIndex, int toIndex) { @Override public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) { verifyApplicationThread(); - Assertions.checkArgument( - fromIndex >= 0 - && fromIndex <= toIndex - && toIndex <= mediaSourceHolderSnapshots.size() - && newFromIndex >= 0); + checkArgument(fromIndex >= 0 && fromIndex <= toIndex && newFromIndex >= 0); + int playlistSize = mediaSourceHolderSnapshots.size(); + toIndex = min(toIndex, playlistSize); + newFromIndex = min(newFromIndex, playlistSize - (toIndex - fromIndex)); + if (fromIndex >= playlistSize || fromIndex == toIndex || fromIndex == newFromIndex) { + // Do nothing. + return; + } Timeline oldTimeline = getCurrentTimeline(); pendingOperationAcks++; - newFromIndex = min(newFromIndex, mediaSourceHolderSnapshots.size() - (toIndex - fromIndex)); Util.moveItems(mediaSourceHolderSnapshots, fromIndex, toIndex, newFromIndex); Timeline newTimeline = createMaskingTimeline(); PlaybackInfo newPlaybackInfo = @@ -829,11 +837,11 @@ public void seekTo( @Player.Command int seekCommand, boolean isRepeatingCurrentItem) { verifyApplicationThread(); + checkArgument(mediaItemIndex >= 0); analyticsCollector.notifySeekStarted(); Timeline timeline = playbackInfo.timeline; - if (mediaItemIndex < 0 - || (!timeline.isEmpty() && mediaItemIndex >= timeline.getWindowCount())) { - throw new IllegalSeekPositionException(timeline, mediaItemIndex, positionMs); + if (!timeline.isEmpty() && mediaItemIndex >= timeline.getWindowCount()) { + return; } pendingOperationAcks++; if (isPlayingAd()) { @@ -2261,8 +2269,6 @@ private List addMediaSourceHolders( } private PlaybackInfo removeMediaItemsInternal(int fromIndex, int toIndex) { - Assertions.checkArgument( - fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolderSnapshots.size()); int currentIndex = getCurrentMediaItemIndex(); Timeline oldTimeline = getCurrentTimeline(); int currentMediaSourceCount = mediaSourceHolderSnapshots.size(); @@ -2301,7 +2307,7 @@ private Timeline createMaskingTimeline() { private PlaybackInfo maskTimelineAndPosition( PlaybackInfo playbackInfo, Timeline timeline, @Nullable Pair periodPositionUs) { - Assertions.checkArgument(timeline.isEmpty() || periodPositionUs != null); + checkArgument(timeline.isEmpty() || periodPositionUs != null); Timeline oldTimeline = playbackInfo.timeline; // Mask the timeline. playbackInfo = playbackInfo.copyWithTimeline(timeline); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 3fa73c69fc9..fb2ae47b596 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -95,7 +95,6 @@ import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.Format; -import androidx.media3.common.IllegalSeekPositionException; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.Metadata; @@ -931,31 +930,100 @@ public void periodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception { } @Test - public void illegalSeekPositionDoesThrow() throws Exception { - final IllegalSeekPositionException[] exception = new IllegalSeekPositionException[1]; - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - .waitForPlaybackState(Player.STATE_BUFFERING) - .executeRunnable( - new PlayerRunnable() { - @Override - public void run(ExoPlayer player) { - try { - player.seekTo(/* mediaItemIndex= */ 100, /* positionMs= */ 0); - } catch (IllegalSeekPositionException e) { - exception[0] = e; - } - } - }) - .waitForPlaybackState(Player.STATE_ENDED) - .build(); - new ExoPlayerTestRunner.Builder(context) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - assertThat(exception[0]).isNotNull(); + public void seekTo_indexLargerThanPlaylist_isIgnored() throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + player.setMediaItem(MediaItem.fromUri("http://test")); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1000); + + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + player.release(); + } + + @Test + public void addMediaItems_indexLargerThanPlaylist_addsToEndOfPlaylist() throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + player.setMediaItem(MediaItem.fromUri("http://test")); + ImmutableList addedItems = + ImmutableList.of(MediaItem.fromUri("http://new1"), MediaItem.fromUri("http://new2")); + + player.addMediaItems(/* index= */ 5000, addedItems); + + assertThat(player.getMediaItemCount()).isEqualTo(3); + assertThat(player.getMediaItemAt(1)).isEqualTo(addedItems.get(0)); + assertThat(player.getMediaItemAt(2)).isEqualTo(addedItems.get(1)); + player.release(); + } + + @Test + public void removeMediaItems_fromIndexLargerThanPlaylist_isIgnored() throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + player.setMediaItems( + ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2"))); + + player.removeMediaItems(/* fromIndex= */ 5000, /* toIndex= */ 6000); + + assertThat(player.getMediaItemCount()).isEqualTo(2); + player.release(); + } + + @Test + public void removeMediaItems_toIndexLargerThanPlaylist_removesUpToEndOfPlaylist() + throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + player.setMediaItems( + ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2"))); + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 6000); + + assertThat(player.getMediaItemCount()).isEqualTo(1); + assertThat(player.getMediaItemAt(0).localConfiguration.uri.toString()) + .isEqualTo("http://item1"); + player.release(); + } + + @Test + public void moveMediaItems_fromIndexLargerThanPlaylist_isIgnored() throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + ImmutableList items = + ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2")); + player.setMediaItems(items); + + player.moveMediaItems(/* fromIndex= */ 5000, /* toIndex= */ 6000, /* newIndex= */ 0); + + assertThat(player.getMediaItemAt(0)).isEqualTo(items.get(0)); + assertThat(player.getMediaItemAt(1)).isEqualTo(items.get(1)); + player.release(); + } + + @Test + public void moveMediaItems_toIndexLargerThanPlaylist_movesItemsUpToEndOfPlaylist() + throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + ImmutableList items = + ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2")); + player.setMediaItems(items); + + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 6000, /* newIndex= */ 0); + + assertThat(player.getMediaItemAt(0)).isEqualTo(items.get(1)); + assertThat(player.getMediaItemAt(1)).isEqualTo(items.get(0)); + player.release(); + } + + @Test + public void moveMediaItems_newIndexLargerThanPlaylist_movesItemsUpToEndOfPlaylist() + throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + ImmutableList items = + ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2")); + player.setMediaItems(items); + + player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 1, /* newIndex= */ 5000); + + assertThat(player.getMediaItemAt(0)).isEqualTo(items.get(1)); + assertThat(player.getMediaItemAt(1)).isEqualTo(items.get(0)); + player.release(); } @Test diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index 6560cea8566..e3a2ca4a339 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -463,6 +463,7 @@ public void seekToDefaultPosition(int mediaItemIndex) { if (!isPlayerCommandAvailable(Player.COMMAND_SEEK_TO_MEDIA_ITEM)) { return; } + checkArgument(mediaItemIndex >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_SEEK_TO_MEDIA_ITEM, @@ -499,6 +500,7 @@ public void seekTo(int mediaItemIndex, long positionMs) { if (!isPlayerCommandAvailable(Player.COMMAND_SEEK_TO_MEDIA_ITEM)) { return; } + checkArgument(mediaItemIndex >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_SEEK_TO_MEDIA_ITEM, @@ -938,6 +940,7 @@ public void addMediaItem(int index, MediaItem mediaItem) { if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { return; } + checkArgument(index >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_CHANGE_MEDIA_ITEMS, @@ -969,6 +972,7 @@ public void addMediaItems(int index, List mediaItems) { if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { return; } + checkArgument(index >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_CHANGE_MEDIA_ITEMS, @@ -1038,6 +1042,7 @@ public void removeMediaItem(int index) { if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { return; } + checkArgument(index >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_CHANGE_MEDIA_ITEMS, @@ -1051,6 +1056,7 @@ public void removeMediaItems(int fromIndex, int toIndex) { if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { return; } + checkArgument(fromIndex >= 0 && toIndex >= fromIndex); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_CHANGE_MEDIA_ITEMS, @@ -1073,19 +1079,17 @@ public void clearMediaItems() { } private void removeMediaItemsInternal(int fromIndex, int toIndex) { - int clippedToIndex = min(toIndex, playerInfo.timeline.getWindowCount()); - - checkArgument( - fromIndex >= 0 - && clippedToIndex >= fromIndex - && clippedToIndex <= playerInfo.timeline.getWindowCount()); - Timeline oldTimeline = playerInfo.timeline; + int playlistSize = playerInfo.timeline.getWindowCount(); + toIndex = min(toIndex, playlistSize); + if (fromIndex >= playlistSize || fromIndex == toIndex) { + return; + } List newWindows = new ArrayList<>(); List newPeriods = new ArrayList<>(); for (int i = 0; i < oldTimeline.getWindowCount(); i++) { - if (i < fromIndex || i >= clippedToIndex) { + if (i < fromIndex || i >= toIndex) { newWindows.add(oldTimeline.getWindow(i, new Window())); } } @@ -1097,7 +1101,7 @@ private void removeMediaItemsInternal(int fromIndex, int toIndex) { int oldPeriodIndex = playerInfo.sessionPositionInfo.positionInfo.periodIndex; int newPeriodIndex = oldPeriodIndex; boolean currentItemRemoved = - getCurrentMediaItemIndex() >= fromIndex && getCurrentMediaItemIndex() < clippedToIndex; + getCurrentMediaItemIndex() >= fromIndex && getCurrentMediaItemIndex() < toIndex; Window window = new Window(); if (oldTimeline.isEmpty()) { // No masking required. Just forwarding command to session. @@ -1117,17 +1121,17 @@ private void removeMediaItemsInternal(int fromIndex, int toIndex) { toIndex); if (oldNextMediaItemIndex == C.INDEX_UNSET) { newMediaItemIndex = newTimeline.getFirstWindowIndex(getShuffleModeEnabled()); - } else if (oldNextMediaItemIndex >= clippedToIndex) { - newMediaItemIndex = oldNextMediaItemIndex - (clippedToIndex - fromIndex); + } else if (oldNextMediaItemIndex >= toIndex) { + newMediaItemIndex = oldNextMediaItemIndex - (toIndex - fromIndex); } else { newMediaItemIndex = oldNextMediaItemIndex; } newPeriodIndex = newTimeline.getWindow(newMediaItemIndex, window).firstPeriodIndex; - } else if (oldMediaItemIndex >= clippedToIndex) { - newMediaItemIndex -= (clippedToIndex - fromIndex); + } else if (oldMediaItemIndex >= toIndex) { + newMediaItemIndex -= (toIndex - fromIndex); newPeriodIndex = getNewPeriodIndexWithoutRemovedPeriods( - oldTimeline, oldPeriodIndex, fromIndex, clippedToIndex); + oldTimeline, oldPeriodIndex, fromIndex, toIndex); } } @@ -1191,8 +1195,8 @@ private void removeMediaItemsInternal(int fromIndex, int toIndex) { final boolean transitionsToEnded = newPlayerInfo.playbackState != Player.STATE_IDLE && newPlayerInfo.playbackState != Player.STATE_ENDED - && fromIndex < clippedToIndex - && clippedToIndex == oldTimeline.getWindowCount() + && fromIndex < toIndex + && toIndex == oldTimeline.getWindowCount() && getCurrentMediaItemIndex() >= fromIndex; if (transitionsToEnded) { newPlayerInfo = @@ -1207,7 +1211,7 @@ private void removeMediaItemsInternal(int fromIndex, int toIndex) { Player.DISCONTINUITY_REASON_REMOVE, /* mediaItemTransition= */ playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex >= fromIndex - && playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex < clippedToIndex, + && playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex < toIndex, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); } } @@ -1217,18 +1221,14 @@ public void moveMediaItem(int currentIndex, int newIndex) { if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { return; } - - checkArgument( - currentIndex >= 0 && currentIndex < playerInfo.timeline.getWindowCount() && newIndex >= 0); + checkArgument(currentIndex >= 0 && newIndex >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.moveMediaItem(controllerStub, seq, currentIndex, newIndex)); - int clippedNewIndex = min(newIndex, playerInfo.timeline.getWindowCount() - 1); - moveMediaItemsInternal( - /* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, clippedNewIndex); + /* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex); } @Override @@ -1236,22 +1236,14 @@ public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { return; } - - checkArgument( - fromIndex >= 0 - && fromIndex <= toIndex - && toIndex <= playerInfo.timeline.getWindowCount() - && newIndex >= 0); + checkArgument(fromIndex >= 0 && fromIndex <= toIndex && newIndex >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.moveMediaItems(controllerStub, seq, fromIndex, toIndex, newIndex)); - int clippedNewIndex = - min(newIndex, playerInfo.timeline.getWindowCount() - (toIndex - fromIndex)); - - moveMediaItemsInternal(fromIndex, toIndex, clippedNewIndex); + moveMediaItemsInternal(fromIndex, toIndex, newIndex); } @Override @@ -1899,16 +1891,18 @@ private void setMediaItemsInternal( } private void moveMediaItemsInternal(int fromIndex, int toIndex, int newIndex) { - if (fromIndex == 0 && toIndex == playerInfo.timeline.getWindowCount()) { + Timeline oldTimeline = playerInfo.timeline; + int playlistSize = playerInfo.timeline.getWindowCount(); + toIndex = min(toIndex, playlistSize); + newIndex = min(newIndex, playlistSize - (toIndex - fromIndex)); + if (fromIndex >= playlistSize || fromIndex == toIndex || fromIndex == newIndex) { return; } - Timeline oldTimeline = playerInfo.timeline; - List newWindows = new ArrayList<>(); List newPeriods = new ArrayList<>(); - for (int i = 0; i < oldTimeline.getWindowCount(); i++) { + for (int i = 0; i < playlistSize; i++) { newWindows.add(oldTimeline.getWindow(i, new Window())); } Util.moveItems(newWindows, fromIndex, toIndex, newIndex); @@ -1968,11 +1962,7 @@ private void seekToInternalByOffset(long offsetMs) { private void seekToInternal(int windowIndex, long positionMs) { Timeline timeline = playerInfo.timeline; - if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { - throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); - } - - if (isPlayingAd()) { + if ((!timeline.isEmpty() && windowIndex >= timeline.getWindowCount()) || isPlayingAd()) { return; } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index 2c670fa855e..ff8e3c7c9a0 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -15,6 +15,7 @@ */ package androidx.media3.session; +import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; @@ -47,7 +48,6 @@ import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; -import androidx.media3.common.IllegalSeekPositionException; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.PlaybackException; @@ -311,13 +311,11 @@ public void seekTo(int mediaItemIndex, long positionMs) { } private void seekToInternal(int mediaItemIndex, long positionMs) { + checkArgument(mediaItemIndex >= 0); int currentMediaItemIndex = getCurrentMediaItemIndex(); Timeline currentTimeline = controllerInfo.playerInfo.timeline; - if (currentMediaItemIndex != mediaItemIndex - && (mediaItemIndex < 0 || mediaItemIndex >= currentTimeline.getWindowCount())) { - throw new IllegalSeekPositionException(currentTimeline, mediaItemIndex, positionMs); - } - if (isPlayingAd()) { + if ((!currentTimeline.isEmpty() && mediaItemIndex >= currentTimeline.getWindowCount()) + || isPlayingAd()) { return; } int newMediaItemIndex = currentMediaItemIndex; @@ -687,6 +685,7 @@ public void addMediaItems(List mediaItems) { @Override public void addMediaItems(int index, List mediaItems) { + checkArgument(index >= 0); if (mediaItems.isEmpty()) { return; } @@ -732,9 +731,10 @@ public void removeMediaItem(int index) { @Override public void removeMediaItems(int fromIndex, int toIndex) { + checkArgument(fromIndex >= 0 && toIndex >= fromIndex); int windowCount = getCurrentTimeline().getWindowCount(); toIndex = min(toIndex, windowCount); - if (fromIndex >= toIndex) { + if (fromIndex >= windowCount || fromIndex == toIndex) { return; } @@ -787,15 +787,16 @@ public void moveMediaItem(int currentIndex, int newIndex) { @Override public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { + checkArgument(fromIndex >= 0 && fromIndex <= toIndex && newIndex >= 0); QueueTimeline queueTimeline = (QueueTimeline) controllerInfo.playerInfo.timeline; int size = queueTimeline.getWindowCount(); toIndex = min(toIndex, size); - if (fromIndex >= toIndex) { - return; - } int moveItemsSize = toIndex - fromIndex; int lastItemIndexAfterRemove = size - moveItemsSize - 1; - newIndex = min(newIndex, lastItemIndexAfterRemove); + newIndex = min(newIndex, lastItemIndexAfterRemove + 1); + if (fromIndex >= size || fromIndex == toIndex || fromIndex == newIndex) { + return; + } int currentMediaItemIndex = getCurrentMediaItemIndex(); int currentMediaItemIndexAfterRemove = diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingTest.java index 7e7f18dc171..e923f21d5a7 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingTest.java @@ -46,6 +46,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -2957,6 +2958,162 @@ public void onEvents(Player player, Player.Events events) { assertThat(reportedStateChangeToEndedAtSameTimeAsDiscontinuity.get()).isTrue(); } + @Test + public void seekTo_indexLargerThanPlaylist_isIgnored() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + AtomicInteger mediaItemIndexAfterSeek = new AtomicInteger(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItem(MediaItem.fromUri("http://test")); + + controller.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000); + + mediaItemIndexAfterSeek.set(controller.getCurrentMediaItemIndex()); + }); + + assertThat(mediaItemIndexAfterSeek.get()).isEqualTo(0); + } + + @Test + public void addMediaItems_indexLargerThanPlaylist_addsToEndOfPlaylist() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + List addedItems = + ImmutableList.of(MediaItem.fromUri("http://new1"), MediaItem.fromUri("http://new2")); + ArrayList mediaItemsAfterAdd = new ArrayList<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItem(MediaItem.fromUri("http://test")); + + controller.addMediaItems(/* index= */ 5000, addedItems); + + for (int i = 0; i < controller.getMediaItemCount(); i++) { + mediaItemsAfterAdd.add(controller.getMediaItemAt(i)); + } + }); + + assertThat(mediaItemsAfterAdd).hasSize(3); + assertThat(mediaItemsAfterAdd.get(1)).isEqualTo(addedItems.get(0)); + assertThat(mediaItemsAfterAdd.get(2)).isEqualTo(addedItems.get(1)); + } + + @Test + public void removeMediaItems_fromIndexLargerThanPlaylist_isIgnored() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + AtomicInteger mediaItemCountAfterRemove = new AtomicInteger(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2"))); + + controller.removeMediaItems(/* fromIndex= */ 5000, /* toIndex= */ 6000); + + mediaItemCountAfterRemove.set(controller.getMediaItemCount()); + }); + + assertThat(mediaItemCountAfterRemove.get()).isEqualTo(2); + } + + @Test + public void removeMediaItems_toIndexLargerThanPlaylist_removesUpToEndOfPlaylist() + throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + AtomicInteger mediaItemCountAfterRemove = new AtomicInteger(); + AtomicReference remainingItemAfterRemove = new AtomicReference<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2"))); + + controller.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 6000); + + mediaItemCountAfterRemove.set(controller.getMediaItemCount()); + remainingItemAfterRemove.set(controller.getMediaItemAt(0)); + }); + + assertThat(mediaItemCountAfterRemove.get()).isEqualTo(1); + assertThat(remainingItemAfterRemove.get().localConfiguration.uri.toString()) + .isEqualTo("http://item1"); + } + + @Test + public void moveMediaItems_fromIndexLargerThanPlaylist_isIgnored() throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + List items = + ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2")); + ArrayList itemsAfterMove = new ArrayList<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems(items); + + controller.moveMediaItems( + /* fromIndex= */ 5000, /* toIndex= */ 6000, /* newIndex= */ 0); + + for (int i = 0; i < controller.getMediaItemCount(); i++) { + itemsAfterMove.add(controller.getMediaItemAt(i)); + } + }); + + assertThat(itemsAfterMove).isEqualTo(items); + } + + @Test + public void moveMediaItems_toIndexLargerThanPlaylist_movesItemsUpToEndOfPlaylist() + throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + List items = + ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2")); + ArrayList itemsAfterMove = new ArrayList<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems(items); + + controller.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 6000, /* newIndex= */ 0); + + for (int i = 0; i < controller.getMediaItemCount(); i++) { + itemsAfterMove.add(controller.getMediaItemAt(i)); + } + }); + + assertThat(itemsAfterMove).containsExactly(items.get(1), items.get(0)).inOrder(); + } + + @Test + public void moveMediaItems_newIndexLargerThanPlaylist_movesItemsUpToEndOfPlaylist() + throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + List items = + ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2")); + ArrayList itemsAfterMove = new ArrayList<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems(items); + + controller.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 1, /* newIndex= */ 5000); + + for (int i = 0; i < controller.getMediaItemCount(); i++) { + itemsAfterMove.add(controller.getMediaItemAt(i)); + } + }); + + assertThat(itemsAfterMove).containsExactly(items.get(1), items.get(0)).inOrder(); + } + private void assertMoveMediaItems( int initialMediaItemCount, int initialMediaItemIndex, diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingWithMediaSessionCompatTest.java index e10dd5ae963..4fb1de36af1 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingWithMediaSessionCompatTest.java @@ -58,6 +58,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -1150,4 +1151,160 @@ public void onEvents(Player player, Player.Events events) { assertThat(currentMediaItemIndexRef.get()).isEqualTo(testCurrentMediaItemIndex); MediaTestUtils.assertTimelineContains(timelineFromGetterRef.get(), testMediaItems); } + + @Test + public void seekTo_indexLargerThanPlaylist_isIgnored() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + AtomicInteger mediaItemIndexAfterSeek = new AtomicInteger(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItem(MediaItem.fromUri("http://test")); + + controller.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000); + + mediaItemIndexAfterSeek.set(controller.getCurrentMediaItemIndex()); + }); + + assertThat(mediaItemIndexAfterSeek.get()).isEqualTo(0); + } + + @Test + public void addMediaItems_indexLargerThanPlaylist_addsToEndOfPlaylist() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + List addedItems = + ImmutableList.of(MediaItem.fromUri("http://new1"), MediaItem.fromUri("http://new2")); + ArrayList mediaItemsAfterAdd = new ArrayList<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItem(MediaItem.fromUri("http://test")); + + controller.addMediaItems(/* index= */ 5000, addedItems); + + for (int i = 0; i < controller.getMediaItemCount(); i++) { + mediaItemsAfterAdd.add(controller.getMediaItemAt(i)); + } + }); + + assertThat(mediaItemsAfterAdd).hasSize(3); + assertThat(mediaItemsAfterAdd.get(1)).isEqualTo(addedItems.get(0)); + assertThat(mediaItemsAfterAdd.get(2)).isEqualTo(addedItems.get(1)); + } + + @Test + public void removeMediaItems_fromIndexLargerThanPlaylist_isIgnored() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + AtomicInteger mediaItemCountAfterRemove = new AtomicInteger(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2"))); + + controller.removeMediaItems(/* fromIndex= */ 5000, /* toIndex= */ 6000); + + mediaItemCountAfterRemove.set(controller.getMediaItemCount()); + }); + + assertThat(mediaItemCountAfterRemove.get()).isEqualTo(2); + } + + @Test + public void removeMediaItems_toIndexLargerThanPlaylist_removesUpToEndOfPlaylist() + throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + AtomicInteger mediaItemCountAfterRemove = new AtomicInteger(); + AtomicReference remainingItemAfterRemove = new AtomicReference<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2"))); + + controller.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 6000); + + mediaItemCountAfterRemove.set(controller.getMediaItemCount()); + remainingItemAfterRemove.set(controller.getMediaItemAt(0)); + }); + + assertThat(mediaItemCountAfterRemove.get()).isEqualTo(1); + assertThat(remainingItemAfterRemove.get().localConfiguration.uri.toString()) + .isEqualTo("http://item1"); + } + + @Test + public void moveMediaItems_fromIndexLargerThanPlaylist_isIgnored() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + List items = + ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2")); + ArrayList itemsAfterMove = new ArrayList<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems(items); + + controller.moveMediaItems( + /* fromIndex= */ 5000, /* toIndex= */ 6000, /* newIndex= */ 0); + + for (int i = 0; i < controller.getMediaItemCount(); i++) { + itemsAfterMove.add(controller.getMediaItemAt(i)); + } + }); + + assertThat(itemsAfterMove).isEqualTo(items); + } + + @Test + public void moveMediaItems_toIndexLargerThanPlaylist_movesItemsUpToEndOfPlaylist() + throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + List items = + ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2")); + ArrayList itemsAfterMove = new ArrayList<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems(items); + + controller.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 6000, /* newIndex= */ 0); + + for (int i = 0; i < controller.getMediaItemCount(); i++) { + itemsAfterMove.add(controller.getMediaItemAt(i)); + } + }); + + assertThat(itemsAfterMove).containsExactly(items.get(1), items.get(0)).inOrder(); + } + + @Test + public void moveMediaItems_newIndexLargerThanPlaylist_movesItemsUpToEndOfPlaylist() + throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + List items = + ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2")); + ArrayList itemsAfterMove = new ArrayList<>(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems(items); + + controller.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 1, /* newIndex= */ 5000); + + for (int i = 0; i < controller.getMediaItemCount(); i++) { + itemsAfterMove.add(controller.getMediaItemAt(i)); + } + }); + + assertThat(itemsAfterMove).containsExactly(items.get(1), items.get(0)).inOrder(); + } } From da6c2dfa649b18e357a9cb9eea373b5ff9002641 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 16 Dec 2022 16:39:12 +0000 Subject: [PATCH 073/141] Check if codec still exists before handling tunneling events The tunneling callbacks are sent via Handler messages and may be handled after the codec/surface was changed or released. We already guard against the codec/surface change condition by creating a new listener and verifying that the current callback happens for the correct listener instance, but we don't guard against a released codec yet. PiperOrigin-RevId: 495882353 (cherry picked from commit 49ccfd63834d8ee68ac8018c42172da05108b35a) --- .../media3/exoplayer/video/MediaCodecVideoRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java index 437062c570f..1bd45fc24aa 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java @@ -2050,7 +2050,7 @@ public boolean handleMessage(Message message) { } private void handleFrameRendered(long presentationTimeUs) { - if (this != tunnelingOnFrameRenderedListener) { + if (this != tunnelingOnFrameRenderedListener || getCodec() == null) { // Stale event. return; } From 2186b6d325c531fa992c78f018eb38fe86e84c66 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 19 Dec 2022 08:41:50 +0000 Subject: [PATCH 074/141] Avoid sending periodic position updates while paused and not loading The period updates were introduced to ensure the buffered position is updated regularly and that any playback position drift is corrected. None of these updates need to happen while the player is paused or not loading and we can avoid the constant binder interactions. PiperOrigin-RevId: 496329800 (cherry picked from commit 0749b05923dd733bb515920334a9aae6067a072f) --- .../media3/session/MediaSessionImpl.java | 23 +++++++---- .../media3/session/MediaControllerTest.java | 39 +++++++++++++++++-- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index d01fb6eee3f..4e075c18bd9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -121,6 +121,7 @@ @Nullable private final BroadcastReceiver broadcastReceiver; private final Handler applicationHandler; private final BitmapLoader bitmapLoader; + private final Runnable periodicSessionPositionInfoUpdateRunnable; @Nullable private PlayerListener playerListener; @Nullable private MediaSession.Listener mediaSessionListener; @@ -246,8 +247,9 @@ public MediaSessionImpl( /* oldPlayerWrapper= */ null, /* newPlayerWrapper= */ playerWrapper)); sessionPositionUpdateDelayMs = DEFAULT_SESSION_POSITION_UPDATE_DELAY_MS; - applicationHandler.postDelayed( - thisRef::notifyPeriodicSessionPositionInfoChangesOnHandler, sessionPositionUpdateDelayMs); + periodicSessionPositionInfoUpdateRunnable = + thisRef::notifyPeriodicSessionPositionInfoChangesOnHandler; + postOrRun(applicationHandler, thisRef::schedulePeriodicSessionPositionInfoChanges); } public void setPlayer(Player player) { @@ -567,10 +569,7 @@ protected MediaSessionServiceLegacyStub createLegacyBrowserService( protected void setSessionPositionUpdateDelayMsOnHandler(long updateDelayMs) { verifyApplicationThread(); sessionPositionUpdateDelayMs = updateDelayMs; - - applicationHandler.removeCallbacks(this::notifyPeriodicSessionPositionInfoChangesOnHandler); - applicationHandler.postDelayed( - this::notifyPeriodicSessionPositionInfoChangesOnHandler, updateDelayMs); + schedulePeriodicSessionPositionInfoChanges(); } @Nullable @@ -718,9 +717,15 @@ private void notifyPeriodicSessionPositionInfoChangesOnHandler() { SessionPositionInfo sessionPositionInfo = playerWrapper.createSessionPositionInfoForBundling(); dispatchRemoteControllerTaskWithoutReturn( (callback, seq) -> callback.onPeriodicSessionPositionInfoChanged(seq, sessionPositionInfo)); - if (sessionPositionUpdateDelayMs > 0) { + schedulePeriodicSessionPositionInfoChanges(); + } + + private void schedulePeriodicSessionPositionInfoChanges() { + applicationHandler.removeCallbacks(periodicSessionPositionInfoUpdateRunnable); + if (sessionPositionUpdateDelayMs > 0 + && (playerWrapper.isPlaying() || playerWrapper.isLoading())) { applicationHandler.postDelayed( - this::notifyPeriodicSessionPositionInfoChangesOnHandler, sessionPositionUpdateDelayMs); + periodicSessionPositionInfoUpdateRunnable, sessionPositionUpdateDelayMs); } } @@ -859,6 +864,7 @@ public void onIsPlayingChanged(boolean isPlaying) { /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onIsPlayingChanged(seq, isPlaying)); + session.schedulePeriodicSessionPositionInfoChanges(); } @Override @@ -877,6 +883,7 @@ public void onIsLoadingChanged(boolean isLoading) { /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onIsLoadingChanged(seq, isLoading)); + session.schedulePeriodicSessionPositionInfoChanges(); } @Override diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java index 3c13e756ceb..a7b5dbfc6c3 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java @@ -24,6 +24,7 @@ import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET_SESSION_ACTIVITY; import static androidx.media3.test.session.common.MediaSessionConstants.TEST_IS_SESSION_COMMAND_AVAILABLE; import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS; +import static androidx.media3.test.session.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -984,11 +985,18 @@ public void getContentPosition_whenPlayingAd_returnsContentPosition() throws Exc @Test public void getBufferedPosition_withPeriodicUpdate_updatedWithoutCallback() throws Exception { long testBufferedPosition = 999L; + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setPlayWhenReady(true) + .setPlaybackSuppressionReason(Player.PLAYBACK_SUPPRESSION_REASON_NONE) + .setPlaybackState(Player.STATE_READY) + .setIsLoading(true) + .build(); + remoteSession.setPlayer(playerConfig); MediaController controller = controllerTestRule.createController(remoteSession.getToken()); - remoteSession.getMockPlayer().setBufferedPosition(testBufferedPosition); - - remoteSession.setSessionPositionUpdateDelayMs(0L); + remoteSession.setSessionPositionUpdateDelayMs(10L); + remoteSession.getMockPlayer().setBufferedPosition(testBufferedPosition); PollingCheck.waitFor( TIMEOUT_MS, () -> { @@ -998,6 +1006,31 @@ public void getBufferedPosition_withPeriodicUpdate_updatedWithoutCallback() thro }); } + @Test + public void getBufferedPosition_whilePausedAndNotLoading_isNotUpdatedPeriodically() + throws Exception { + long testBufferedPosition = 999L; + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setPlayWhenReady(false) + .setPlaybackSuppressionReason(Player.PLAYBACK_SUPPRESSION_REASON_NONE) + .setPlaybackState(Player.STATE_READY) + .setIsLoading(false) + .build(); + remoteSession.setPlayer(playerConfig); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + remoteSession.setSessionPositionUpdateDelayMs(10L); + + remoteSession.getMockPlayer().setBufferedPosition(testBufferedPosition); + Thread.sleep(NO_RESPONSE_TIMEOUT_MS); + AtomicLong bufferedPositionAfterDelay = new AtomicLong(); + threadTestRule + .getHandler() + .postAndSync(() -> bufferedPositionAfterDelay.set(controller.getBufferedPosition())); + + assertThat(bufferedPositionAfterDelay.get()).isNotEqualTo(testBufferedPosition); + } + @Test public void getContentBufferedPosition_byDefault_returnsZero() throws Exception { MediaController controller = controllerTestRule.createController(remoteSession.getToken()); From 776859005b632b9b3ba3dfddd99c3ee476d86562 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 19 Dec 2022 12:16:35 +0000 Subject: [PATCH 075/141] Add playlist and seek operations to SimpleBasePlayer These are the remaining setter operations. They all share the same logic that handles playlist and/or position changes. The logic to create the placeholder state is mostly copied from ExoPlayerImpl's maskTimelineAndPosition and getPeriodPositonUsAfterTimelineChanged. PiperOrigin-RevId: 496364712 (cherry picked from commit 5fa115641d5b45b106844f3e629372417eb100b1) --- RELEASENOTES.md | 2 + .../media3/common/SimpleBasePlayer.java | 435 +- .../media3/common/SimpleBasePlayerTest.java | 3546 ++++++++++++++++- 3 files changed, 3956 insertions(+), 27 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3b4ca8610d8..f0c4f888924 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,6 +26,8 @@ * SubRip: Add support for UTF-16 files if they start with a byte order mark. * Session: + * Add abstract `SimpleBasePlayer` to help implement the `Player` interface + for custom players. * Add helper method to convert platform session token to Media3 `SessionToken` ([#171](https://github.com/androidx/media/issues/171)). * Metadata: diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index aa677d85c8a..6ab13a7a373 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -22,6 +22,7 @@ import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.common.util.Util.usToMs; import static java.lang.Math.max; +import static java.lang.Math.min; import android.graphics.Rect; import android.os.Looper; @@ -48,6 +49,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.ForOverride; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -2018,33 +2020,118 @@ public final boolean getPlayWhenReady() { @Override public final void setMediaItems(List mediaItems, boolean resetPosition) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + int startIndex = resetPosition ? C.INDEX_UNSET : state.currentMediaItemIndex; + long startPositionMs = resetPosition ? C.TIME_UNSET : state.contentPositionMsSupplier.get(); + setMediaItemsInternal(mediaItems, startIndex, startPositionMs); } @Override public final void setMediaItems( List mediaItems, int startIndex, long startPositionMs) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + if (startIndex == C.INDEX_UNSET) { + startIndex = state.currentMediaItemIndex; + startPositionMs = state.contentPositionMsSupplier.get(); + } + setMediaItemsInternal(mediaItems, startIndex, startPositionMs); + } + + @RequiresNonNull("state") + private void setMediaItemsInternal( + List mediaItems, int startIndex, long startPositionMs) { + checkArgument(startIndex == C.INDEX_UNSET || startIndex >= 0); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) + && (mediaItems.size() != 1 || !shouldHandleCommand(Player.COMMAND_SET_MEDIA_ITEM))) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetMediaItems(mediaItems, startIndex, startPositionMs), + /* placeholderStateSupplier= */ () -> { + ArrayList placeholderPlaylist = new ArrayList<>(); + for (int i = 0; i < mediaItems.size(); i++) { + placeholderPlaylist.add(getPlaceholderMediaItemData(mediaItems.get(i))); + } + return getStateWithNewPlaylistAndPosition( + state, placeholderPlaylist, startIndex, startPositionMs); + }); } @Override public final void addMediaItems(int index, List mediaItems) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + checkArgument(index >= 0); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + int playlistSize = state.playlist.size(); + if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) || mediaItems.isEmpty()) { + return; + } + int correctedIndex = min(index, playlistSize); + updateStateForPendingOperation( + /* pendingOperation= */ handleAddMediaItems(correctedIndex, mediaItems), + /* placeholderStateSupplier= */ () -> { + ArrayList placeholderPlaylist = new ArrayList<>(state.playlist); + for (int i = 0; i < mediaItems.size(); i++) { + placeholderPlaylist.add( + i + correctedIndex, getPlaceholderMediaItemData(mediaItems.get(i))); + } + return getStateWithNewPlaylist(state, placeholderPlaylist, period); + }); } @Override public final void moveMediaItems(int fromIndex, int toIndex, int newIndex) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + checkArgument(fromIndex >= 0 && toIndex >= fromIndex && newIndex >= 0); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + int playlistSize = state.playlist.size(); + if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) + || playlistSize == 0 + || fromIndex >= playlistSize) { + return; + } + int correctedToIndex = min(toIndex, playlistSize); + int correctedNewIndex = min(newIndex, state.playlist.size() - (correctedToIndex - fromIndex)); + if (fromIndex == correctedToIndex || correctedNewIndex == fromIndex) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleMoveMediaItems( + fromIndex, correctedToIndex, correctedNewIndex), + /* placeholderStateSupplier= */ () -> { + ArrayList placeholderPlaylist = new ArrayList<>(state.playlist); + Util.moveItems(placeholderPlaylist, fromIndex, correctedToIndex, correctedNewIndex); + return getStateWithNewPlaylist(state, placeholderPlaylist, period); + }); } @Override public final void removeMediaItems(int fromIndex, int toIndex) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + checkArgument(fromIndex >= 0 && toIndex >= fromIndex); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + int playlistSize = state.playlist.size(); + if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) + || playlistSize == 0 + || fromIndex >= playlistSize) { + return; + } + int correctedToIndex = min(toIndex, playlistSize); + if (fromIndex == correctedToIndex) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleRemoveMediaItems(fromIndex, correctedToIndex), + /* placeholderStateSupplier= */ () -> { + ArrayList placeholderPlaylist = new ArrayList<>(state.playlist); + Util.removeRange(placeholderPlaylist, fromIndex, correctedToIndex); + return getStateWithNewPlaylist(state, placeholderPlaylist, period); + }); } @Override @@ -2138,8 +2225,21 @@ public final void seekTo( long positionMs, @Player.Command int seekCommand, boolean isRepeatingCurrentItem) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + checkArgument(mediaItemIndex >= 0); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!shouldHandleCommand(seekCommand) + || isPlayingAd() + || (!state.playlist.isEmpty() && mediaItemIndex >= state.playlist.size())) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSeek(mediaItemIndex, positionMs, seekCommand), + /* placeholderStateSupplier= */ () -> + getStateWithNewPlaylistAndPosition(state, state.playlist, mediaItemIndex, positionMs), + /* seeked= */ true, + isRepeatingCurrentItem); } @Override @@ -2614,7 +2714,8 @@ protected final void invalidateState() { if (!pendingOperations.isEmpty() || released) { return; } - updateStateAndInformListeners(getState()); + updateStateAndInformListeners( + getState(), /* seeked= */ false, /* isRepeatingCurrentItem= */ false); } /** @@ -2650,6 +2751,26 @@ protected State getPlaceholderState(State suggestedPlaceholderState) { return suggestedPlaceholderState; } + /** + * Returns the placeholder {@link MediaItemData} used for a new {@link MediaItem} added to the + * playlist. + * + *

      An implementation only needs to override this method if it can determine a more accurate + * placeholder state than the default. + * + * @param mediaItem The {@link MediaItem} added to the playlist. + * @return The {@link MediaItemData} used as placeholder while adding the item to the playlist is + * in progress. + */ + @ForOverride + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return new MediaItemData.Builder(new PlaceholderUid()) + .setMediaItem(mediaItem) + .setIsDynamic(true) + .setIsPlaceholder(true) + .build(); + } + /** * Handles calls to {@link Player#setPlayWhenReady}, {@link Player#play} and {@link Player#pause}. * @@ -2874,6 +2995,101 @@ protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutpu throw new IllegalStateException(); } + /** + * Handles calls to {@link Player#setMediaItem} and {@link Player#setMediaItems}. + * + *

      Will only be called if {@link Player#COMMAND_SET_MEDIA_ITEM} or {@link + * Player#COMMAND_CHANGE_MEDIA_ITEMS} is available. If only {@link Player#COMMAND_SET_MEDIA_ITEM} + * is available, the list of media items will always contain exactly one item. + * + * @param mediaItems The media items to add. + * @param startIndex The index at which to start playback from, or {@link C#INDEX_UNSET} to start + * at the default item. + * @param startPositionMs The position in milliseconds to start playback from, or {@link + * C#TIME_UNSET} to start at the default position in the media item. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#addMediaItem} and {@link Player#addMediaItems}. + * + *

      Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available. + * + * @param index The index at which to add the items. The index is in the range 0 <= {@code + * index} <= {@link #getMediaItemCount()}. + * @param mediaItems The media items to add. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#moveMediaItem} and {@link Player#moveMediaItems}. + * + *

      Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available. + * + * @param fromIndex The start index of the items to move. The index is in the range 0 <= {@code + * fromIndex} < {@link #getMediaItemCount()}. + * @param toIndex The index of the first item not to be included in the move (exclusive). The + * index is in the range {@code fromIndex} < {@code toIndex} <= {@link + * #getMediaItemCount()}. + * @param newIndex The new index of the first moved item. The index is in the range {@code 0} + * <= {@code newIndex} < {@link #getMediaItemCount() - (toIndex - fromIndex)}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleMoveMediaItems(int fromIndex, int toIndex, int newIndex) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#removeMediaItem} and {@link Player#removeMediaItems}. + * + *

      Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available. + * + * @param fromIndex The index at which to start removing media items. The index is in the range 0 + * <= {@code fromIndex} < {@link #getMediaItemCount()}. + * @param toIndex The index of the first item to be kept (exclusive). The index is in the range + * {@code fromIndex} < {@code toIndex} <= {@link #getMediaItemCount()}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#seekTo} and other seek operations (for example, {@link + * Player#seekToNext}). + * + *

      Will only be called if the appropriate {@link Player.Command}, for example {@link + * Player#COMMAND_SEEK_TO_MEDIA_ITEM} or {@link Player#COMMAND_SEEK_TO_NEXT}, is available. + * + * @param mediaItemIndex The media item index to seek to. The index is in the range 0 <= {@code + * mediaItemIndex} < {@code mediaItems.size()}. + * @param positionMs The position in milliseconds to start playback from, or {@link C#TIME_UNSET} + * to start at the default position in the media item. + * @param seekCommand The {@link Player.Command} used to trigger the seek. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + throw new IllegalStateException(); + } + @RequiresNonNull("state") private boolean shouldHandleCommand(@Player.Command int commandCode) { return !released && state.availableCommands.contains(commandCode); @@ -2881,7 +3097,8 @@ private boolean shouldHandleCommand(@Player.Command int commandCode) { @SuppressWarnings("deprecation") // Calling deprecated listener methods. @RequiresNonNull("state") - private void updateStateAndInformListeners(State newState) { + private void updateStateAndInformListeners( + State newState, boolean seeked, boolean isRepeatingCurrentItem) { State previousState = state; // Assign new state immediately such that all getters return the right values, but use a // snapshot of the previous and new state so that listener invocations are triggered correctly. @@ -2903,10 +3120,11 @@ private void updateStateAndInformListeners(State newState) { MediaMetadata previousMediaMetadata = getMediaMetadataInternal(previousState); MediaMetadata newMediaMetadata = getMediaMetadataInternal(newState); int positionDiscontinuityReason = - getPositionDiscontinuityReason(previousState, newState, window, period); + getPositionDiscontinuityReason(previousState, newState, seeked, window, period); boolean timelineChanged = !previousState.timeline.equals(newState.timeline); int mediaItemTransitionReason = - getMediaItemTransitionReason(previousState, newState, positionDiscontinuityReason, window); + getMediaItemTransitionReason( + previousState, newState, positionDiscontinuityReason, isRepeatingCurrentItem, window); if (timelineChanged) { @Player.TimelineChangeReason @@ -3090,7 +3308,7 @@ private void updateStateAndInformListeners(State newState) { listeners.queueEvent( Player.EVENT_METADATA, listener -> listener.onMetadata(newState.timedMetadata)); } - if (false /* TODO: add flag to know when a seek request has been resolved */) { + if (positionDiscontinuityReason == Player.DISCONTINUITY_REASON_SEEK) { listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onSeekProcessed); } if (!previousState.availableCommands.equals(newState.availableCommands)) { @@ -3122,18 +3340,33 @@ private void verifyApplicationThreadAndInitState() { @RequiresNonNull("state") private void updateStateForPendingOperation( ListenableFuture pendingOperation, Supplier placeholderStateSupplier) { + updateStateForPendingOperation( + pendingOperation, + placeholderStateSupplier, + /* seeked= */ false, + /* isRepeatingCurrentItem= */ false); + } + + @RequiresNonNull("state") + private void updateStateForPendingOperation( + ListenableFuture pendingOperation, + Supplier placeholderStateSupplier, + boolean seeked, + boolean isRepeatingCurrentItem) { if (pendingOperation.isDone() && pendingOperations.isEmpty()) { - updateStateAndInformListeners(getState()); + updateStateAndInformListeners(getState(), seeked, isRepeatingCurrentItem); } else { pendingOperations.add(pendingOperation); State suggestedPlaceholderState = placeholderStateSupplier.get(); - updateStateAndInformListeners(getPlaceholderState(suggestedPlaceholderState)); + updateStateAndInformListeners( + getPlaceholderState(suggestedPlaceholderState), seeked, isRepeatingCurrentItem); pendingOperation.addListener( () -> { castNonNull(state); // Already checked by method @RequiresNonNull pre-condition. pendingOperations.remove(pendingOperation); if (pendingOperations.isEmpty() && !released) { - updateStateAndInformListeners(getState()); + updateStateAndInformListeners( + getState(), /* seeked= */ false, /* isRepeatingCurrentItem= */ false); } }, this::postOrRunOnApplicationHandler); @@ -3218,7 +3451,11 @@ private static int getPeriodIndexFromWindowPosition( return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; } for (int i = 0; i < previousPlaylist.size(); i++) { - if (!previousPlaylist.get(i).uid.equals(newPlaylist.get(i).uid)) { + Object previousUid = previousPlaylist.get(i).uid; + Object newUid = newPlaylist.get(i).uid; + boolean resolvedAutoGeneratedPlaceholder = + previousUid instanceof PlaceholderUid && !(newUid instanceof PlaceholderUid); + if (!previousUid.equals(newUid) && !resolvedAutoGeneratedPlaceholder) { return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; } } @@ -3226,11 +3463,18 @@ private static int getPeriodIndexFromWindowPosition( } private static int getPositionDiscontinuityReason( - State previousState, State newState, Timeline.Window window, Timeline.Period period) { + State previousState, + State newState, + boolean seeked, + Timeline.Window window, + Timeline.Period period) { if (newState.hasPositionDiscontinuity) { // We were asked to report a discontinuity. return newState.positionDiscontinuityReason; } + if (seeked) { + return Player.DISCONTINUITY_REASON_SEEK; + } if (previousState.playlist.isEmpty()) { // First change from an empty playlist is not reported as a discontinuity. return C.INDEX_UNSET; @@ -3244,6 +3488,10 @@ private static int getPositionDiscontinuityReason( getCurrentPeriodIndexInternal(previousState, window, period)); Object newPeriodUid = newState.timeline.getUidOfPeriod(getCurrentPeriodIndexInternal(newState, window, period)); + if (previousPeriodUid instanceof PlaceholderUid && !(newPeriodUid instanceof PlaceholderUid)) { + // An auto-generated placeholder was resolved to a real item. + return C.INDEX_UNSET; + } if (!newPeriodUid.equals(previousPeriodUid) || previousState.currentAdGroupIndex != newState.currentAdGroupIndex || previousState.currentAdIndexInAdGroup != newState.currentAdIndexInAdGroup) { @@ -3340,6 +3588,7 @@ private static int getMediaItemTransitionReason( State previousState, State newState, int positionDiscontinuityReason, + boolean isRepeatingCurrentItem, Timeline.Window window) { Timeline previousTimeline = previousState.timeline; Timeline newTimeline = newState.timeline; @@ -3353,6 +3602,10 @@ private static int getMediaItemTransitionReason( .uid; Object newWindowUid = newState.timeline.getWindow(getCurrentMediaItemIndexInternal(newState), window).uid; + if (previousWindowUid instanceof PlaceholderUid && !(newWindowUid instanceof PlaceholderUid)) { + // An auto-generated placeholder was resolved to a real item. + return C.INDEX_UNSET; + } if (!previousWindowUid.equals(newWindowUid)) { if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) { return MEDIA_ITEM_TRANSITION_REASON_AUTO; @@ -3368,8 +3621,7 @@ private static int getMediaItemTransitionReason( && getContentPositionMsInternal(previousState) > getContentPositionMsInternal(newState)) { return MEDIA_ITEM_TRANSITION_REASON_REPEAT; } - if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK - && /* TODO: mark repetition seeks to detect this case */ false) { + if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK && isRepeatingCurrentItem) { return MEDIA_ITEM_TRANSITION_REASON_SEEK; } return C.INDEX_UNSET; @@ -3382,4 +3634,139 @@ private static Size getSurfaceHolderSize(SurfaceHolder surfaceHolder) { Rect surfaceFrame = surfaceHolder.getSurfaceFrame(); return new Size(surfaceFrame.width(), surfaceFrame.height()); } + + private static int getMediaItemIndexInNewPlaylist( + List oldPlaylist, + Timeline newPlaylistTimeline, + int oldMediaItemIndex, + Timeline.Period period) { + if (oldPlaylist.isEmpty()) { + return oldMediaItemIndex < newPlaylistTimeline.getWindowCount() + ? oldMediaItemIndex + : C.INDEX_UNSET; + } + Object oldFirstPeriodUid = + oldPlaylist.get(oldMediaItemIndex).getPeriodUid(/* periodIndexInMediaItem= */ 0); + if (newPlaylistTimeline.getIndexOfPeriod(oldFirstPeriodUid) == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + return newPlaylistTimeline.getPeriodByUid(oldFirstPeriodUid, period).windowIndex; + } + + private static State getStateWithNewPlaylist( + State oldState, List newPlaylist, Timeline.Period period) { + State.Builder stateBuilder = oldState.buildUpon(); + stateBuilder.setPlaylist(newPlaylist); + Timeline newTimeline = stateBuilder.timeline; + long oldPositionMs = oldState.contentPositionMsSupplier.get(); + int oldIndex = getCurrentMediaItemIndexInternal(oldState); + int newIndex = getMediaItemIndexInNewPlaylist(oldState.playlist, newTimeline, oldIndex, period); + long newPositionMs = newIndex == C.INDEX_UNSET ? C.TIME_UNSET : oldPositionMs; + // If the current item no longer exists, try to find a matching subsequent item. + for (int i = oldIndex + 1; newIndex == C.INDEX_UNSET && i < oldState.playlist.size(); i++) { + // TODO: Use shuffle order to iterate. + newIndex = + getMediaItemIndexInNewPlaylist( + oldState.playlist, newTimeline, /* oldMediaItemIndex= */ i, period); + } + // If this fails, transition to ENDED state. + if (oldState.playbackState != Player.STATE_IDLE && newIndex == C.INDEX_UNSET) { + stateBuilder.setPlaybackState(Player.STATE_ENDED).setIsLoading(false); + } + return buildStateForNewPosition( + stateBuilder, + oldState, + oldPositionMs, + newPlaylist, + newIndex, + newPositionMs, + /* keepAds= */ true); + } + + private static State getStateWithNewPlaylistAndPosition( + State oldState, List newPlaylist, int newIndex, long newPositionMs) { + State.Builder stateBuilder = oldState.buildUpon(); + stateBuilder.setPlaylist(newPlaylist); + if (oldState.playbackState != Player.STATE_IDLE) { + if (newPlaylist.isEmpty()) { + stateBuilder.setPlaybackState(Player.STATE_ENDED).setIsLoading(false); + } else { + stateBuilder.setPlaybackState(Player.STATE_BUFFERING); + } + } + long oldPositionMs = oldState.contentPositionMsSupplier.get(); + return buildStateForNewPosition( + stateBuilder, + oldState, + oldPositionMs, + newPlaylist, + newIndex, + newPositionMs, + /* keepAds= */ false); + } + + private static State buildStateForNewPosition( + State.Builder stateBuilder, + State oldState, + long oldPositionMs, + List newPlaylist, + int newIndex, + long newPositionMs, + boolean keepAds) { + // Resolve unset or invalid index and position. + oldPositionMs = getPositionOrDefaultInMediaItem(oldPositionMs, oldState); + if (!newPlaylist.isEmpty() && (newIndex == C.INDEX_UNSET || newIndex >= newPlaylist.size())) { + newIndex = 0; // TODO: Use shuffle order to get first index. + newPositionMs = C.TIME_UNSET; + } + if (!newPlaylist.isEmpty() && newPositionMs == C.TIME_UNSET) { + newPositionMs = usToMs(newPlaylist.get(newIndex).defaultPositionUs); + } + boolean oldOrNewPlaylistEmpty = oldState.playlist.isEmpty() || newPlaylist.isEmpty(); + boolean mediaItemChanged = + !oldOrNewPlaylistEmpty + && !oldState + .playlist + .get(getCurrentMediaItemIndexInternal(oldState)) + .uid + .equals(newPlaylist.get(newIndex).uid); + if (oldOrNewPlaylistEmpty || mediaItemChanged || newPositionMs < oldPositionMs) { + // New item or seeking back. Assume no buffer and no ad playback persists. + stateBuilder + .setCurrentMediaItemIndex(newIndex) + .setCurrentAd(C.INDEX_UNSET, C.INDEX_UNSET) + .setContentPositionMs(newPositionMs) + .setContentBufferedPositionMs(PositionSupplier.getConstant(newPositionMs)) + .setTotalBufferedDurationMs(PositionSupplier.ZERO); + } else if (newPositionMs == oldPositionMs) { + // Unchanged position. Assume ad playback and buffer in current item persists. + stateBuilder.setCurrentMediaItemIndex(newIndex); + if (oldState.currentAdGroupIndex != C.INDEX_UNSET && keepAds) { + stateBuilder.setTotalBufferedDurationMs( + PositionSupplier.getConstant( + oldState.adBufferedPositionMsSupplier.get() - oldState.adPositionMsSupplier.get())); + } else { + stateBuilder + .setCurrentAd(C.INDEX_UNSET, C.INDEX_UNSET) + .setTotalBufferedDurationMs( + PositionSupplier.getConstant( + getContentBufferedPositionMsInternal(oldState) - oldPositionMs)); + } + } else { + // Seeking forward. Assume remaining buffer in current item persist, but no ad playback. + long contentBufferedDurationMs = + max(getContentBufferedPositionMsInternal(oldState), newPositionMs); + long totalBufferedDurationMs = + max(0, oldState.totalBufferedDurationMsSupplier.get() - (newPositionMs - oldPositionMs)); + stateBuilder + .setCurrentMediaItemIndex(newIndex) + .setCurrentAd(C.INDEX_UNSET, C.INDEX_UNSET) + .setContentPositionMs(newPositionMs) + .setContentBufferedPositionMs(PositionSupplier.getConstant(contentBufferedDurationMs)) + .setTotalBufferedDurationMs(PositionSupplier.getConstant(totalBufferedDurationMs)); + } + return stateBuilder.build(); + } + + private static final class PlaceholderUid {} } diff --git a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java index 2b2d6fb4ac1..a78d2a9c589 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -49,7 +50,9 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.Ignore; import org.junit.Test; @@ -1393,6 +1396,7 @@ protected State getState() { /* adIndexInAdGroup= */ C.INDEX_UNSET), Player.DISCONTINUITY_REASON_SEEK); verify(listener).onMediaItemTransition(mediaItem1, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onSeekProcessed(); verify(listener) .onEvents( player, @@ -1432,9 +1436,6 @@ protected State getState() { verifyNoMoreInteractions(listener); // Assert that we actually called all listeners. for (Method method : Player.Listener.class.getDeclaredMethods()) { - if (method.getName().equals("onSeekProcessed")) { - continue; - } if (method.getName().equals("onAudioSessionIdChanged") || method.getName().equals("onSkipSilenceEnabledChanged")) { // Skip listeners for ExoPlayer-specific states @@ -3810,6 +3811,3545 @@ protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutpu assertThat(callForwarded.get()).isFalse(); } + @Test + public void addMediaItems_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.addMediaItems( + /* index= */ 1, + ImmutableList.of( + new MediaItem.Builder().setMediaId("3").build(), + new MediaItem.Builder().setMediaId("4").build())); + + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void addMediaItems_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(3) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.addMediaItems( + /* index= */ 1, + ImmutableList.of( + new MediaItem.Builder().setMediaId("3").build(), + new MediaItem.Builder().setMediaId("4").build())); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(3); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(4); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(1); + assertThat(window.isPlaceholder).isFalse(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window); + assertThat(window.mediaItem.mediaId).isEqualTo("4"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 3, window); + assertThat(window.uid).isEqualTo(2); + assertThat(window.isPlaceholder).isFalse(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(3); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + addMediaItems_asyncHandlingWhileAdIsPlaying_usesPlaceholderStateAndInformsListeners() { + SimpleBasePlayer.PeriodData adPeriodData = + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 123)) + .build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setPeriods(ImmutableList.of(adPeriodData)) + .build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setPeriods(ImmutableList.of(adPeriodData)) + .build())) + .setCurrentMediaItemIndex(1) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.addMediaItem(/* index= */ 0, new MediaItem.Builder().setMediaId("id").build()); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("id"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(1); + assertThat(window.isPlaceholder).isFalse(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void addMediaItems_asyncHandlingFromEmpty_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.addMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build())); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + addMediaItems_asyncHandlingFromEmptyWithPreviouslySetPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.addMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem)); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + addMediaItems_asyncHandlingFromEmptyWithPreviouslySetPositionExceedingNewPlaylistSize_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(5000) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(1000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(1_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.addMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build())); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + addMediaItems_asyncHandlingFromEmptyWithPreviouslySetIndexAndDefaultPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.addMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem)); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void addMediaItems_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.addMediaItem(new MediaItem.Builder().setMediaId("id").build()); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void addMediaItems_withInvalidIndex_addsToEndOfPlaylist() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .build(); + AtomicInteger indexInHandleMethod = new AtomicInteger(C.INDEX_UNSET); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + indexInHandleMethod.set(index); + return SettableFuture.create(); + } + }; + + player.addMediaItem(/* index= */ 5000, new MediaItem.Builder().setMediaId("new").build()); + + assertThat(indexInHandleMethod.get()).isEqualTo(1); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(1); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("new"); + } + + @Test + public void moveMediaItems_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build())) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .setCurrentMediaItemIndex(2) + .build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleMoveMediaItems( + int fromIndex, int toIndex, int newIndex) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0); + + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void moveMediaItems_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build())) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .setCurrentMediaItemIndex(2) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleMoveMediaItems( + int fromIndex, int toIndex, int newIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(2); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(3); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(2); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(3); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window); + assertThat(window.uid).isEqualTo(1); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(2); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void + moveMediaItems_asyncHandlingWhileAdIsPlaying_usesPlaceholderStateAndInformsListeners() { + SimpleBasePlayer.PeriodData adPeriodData = + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 123)) + .build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setPeriods(ImmutableList.of(adPeriodData)) + .build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setPeriods(ImmutableList.of(adPeriodData)) + .build())) + .setCurrentMediaItemIndex(2) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleMoveMediaItems( + int fromIndex, int toIndex, int newIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(2); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(3); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(2); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(3); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window); + assertThat(window.uid).isEqualTo(1); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(2); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void moveMediaItems_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleMoveMediaItems( + int fromIndex, int toIndex, int newIndex) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, /* newIndex= */ 0); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void moveMediaItems_withInvalidIndices_usesValidIndexRange() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build())) + .build(); + AtomicInteger fromIndexInHandleMethod = new AtomicInteger(C.INDEX_UNSET); + AtomicInteger toIndexInHandleMethod = new AtomicInteger(C.INDEX_UNSET); + AtomicInteger newIndexInHandleMethod = new AtomicInteger(C.INDEX_UNSET); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleMoveMediaItems( + int fromIndex, int toIndex, int newIndex) { + fromIndexInHandleMethod.set(fromIndex); + toIndexInHandleMethod.set(toIndex); + newIndexInHandleMethod.set(newIndex); + return SettableFuture.create(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2500, /* newIndex= */ 0); + assertThat(fromIndexInHandleMethod.get()).isEqualTo(1); + assertThat(toIndexInHandleMethod.get()).isEqualTo(3); + assertThat(newIndexInHandleMethod.get()).isEqualTo(0); + + player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 6000); + assertThat(fromIndexInHandleMethod.get()).isEqualTo(0); + assertThat(toIndexInHandleMethod.get()).isEqualTo(2); + assertThat(newIndexInHandleMethod.get()).isEqualTo(1); + + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(3); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(1); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(2); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window); + assertThat(window.uid).isEqualTo(3); + verify(listener, times(2)) + .onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); + verifyNoMoreInteractions(listener); + } + + @Test + public void removeMediaItems_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build())) + .setCurrentMediaItemIndex(3) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build())) + .setCurrentMediaItemIndex(1) + .build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3); + + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void removeMediaItems_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build())) + .setCurrentMediaItemIndex(3) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 5).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build())) + .setCurrentMediaItemIndex(1) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(1); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(4); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void + removeMediaItems_asyncHandlingWhileAdIsPlaying_usesPlaceholderStateAndInformsListeners() { + SimpleBasePlayer.PeriodData adPeriodData = + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 123)) + .build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4) + .setPeriods(ImmutableList.of(adPeriodData)) + .build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .setCurrentMediaItemIndex(3) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 5).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4) + .setPeriods(ImmutableList.of(adPeriodData)) + .build())) + .setCurrentMediaItemIndex(1) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(1); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(4); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Testing deprecated listener call. + @Test + public void + removeMediaItems_asyncHandlingRemovingCurrentItemWithSubsequentMatch_usesPlaceholderStateAndInformsListeners() { + MediaItem lastMediaItem = new MediaItem.Builder().setMediaId("id").build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4) + .setMediaItem(lastMediaItem) + .build())) + .setCurrentMediaItemIndex(1) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 5).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4) + .setMediaItem(lastMediaItem) + .build())) + .setCurrentMediaItemIndex(1) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(1); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(4); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener) + .onMediaItemTransition(lastMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Testing deprecated listener call. + @Test + public void + removeMediaItems_asyncHandlingRemovingCurrentItemWithoutSubsequentMatch_usesPlaceholderStateAndInformsListeners() { + MediaItem firstMediaItem = new MediaItem.Builder().setMediaId("id").build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setMediaItem(firstMediaItem) + .build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build())) + .setCurrentMediaItemIndex(1) + .setPlaybackState(Player.STATE_READY) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setMediaItem(firstMediaItem) + .build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 5).build())) + .setCurrentMediaItemIndex(0) + .setPlaybackState(Player.STATE_ENDED) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(1); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(1); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener) + .onMediaItemTransition( + firstMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener).onPlaybackStateChanged(Player.STATE_ENDED); + verify(listener).onPlayerStateChanged(/* playWhenReady= */ false, Player.STATE_ENDED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Testing deprecated listener call. + @Test + public void + removeMediaItems_asyncHandlingRemovingEntirePlaylist_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build())) + .setCurrentMediaItemIndex(1) + .setPlaybackState(Player.STATE_READY) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist(ImmutableList.of()) + .setCurrentMediaItemIndex(C.INDEX_UNSET) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setPlaybackState(Player.STATE_ENDED) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.clearMediaItems(); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener) + .onMediaItemTransition( + /* mediaItem= */ null, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener).onPlaybackStateChanged(Player.STATE_ENDED); + verify(listener).onPlayerStateChanged(/* playWhenReady= */ false, Player.STATE_ENDED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verifyNoMoreInteractions(listener); + } + + @Test + public void removeMediaItems_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.removeMediaItem(/* index= */ 0); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void removeMediaItems_withInvalidIndex_removesToEndOfPlaylist() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicInteger fromIndexInHandleMethod = new AtomicInteger(C.INDEX_UNSET); + AtomicInteger toIndexInHandleMethod = new AtomicInteger(C.INDEX_UNSET); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + fromIndexInHandleMethod.set(fromIndex); + toIndexInHandleMethod.set(toIndex); + return SettableFuture.create(); + } + }; + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 5000); + + assertThat(fromIndexInHandleMethod.get()).isEqualTo(1); + assertThat(toIndexInHandleMethod.get()).isEqualTo(2); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(1); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(1); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void setMediaItems_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("new").build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3) + .setMediaItem(newMediaItem) + .build())) + .setCurrentMediaItemIndex(1) + .build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems( + ImmutableList.of( + new MediaItem.Builder().setMediaId("2").build(), + new MediaItem.Builder().setMediaId("3").build())); + + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + setMediaItems_asyncHandlingWithIndexAndPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem), + /* startIndex= */ 1, + /* startPositionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithIndexAndPositionFromEmpty_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("1").build(), newMediaItem), + /* startIndex= */ 1, + /* startPositionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("1"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithIndexAndDefaultPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(3_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("1").build(), newMediaItem), + /* startIndex= */ 1, + /* startPositionMs= */ C.TIME_UNSET); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("1"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + setMediaItems_asyncHandlingWithEmptyPlaylistAndIndexAndPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist(ImmutableList.of()) + .setCurrentMediaItemIndex(20) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* startIndex= */ 20, /* startPositionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(20); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verify(listener) + .onTimelineChanged(Timeline.EMPTY, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener) + .onMediaItemTransition( + /* mediaItem= */ null, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(20); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + setMediaItems_asyncHandlingWithEmptyPlaylistAndIndexAndDefaultPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + State updatedState = + state.buildUpon().setPlaylist(ImmutableList.of()).setCurrentMediaItemIndex(20).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems( + ImmutableList.of(), /* startIndex= */ 20, /* startPositionMs= */ C.TIME_UNSET); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(20); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verify(listener) + .onTimelineChanged(Timeline.EMPTY, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener) + .onMediaItemTransition( + /* mediaItem= */ null, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(20); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void setMediaItems_asyncHandlingWithResetTrue_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.setMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build()), + /* resetPosition= */ true); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetTrueFromEmpty_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.setMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build()), + /* resetPosition= */ true); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + setMediaItems_asyncHandlingWithResetTrueToEmpty_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist(ImmutableList.of()) + .setCurrentMediaItemIndex(C.INDEX_UNSET) + .setContentPositionMs(C.TIME_UNSET) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ true); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verify(listener) + .onTimelineChanged(Timeline.EMPTY, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition( + /* mediaItem= */ null, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetTrueFromEmptyToEmpty_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setCurrentMediaItemIndex(C.INDEX_UNSET) + .setContentPositionMs(C.TIME_UNSET) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ true); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void setMediaItems_asyncHandlingWithResetFalse_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem), + /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyWithSetPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem), + /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyWithSetPositionExceedingPlaylistSize_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(5000) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(1000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(1_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.setMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build()), + /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyWithIndexAndDefaultPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem), + /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyWithDefaultIndexAndPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.setMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build()), + /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("2"); + assertThat(window.isPlaceholder).isTrue(); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + setMediaItems_asyncHandlingWithResetFalseToEmpty_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist(ImmutableList.of()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verify(listener) + .onTimelineChanged(Timeline.EMPTY, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition( + /* mediaItem= */ null, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyToEmptyWithSetPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyToEmptyWithIndexAndDefaultPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyToEmptyWithDefaultIndexAndPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @Test + public void setMediaItems_withoutAvailableCommandForEmptyPlaylist_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setMediaItems(ImmutableList.of()); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setMediaItems_withoutAvailableCommandForSingleItemPlaylist_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .removeAll(Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_SET_MEDIA_ITEM) + .build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setMediaItems(ImmutableList.of(new MediaItem.Builder().setMediaId("new").build())); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setMediaItems_withJustSetMediaItemCommandForSingleItemPlaylist_isForwarded() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().add(Player.COMMAND_SET_MEDIA_ITEM).build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setMediaItems(ImmutableList.of(new MediaItem.Builder().setMediaId("new").build())); + + assertThat(callForwarded.get()).isTrue(); + } + + @Test + public void setMediaItems_withJustChangeMediaItemsCommandForSingleItemPlaylist_isForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_CHANGE_MEDIA_ITEMS).build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setMediaItems(ImmutableList.of(new MediaItem.Builder().setMediaId("new").build())); + + assertThat(callForwarded.get()).isTrue(); + } + + @Test + public void setMediaItems_withoutAvailableCommandForMultiItemPlaylist_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setMediaItems( + ImmutableList.of( + new MediaItem.Builder().setMediaId("1").build(), + new MediaItem.Builder().setMediaId("2").build())); + + assertThat(callForwarded.get()).isFalse(); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void seekTo_immediateHandling_updatesStateAndInformsListeners() { + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2) + .setMediaItem(newMediaItem) + .build())) + .build(); + State updatedState = + state.buildUpon().setCurrentMediaItemIndex(1).setContentPositionMs(3000).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000); + + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void seekTo_asyncHandlingWithIndexAndPosition_usesPlaceholderStateAndInformsListeners() { + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2) + .setMediaItem(newMediaItem) + .build())) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state.buildUpon().setCurrentMediaItemIndex(1).setContentPositionMs(3005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getBufferedPosition()).isEqualTo(3000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithIndexAndDefaultPosition_usesPlaceholderStateAndInformsListeners() { + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2) + .setMediaItem(newMediaItem) + .setDefaultPositionUs(3_000_000) + .build())) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state.buildUpon().setCurrentMediaItemIndex(1).setContentPositionMs(3005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getBufferedPosition()).isEqualTo(3000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithIndexAndPositionAndEmptyPlaylist_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist(ImmutableList.of()) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state.buildUpon().setCurrentMediaItemIndex(1).setContentPositionMs(3005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getBufferedPosition()).isEqualTo(3000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithIndexAndDefaultPositionAndEmptyPlaylist_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist(ImmutableList.of()) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state.buildUpon().setCurrentMediaItemIndex(1).setContentPositionMs(100).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getBufferedPosition()).isEqualTo(0); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(100); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithSeekBackInCurrentItem_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = state.buildUpon().setContentPositionMs(3005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getBufferedPosition()).isEqualTo(3000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(3005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithSeekToCurrentPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .setContentPositionMs(3000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(7000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = state.buildUpon().setContentPositionMs(3005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getBufferedPosition()).isEqualTo(10000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(7000); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(3005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithSeekForwardInCurrentItem_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .setContentPositionMs(3000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(7000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = state.buildUpon().setContentPositionMs(7005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* positionMs= */ 7000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(7000); + assertThat(player.getBufferedPosition()).isEqualTo(10000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(3000); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(7005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithRepeatOfCurrentItem_usesPlaceholderStateAndInformsListeners() { + MediaItem mediaItem = new MediaItem.Builder().setMediaId("id").build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setMediaItem(mediaItem) + .build())) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = state.buildUpon().setContentPositionMs(5).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekToNext(); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getBufferedPosition()).isEqualTo(0); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onMediaItemTransition(mediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5); + verifyNoMoreInteractions(listener); + } + + @Test + public void seekTo_withoutAvailableCommandForSeekToMediaItem_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_MEDIA_ITEM) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 4000); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekTo_withoutAvailableCommandForSeekInCurrentMediaItem_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekTo(/* positionMs= */ 4000); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekToDefaultPosition_withoutAvailableCommandForSeekToMediaItem_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_MEDIA_ITEM) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToDefaultPosition(/* mediaItemIndex= */ 1); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void + seekToDefaultPosition_withoutAvailableCommandForSeekToDefaultPosition_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_DEFAULT_POSITION) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToDefaultPosition(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekBack_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_SEEK_BACK).build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekBack(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekToPrevious_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToPrevious(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekToPreviousMediaItem_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToPreviousMediaItem(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekForward_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_SEEK_FORWARD).build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekForward(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekToNext_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_SEEK_TO_NEXT).build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToNext(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekToNextMediaItem_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToNextMediaItem(); + + assertThat(callForwarded.get()).isFalse(); + } + private static Object[] getAnyArguments(Method method) { Object[] arguments = new Object[method.getParameterCount()]; Class[] argumentTypes = method.getParameterTypes(); From 1126bbb4bce0f5a38d840a64bfbc3562bf7458ba Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 19 Dec 2022 13:42:29 +0000 Subject: [PATCH 076/141] Remove ellipsis from Player javadoc PiperOrigin-RevId: 496377192 (cherry picked from commit f0696f95720418d3c95a72f1454f712a40e40b8d) --- .../common/src/main/java/androidx/media3/common/Player.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index be64212d219..089bb3c0ae9 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -2094,7 +2094,7 @@ default void onMetadata(Metadata metadata) {} * setPlaybackParameters(getPlaybackParameters().withSpeed(speed))}. * * @param speed The linear factor by which playback will be sped up. Must be higher than 0. 1 is - * normal speed, 2 is twice as fast, 0.5 is half normal speed... + * normal speed, 2 is twice as fast, 0.5 is half normal speed. */ void setPlaybackSpeed(@FloatRange(from = 0, fromInclusive = false) float speed); From 79bb53a1832977041c5e0f3c4acb4570274a75f8 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 19 Dec 2022 13:53:30 +0000 Subject: [PATCH 077/141] Fix Dackka error due to param name mismatch https://developer.android.com/reference/androidx/leanback/media/PlayerAdapter#seekTo(long) #minor-release PiperOrigin-RevId: 496378709 (cherry picked from commit aae6941981dfcfcdd46544f585335ff26d8f81e9) --- .../androidx/media3/ui/leanback/LeanbackPlayerAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java b/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java index 84a8c9eb75b..51bc101b0d6 100644 --- a/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java +++ b/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java @@ -158,8 +158,8 @@ public void pause() { } @Override - public void seekTo(long positionMs) { - player.seekTo(player.getCurrentMediaItemIndex(), positionMs); + public void seekTo(long positionInMs) { + player.seekTo(player.getCurrentMediaItemIndex(), positionInMs); } @Override From fdc59304e617ff5db1b4a9c747d8946dcf3e4421 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 19 Dec 2022 15:53:42 +0000 Subject: [PATCH 078/141] Remove TODO from `ControllerInfo` - the existing approach is fine PiperOrigin-RevId: 496398934 (cherry picked from commit 14947539e53143e84f4453505a403fbe3625af5d) --- .../src/main/java/androidx/media3/session/MediaSession.java | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 2bf05e7a272..d03f24e0bff 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -504,7 +504,6 @@ public String toString() { /* connectionHints= */ Bundle.EMPTY); } - // TODO(b/259546357): Remove when ControllerInfo can be instantiated cleanly in tests. /** Returns a {@link ControllerInfo} suitable for use when testing client code. */ @VisibleForTesting(otherwise = PRIVATE) public static ControllerInfo createTestOnlyControllerInfo( From 9c81f3b01158846912761f82cb734e4b7a16d2d6 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Mon, 19 Dec 2022 17:43:50 +0000 Subject: [PATCH 079/141] Add BitmapLoader injection in MediaController Also clean up the strict mode violations of using `BitmapFactory.convertToByteArray` on the main thread. PiperOrigin-RevId: 496422355 (cherry picked from commit d848d3358a67ce2439db7cf170eec7b8c3ecffbf) --- .../androidx/media3/session/MediaBrowser.java | 42 +++++++++-- .../session/MediaBrowserImplLegacy.java | 5 +- .../media3/session/MediaController.java | 34 +++++++-- .../session/MediaControllerImplLegacy.java | 67 ++++++++++++++--- .../androidx/media3/session/MediaUtils.java | 18 ----- ...CompatCallbackWithMediaControllerTest.java | 74 ++++++++++++++++++- 6 files changed, 200 insertions(+), 40 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java b/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java index 2ee3be9c966..ffe84bb11e5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java @@ -32,6 +32,7 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.common.util.Consumer; +import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.session.MediaLibraryService.LibraryParams; import com.google.common.collect.ImmutableList; @@ -57,6 +58,7 @@ public static final class Builder { private Bundle connectionHints; private Listener listener; private Looper applicationLooper; + private @MonotonicNonNull BitmapLoader bitmapLoader; /** * Creates a builder for {@link MediaBrowser}. @@ -121,6 +123,21 @@ public Builder setApplicationLooper(Looper looper) { return this; } + /** + * Sets a {@link BitmapLoader} for the {@link MediaBrowser} to decode bitmaps from compressed + * binary data. If not set, a {@link CacheBitmapLoader} that wraps a {@link SimpleBitmapLoader} + * will be used. + * + * @param bitmapLoader The bitmap loader. + * @return The builder to allow chaining. + */ + @UnstableApi + @CanIgnoreReturnValue + public Builder setBitmapLoader(BitmapLoader bitmapLoader) { + this.bitmapLoader = checkNotNull(bitmapLoader); + return this; + } + /** * Builds a {@link MediaBrowser} asynchronously. * @@ -149,8 +166,12 @@ public Builder setApplicationLooper(Looper looper) { */ public ListenableFuture buildAsync() { MediaControllerHolder holder = new MediaControllerHolder<>(applicationLooper); + if (token.isLegacySession() && bitmapLoader == null) { + bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader()); + } MediaBrowser browser = - new MediaBrowser(context, token, connectionHints, listener, applicationLooper, holder); + new MediaBrowser( + context, token, connectionHints, listener, applicationLooper, holder, bitmapLoader); postOrRun(new Handler(applicationLooper), () -> holder.setController(browser)); return holder; } @@ -215,8 +236,16 @@ default void onSearchResultChanged( Bundle connectionHints, Listener listener, Looper applicationLooper, - ConnectionCallback connectionCallback) { - super(context, token, connectionHints, listener, applicationLooper, connectionCallback); + ConnectionCallback connectionCallback, + @Nullable BitmapLoader bitmapLoader) { + super( + context, + token, + connectionHints, + listener, + applicationLooper, + connectionCallback, + bitmapLoader); } @Override @@ -226,10 +255,13 @@ MediaBrowserImpl createImpl( Context context, SessionToken token, Bundle connectionHints, - Looper applicationLooper) { + Looper applicationLooper, + @Nullable BitmapLoader bitmapLoader) { MediaBrowserImpl impl; if (token.isLegacySession()) { - impl = new MediaBrowserImplLegacy(context, this, token, applicationLooper); + impl = + new MediaBrowserImplLegacy( + context, this, token, applicationLooper, checkNotNull(bitmapLoader)); } else { impl = new MediaBrowserImplBase(context, this, token, connectionHints, applicationLooper); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java index fc924af3d92..8b3fb24eefb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java @@ -57,8 +57,9 @@ Context context, @UnderInitialization MediaBrowser instance, SessionToken token, - Looper applicationLooper) { - super(context, instance, token, applicationLooper); + Looper applicationLooper, + BitmapLoader bitmapLoader) { + super(context, instance, token, applicationLooper, bitmapLoader); this.instance = instance; } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 496e6ea946f..e9855421d68 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -67,6 +67,7 @@ import java.util.concurrent.Future; import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A controller that interacts with a {@link MediaSession}, a {@link MediaSessionService} hosting a @@ -183,6 +184,7 @@ public static final class Builder { private Bundle connectionHints; private Listener listener; private Looper applicationLooper; + private @MonotonicNonNull BitmapLoader bitmapLoader; /** * Creates a builder for {@link MediaController}. @@ -261,6 +263,21 @@ public Builder setApplicationLooper(Looper looper) { return this; } + /** + * Sets a {@link BitmapLoader} for the {@link MediaController} to decode bitmaps from compressed + * binary data. If not set, a {@link CacheBitmapLoader} that wraps a {@link SimpleBitmapLoader} + * will be used. + * + * @param bitmapLoader The bitmap loader. + * @return The builder to allow chaining. + */ + @UnstableApi + @CanIgnoreReturnValue + public Builder setBitmapLoader(BitmapLoader bitmapLoader) { + this.bitmapLoader = checkNotNull(bitmapLoader); + return this; + } + /** * Builds a {@link MediaController} asynchronously. * @@ -290,8 +307,12 @@ public Builder setApplicationLooper(Looper looper) { public ListenableFuture buildAsync() { MediaControllerHolder holder = new MediaControllerHolder<>(applicationLooper); + if (token.isLegacySession() && bitmapLoader == null) { + bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader()); + } MediaController controller = - new MediaController(context, token, connectionHints, listener, applicationLooper, holder); + new MediaController( + context, token, connectionHints, listener, applicationLooper, holder, bitmapLoader); postOrRun(new Handler(applicationLooper), () -> holder.setController(controller)); return holder; } @@ -404,7 +425,8 @@ default void onExtrasChanged(MediaController controller, Bundle extras) {} Bundle connectionHints, Listener listener, Looper applicationLooper, - ConnectionCallback connectionCallback) { + ConnectionCallback connectionCallback, + @Nullable BitmapLoader bitmapLoader) { checkNotNull(context, "context must not be null"); checkNotNull(token, "token must not be null"); @@ -417,7 +439,7 @@ default void onExtrasChanged(MediaController controller, Bundle extras) {} applicationHandler = new Handler(applicationLooper); this.connectionCallback = connectionCallback; - impl = createImpl(context, token, connectionHints, applicationLooper); + impl = createImpl(context, token, connectionHints, applicationLooper, bitmapLoader); impl.connect(); } @@ -427,9 +449,11 @@ MediaControllerImpl createImpl( Context context, SessionToken token, Bundle connectionHints, - Looper applicationLooper) { + Looper applicationLooper, + @Nullable BitmapLoader bitmapLoader) { if (token.isLegacySession()) { - return new MediaControllerImplLegacy(context, this, token, applicationLooper); + return new MediaControllerImplLegacy( + context, this, token, applicationLooper, checkNotNull(bitmapLoader)); } else { return new MediaControllerImplBase(context, this, token, connectionHints, applicationLooper); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index ff8e3c7c9a0..489997beb8b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -25,6 +25,7 @@ import android.app.PendingIntent; import android.content.Context; +import android.graphics.Bitmap; import android.media.AudioManager; import android.os.Bundle; import android.os.Handler; @@ -76,7 +77,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; import org.checkerframework.checker.initialization.qual.UnderInitialization; import org.checkerframework.checker.nullness.compatqual.NullableType; @@ -93,6 +97,7 @@ private final SessionToken token; private final ListenerSet listeners; private final ControllerCompatCallback controllerCompatCallback; + private final BitmapLoader bitmapLoader; @Nullable private MediaControllerCompat controllerCompat; @Nullable private MediaBrowserCompat browserCompat; @@ -106,7 +111,8 @@ public MediaControllerImplLegacy( Context context, @UnderInitialization MediaController instance, SessionToken token, - Looper applicationLooper) { + Looper applicationLooper, + BitmapLoader bitmapLoader) { // Initialize default values. legacyPlayerInfo = new LegacyPlayerInfo(); pendingLegacyPlayerInfo = new LegacyPlayerInfo(); @@ -122,6 +128,7 @@ public MediaControllerImplLegacy( this.instance = instance; controllerCompatCallback = new ControllerCompatCallback(applicationLooper); this.token = token; + this.bitmapLoader = bitmapLoader; } /* package */ MediaController getInstance() { @@ -716,11 +723,7 @@ public void addMediaItems(int index, List mediaItems) { /* mediaItemTransitionReason= */ null); if (isPrepared()) { - for (int i = 0; i < mediaItems.size(); i++) { - MediaItem mediaItem = mediaItems.get(i); - controllerCompat.addQueueItem( - MediaUtils.convertToMediaDescriptionCompat(mediaItem), index + i); - } + addQueueItems(mediaItems, index); } } @@ -1340,15 +1343,61 @@ private void initializeLegacyPlaylist() { } // Add all other items to the playlist if supported. if (getAvailableCommands().contains(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { + List adjustedMediaItems = new ArrayList<>(); for (int i = 0; i < queueTimeline.getWindowCount(); i++) { if (i == currentIndex || queueTimeline.getQueueId(i) != QueueItem.UNKNOWN_ID) { // Skip the current item (added above) and all items already known to the session. continue; } - MediaItem mediaItem = queueTimeline.getWindow(/* windowIndex= */ i, window).mediaItem; - controllerCompat.addQueueItem( - MediaUtils.convertToMediaDescriptionCompat(mediaItem), /* index= */ i); + adjustedMediaItems.add(queueTimeline.getWindow(/* windowIndex= */ i, window).mediaItem); + } + addQueueItems(adjustedMediaItems, /* startIndex= */ 0); + } + } + + private void addQueueItems(List mediaItems, int startIndex) { + List<@NullableType ListenableFuture> bitmapFutures = new ArrayList<>(); + final AtomicInteger resultCount = new AtomicInteger(0); + Runnable handleBitmapFuturesTask = + () -> { + int completedBitmapFutureCount = resultCount.incrementAndGet(); + if (completedBitmapFutureCount == mediaItems.size()) { + handleBitmapFuturesAllCompletedAndAddQueueItems( + bitmapFutures, mediaItems, /* startIndex= */ startIndex); + } + }; + + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem mediaItem = mediaItems.get(i); + MediaMetadata metadata = mediaItem.mediaMetadata; + if (metadata.artworkData == null) { + bitmapFutures.add(null); + handleBitmapFuturesTask.run(); + } else { + ListenableFuture bitmapFuture = bitmapLoader.decodeBitmap(metadata.artworkData); + bitmapFutures.add(bitmapFuture); + bitmapFuture.addListener(handleBitmapFuturesTask, getInstance().applicationHandler::post); + } + } + } + + private void handleBitmapFuturesAllCompletedAndAddQueueItems( + List<@NullableType ListenableFuture> bitmapFutures, + List mediaItems, + int startIndex) { + for (int i = 0; i < bitmapFutures.size(); i++) { + @Nullable ListenableFuture future = bitmapFutures.get(i); + @Nullable Bitmap bitmap = null; + if (future != null) { + try { + bitmap = Futures.getDone(future); + } catch (CancellationException | ExecutionException e) { + Log.d(TAG, "Failed to get bitmap"); + } } + controllerCompat.addQueueItem( + MediaUtils.convertToMediaDescriptionCompat(mediaItems.get(i), bitmap), + /* index= */ startIndex + i); } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 919d5521787..11735dd6ac7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -46,7 +46,6 @@ import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.media.AudioManager; import android.net.Uri; import android.os.Bundle; @@ -311,23 +310,6 @@ public static List truncateListBySize( return result; } - /** - * Converts a {@link MediaItem} to a {@link MediaDescriptionCompat}. - * - * @deprecated Use {@link #convertToMediaDescriptionCompat(MediaItem, Bitmap)} instead. - */ - @Deprecated - public static MediaDescriptionCompat convertToMediaDescriptionCompat(MediaItem item) { - MediaMetadata metadata = item.mediaMetadata; - @Nullable Bitmap artworkBitmap = null; - if (metadata.artworkData != null) { - artworkBitmap = - BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData.length); - } - - return convertToMediaDescriptionCompat(item, artworkBitmap); - } - /** Converts a {@link MediaItem} to a {@link MediaDescriptionCompat} */ public static MediaDescriptionCompat convertToMediaDescriptionCompat( MediaItem item, @Nullable Bitmap artworkBitmap) { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java index 82e4008c4a1..b16847b8a88 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java @@ -37,6 +37,7 @@ import android.support.v4.media.session.PlaybackStateCompat.ShuffleMode; import androidx.media.AudioManagerCompat; import androidx.media.VolumeProviderCompat; +import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; @@ -52,6 +53,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SdkSuppress; import androidx.test.filters.SmallTest; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -327,7 +329,7 @@ public void setPlaybackParameters_withDefault_notifiesOnSetPlaybackSpeedWithDefa @Test public void addMediaItems() throws Exception { int size = 2; - List testList = MediaTestUtils.createMediaItems(size); + List testList = MediaTestUtils.createMediaItemsWithArtworkData(size); List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); @@ -345,6 +347,7 @@ public void addMediaItems() throws Exception { assertThat(sessionCallback.queueIndices.get(i)).isEqualTo(testIndex + i); assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getMediaId()) .isEqualTo(testList.get(i).mediaId); + assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getIconBitmap()).isNotNull(); } } @@ -391,6 +394,75 @@ public void seekToNextMediaItem() throws Exception { assertThat(sessionCallback.onSkipToNextCalled).isTrue(); } + @Test + public void setMediaItems_nonEmptyList_startFromFirstMediaItem() throws Exception { + int size = 3; + List testList = MediaTestUtils.createMediaItemsWithArtworkData(size); + + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackState(PlaybackStateCompat.STATE_PLAYING); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(size); + + controller.setMediaItems(testList); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue(); + assertThat(sessionCallback.mediaId).isEqualTo(testList.get(0).mediaId); + for (int i = 0; i < size - 1; i++) { + assertThat(sessionCallback.queueIndices.get(i)).isEqualTo(i); + assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getMediaId()) + .isEqualTo(testList.get(i + 1).mediaId); + assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getIconBitmap()).isNotNull(); + } + } + + @Test + public void setMediaItems_nonEmptyList_startFromNonFirstMediaItem() throws Exception { + int size = 5; + List testList = MediaTestUtils.createMediaItemsWithArtworkData(size); + + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackState(PlaybackStateCompat.STATE_PLAYING); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(size); + int testStartIndex = 2; + + controller.setMediaItems(testList, testStartIndex, /* startPositionMs= */ C.TIME_UNSET); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue(); + assertThat(sessionCallback.mediaId).isEqualTo(testList.get(testStartIndex).mediaId); + for (int i = 0; i < size - 1; i++) { + assertThat(sessionCallback.queueIndices.get(i)).isEqualTo(i); + int adjustedIndex = (i < testStartIndex) ? i : i + 1; + assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getMediaId()) + .isEqualTo(testList.get(adjustedIndex).mediaId); + assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getIconBitmap()).isNotNull(); + } + } + + @Test + public void setMediaItems_emptyList() throws Exception { + int size = 3; + List testList = MediaTestUtils.createMediaItems(size); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); + + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackState(PlaybackStateCompat.STATE_PLAYING); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(size); + + controller.setMediaItems(ImmutableList.of()); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + for (int i = 0; i < size; i++) { + assertThat(sessionCallback.queueDescriptionListForRemove.get(i).getMediaId()) + .isEqualTo(testList.get(i).mediaId); + } + } + @Test public void setShuffleMode() throws Exception { session.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE); From 16a67a4ce7be7e43837d1d9f3ae9df6954c76280 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 20 Dec 2022 15:56:19 +0000 Subject: [PATCH 080/141] Clarify some Player command and method javadoc #minor-release PiperOrigin-RevId: 496661152 (cherry picked from commit 31e875b7a094963a9ef2a355ab1a4c6d7d3d9687) --- .../java/androidx/media3/common/Player.java | 162 +++++++++++------- 1 file changed, 99 insertions(+), 63 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index 089bb3c0ae9..d974a10a448 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -69,7 +69,7 @@ */ public interface Player { - /** A set of {@link Event events}. */ + /** A set of {@linkplain Event events}. */ final class Events { private final FlagSet flags; @@ -77,7 +77,7 @@ final class Events { /** * Creates an instance. * - * @param flags The {@link FlagSet} containing the {@link Event events}. + * @param flags The {@link FlagSet} containing the {@linkplain Event events}. */ @UnstableApi public Events(FlagSet flags) { @@ -95,10 +95,10 @@ public boolean contains(@Event int event) { } /** - * Returns whether any of the given {@link Event events} occurred. + * Returns whether any of the given {@linkplain Event events} occurred. * - * @param events The {@link Event events}. - * @return Whether any of the {@link Event events} occurred. + * @param events The {@linkplain Event events}. + * @return Whether any of the {@linkplain Event events} occurred. */ public boolean containsAny(@Event int... events) { return flags.containsAny(events); @@ -210,6 +210,7 @@ public PositionInfo( /** Creates an instance. */ @UnstableApi + @SuppressWarnings("deprecation") // Setting deprecated windowIndex field public PositionInfo( @Nullable Object windowUid, int mediaItemIndex, @@ -349,7 +350,7 @@ private static String keyForField(@FieldNumber int field) { } /** - * A set of {@link Command commands}. + * A set of {@linkplain Command commands}. * *

      Instances are immutable. */ @@ -433,9 +434,9 @@ public Builder addIf(@Command int command, boolean condition) { } /** - * Adds {@link Command commands}. + * Adds {@linkplain Command commands}. * - * @param commands The {@link Command commands} to add. + * @param commands The {@linkplain Command commands} to add. * @return This builder. * @throws IllegalStateException If {@link #build()} has already been called. */ @@ -448,7 +449,7 @@ public Builder addAll(@Command int... commands) { /** * Adds {@link Commands}. * - * @param commands The set of {@link Command commands} to add. + * @param commands The set of {@linkplain Command commands} to add. * @return This builder. * @throws IllegalStateException If {@link #build()} has already been called. */ @@ -459,7 +460,7 @@ public Builder addAll(Commands commands) { } /** - * Adds all existing {@link Command commands}. + * Adds all existing {@linkplain Command commands}. * * @return This builder. * @throws IllegalStateException If {@link #build()} has already been called. @@ -498,9 +499,9 @@ public Builder removeIf(@Command int command, boolean condition) { } /** - * Removes {@link Command commands}. + * Removes {@linkplain Command commands}. * - * @param commands The {@link Command commands} to remove. + * @param commands The {@linkplain Command commands} to remove. * @return This builder. * @throws IllegalStateException If {@link #build()} has already been called. */ @@ -634,7 +635,8 @@ interface Listener { *

      State changes and events that happen within one {@link Looper} message queue iteration are * reported together and only after all individual callbacks were triggered. * - *

      Only state changes represented by {@link Event events} are reported through this method. + *

      Only state changes represented by {@linkplain Event events} are reported through this + * method. * *

      Listeners should prefer this method over individual callbacks in the following cases: * @@ -782,7 +784,7 @@ default void onPlayerStateChanged(boolean playWhenReady, @State int playbackStat *

      {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. * - * @param playbackState The new playback {@link State state}. + * @param playbackState The new playback {@link State}. */ default void onPlaybackStateChanged(@State int playbackState) {} @@ -793,7 +795,7 @@ default void onPlaybackStateChanged(@State int playbackState) {} * other events that happen in the same {@link Looper} message queue iteration. * * @param playWhenReady Whether playback will proceed when ready. - * @param reason The {@link PlayWhenReadyChangeReason reason} for the change. + * @param reason The {@link PlayWhenReadyChangeReason} for the change. */ default void onPlayWhenReadyChanged( boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {} @@ -835,7 +837,7 @@ default void onRepeatModeChanged(@RepeatMode int repeatMode) {} *

      {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. * - * @param shuffleModeEnabled Whether shuffling of {@link MediaItem media items} is enabled. + * @param shuffleModeEnabled Whether shuffling of {@linkplain MediaItem media items} is enabled. */ default void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {} @@ -1040,10 +1042,10 @@ default void onSurfaceSizeChanged(int width, int height) {} default void onRenderedFirstFrame() {} /** - * Called when there is a change in the {@link Cue Cues}. + * Called when there is a change in the {@linkplain Cue cues}. * - *

      Both {@link #onCues(List)} and {@link #onCues(CueGroup)} are called when there is a change - * in the cues. You should only implement one or the other. + *

      Both this method and {@link #onCues(CueGroup)} are called when there is a change in the + * cues. You should only implement one or the other. * *

      {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. @@ -1057,8 +1059,8 @@ default void onCues(List cues) {} /** * Called when there is a change in the {@link CueGroup}. * - *

      Both {@link #onCues(List)} and {@link #onCues(CueGroup)} are called when there is a change - * in the cues. You should only implement one or the other. + *

      Both this method and {@link #onCues(List)} are called when there is a change in the cues. + * You should only implement one or the other. * *

      {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. @@ -1405,21 +1407,47 @@ default void onMetadata(Metadata metadata) {} int EVENT_DEVICE_VOLUME_CHANGED = 30; /** - * Commands that can be executed on a {@code Player}. One of {@link #COMMAND_PLAY_PAUSE}, {@link - * #COMMAND_PREPARE}, {@link #COMMAND_STOP}, {@link #COMMAND_SEEK_TO_DEFAULT_POSITION}, {@link - * #COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM}, {@link #COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM}, {@link - * #COMMAND_SEEK_TO_PREVIOUS}, {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM}, {@link - * #COMMAND_SEEK_TO_NEXT}, {@link #COMMAND_SEEK_TO_MEDIA_ITEM}, {@link #COMMAND_SEEK_BACK}, {@link - * #COMMAND_SEEK_FORWARD}, {@link #COMMAND_SET_SPEED_AND_PITCH}, {@link - * #COMMAND_SET_SHUFFLE_MODE}, {@link #COMMAND_SET_REPEAT_MODE}, {@link - * #COMMAND_GET_CURRENT_MEDIA_ITEM}, {@link #COMMAND_GET_TIMELINE}, {@link - * #COMMAND_GET_MEDIA_ITEMS_METADATA}, {@link #COMMAND_SET_MEDIA_ITEMS_METADATA}, {@link - * #COMMAND_CHANGE_MEDIA_ITEMS}, {@link #COMMAND_GET_AUDIO_ATTRIBUTES}, {@link - * #COMMAND_GET_VOLUME}, {@link #COMMAND_GET_DEVICE_VOLUME}, {@link #COMMAND_SET_VOLUME}, {@link - * #COMMAND_SET_DEVICE_VOLUME}, {@link #COMMAND_ADJUST_DEVICE_VOLUME}, {@link - * #COMMAND_SET_VIDEO_SURFACE}, {@link #COMMAND_GET_TEXT}, {@link - * #COMMAND_SET_TRACK_SELECTION_PARAMETERS}, {@link #COMMAND_GET_TRACKS} or {@link - * #COMMAND_SET_MEDIA_ITEM}. + * Commands that indicate which method calls are currently permitted on a particular {@code + * Player} instance, and which corresponding {@link Player.Listener} methods will be invoked. + * + *

      The currently available commands can be inspected with {@link #getAvailableCommands()} and + * {@link #isCommandAvailable(int)}. + * + *

      One of the following values: + * + *

        + *
      • {@link #COMMAND_PLAY_PAUSE} + *
      • {@link #COMMAND_PREPARE} + *
      • {@link #COMMAND_STOP} + *
      • {@link #COMMAND_SEEK_TO_DEFAULT_POSITION} + *
      • {@link #COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM} + *
      • {@link #COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM} + *
      • {@link #COMMAND_SEEK_TO_PREVIOUS} + *
      • {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} + *
      • {@link #COMMAND_SEEK_TO_NEXT} + *
      • {@link #COMMAND_SEEK_TO_MEDIA_ITEM} + *
      • {@link #COMMAND_SEEK_BACK} + *
      • {@link #COMMAND_SEEK_FORWARD} + *
      • {@link #COMMAND_SET_SPEED_AND_PITCH} + *
      • {@link #COMMAND_SET_SHUFFLE_MODE} + *
      • {@link #COMMAND_SET_REPEAT_MODE} + *
      • {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} + *
      • {@link #COMMAND_GET_TIMELINE} + *
      • {@link #COMMAND_GET_MEDIA_ITEMS_METADATA} + *
      • {@link #COMMAND_SET_MEDIA_ITEMS_METADATA} + *
      • {@link #COMMAND_SET_MEDIA_ITEM} + *
      • {@link #COMMAND_CHANGE_MEDIA_ITEMS} + *
      • {@link #COMMAND_GET_AUDIO_ATTRIBUTES} + *
      • {@link #COMMAND_GET_VOLUME} + *
      • {@link #COMMAND_GET_DEVICE_VOLUME} + *
      • {@link #COMMAND_SET_VOLUME} + *
      • {@link #COMMAND_SET_DEVICE_VOLUME} + *
      • {@link #COMMAND_ADJUST_DEVICE_VOLUME} + *
      • {@link #COMMAND_SET_VIDEO_SURFACE} + *
      • {@link #COMMAND_GET_TEXT} + *
      • {@link #COMMAND_SET_TRACK_SELECTION_PARAMETERS} + *
      • {@link #COMMAND_GET_TRACKS} + *
      */ // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility // with Kotlin usages from before TYPE_USE was added. @@ -1465,11 +1493,11 @@ default void onMetadata(Metadata metadata) {} int COMMAND_PLAY_PAUSE = 1; /** Command to prepare the player. */ int COMMAND_PREPARE = 2; - /** Command to stop playback or release the player. */ + /** Command to stop playback. */ int COMMAND_STOP = 3; /** Command to seek to the default position of the current {@link MediaItem}. */ int COMMAND_SEEK_TO_DEFAULT_POSITION = 4; - /** Command to seek anywhere into the current {@link MediaItem}. */ + /** Command to seek anywhere inside the current {@link MediaItem}. */ int COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM = 5; /** * @deprecated Use {@link #COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM} instead. @@ -1482,7 +1510,10 @@ default void onMetadata(Metadata metadata) {} */ @UnstableApi @Deprecated int COMMAND_SEEK_TO_PREVIOUS_WINDOW = COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; - /** Command to seek to an earlier position in the current or previous {@link MediaItem}. */ + /** + * Command to seek to an earlier position in the current {@link MediaItem} or the default position + * of the previous {@link MediaItem}. + */ int COMMAND_SEEK_TO_PREVIOUS = 7; /** Command to seek to the default position of the next {@link MediaItem}. */ int COMMAND_SEEK_TO_NEXT_MEDIA_ITEM = 8; @@ -1490,7 +1521,10 @@ default void onMetadata(Metadata metadata) {} * @deprecated Use {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} instead. */ @UnstableApi @Deprecated int COMMAND_SEEK_TO_NEXT_WINDOW = COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; - /** Command to seek to a later position in the current or next {@link MediaItem}. */ + /** + * Command to seek to a later position in the current {@link MediaItem} or the default position of + * the next {@link MediaItem}. + */ int COMMAND_SEEK_TO_NEXT = 9; /** Command to seek anywhere in any {@link MediaItem}. */ int COMMAND_SEEK_TO_MEDIA_ITEM = 10; @@ -1498,9 +1532,9 @@ default void onMetadata(Metadata metadata) {} * @deprecated Use {@link #COMMAND_SEEK_TO_MEDIA_ITEM} instead. */ @UnstableApi @Deprecated int COMMAND_SEEK_TO_WINDOW = COMMAND_SEEK_TO_MEDIA_ITEM; - /** Command to seek back by a fixed increment into the current {@link MediaItem}. */ + /** Command to seek back by a fixed increment inside the current {@link MediaItem}. */ int COMMAND_SEEK_BACK = 11; - /** Command to seek forward by a fixed increment into the current {@link MediaItem}. */ + /** Command to seek forward by a fixed increment inside the current {@link MediaItem}. */ int COMMAND_SEEK_FORWARD = 12; /** Command to set the playback speed and pitch. */ int COMMAND_SET_SPEED_AND_PITCH = 13; @@ -1512,13 +1546,15 @@ default void onMetadata(Metadata metadata) {} int COMMAND_GET_CURRENT_MEDIA_ITEM = 16; /** Command to get the information about the current timeline. */ int COMMAND_GET_TIMELINE = 17; - /** Command to get the {@link MediaItem MediaItems} metadata. */ + /** Command to get metadata related to the playlist and current {@link MediaItem}. */ + // TODO(b/263132691): Rename this to COMMAND_GET_METADATA int COMMAND_GET_MEDIA_ITEMS_METADATA = 18; - /** Command to set the {@link MediaItem MediaItems} metadata. */ + /** Command to set the playlist metadata. */ + // TODO(b/263132691): Rename this to COMMAND_SET_PLAYLIST_METADATA int COMMAND_SET_MEDIA_ITEMS_METADATA = 19; - /** Command to set a {@link MediaItem MediaItem}. */ + /** Command to set a {@link MediaItem}. */ int COMMAND_SET_MEDIA_ITEM = 31; - /** Command to change the {@link MediaItem MediaItems} in the playlist. */ + /** Command to change the {@linkplain MediaItem media items} in the playlist. */ int COMMAND_CHANGE_MEDIA_ITEMS = 20; /** Command to get the player current {@link AudioAttributes}. */ int COMMAND_GET_AUDIO_ATTRIBUTES = 21; @@ -1528,7 +1564,7 @@ default void onMetadata(Metadata metadata) {} int COMMAND_GET_DEVICE_VOLUME = 23; /** Command to set the player volume. */ int COMMAND_SET_VOLUME = 24; - /** Command to set the device volume and mute it. */ + /** Command to set the device volume. */ int COMMAND_SET_DEVICE_VOLUME = 25; /** Command to increase and decrease the device volume and mute it. */ int COMMAND_ADJUST_DEVICE_VOLUME = 26; @@ -1573,17 +1609,17 @@ default void onMetadata(Metadata metadata) {} void removeListener(Listener listener); /** - * Clears the playlist, adds the specified {@link MediaItem MediaItems} and resets the position to - * the default position. + * Clears the playlist, adds the specified {@linkplain MediaItem media items} and resets the + * position to the default position. * - * @param mediaItems The new {@link MediaItem MediaItems}. + * @param mediaItems The new {@linkplain MediaItem media items}. */ void setMediaItems(List mediaItems); /** - * Clears the playlist and adds the specified {@link MediaItem MediaItems}. + * Clears the playlist and adds the specified {@linkplain MediaItem media items}. * - * @param mediaItems The new {@link MediaItem MediaItems}. + * @param mediaItems The new {@linkplain MediaItem media items}. * @param resetPosition Whether the playback position should be reset to the default position in * the first {@link Timeline.Window}. If false, playback will start from the position defined * by {@link #getCurrentMediaItemIndex()} and {@link #getCurrentPosition()}. @@ -1591,9 +1627,9 @@ default void onMetadata(Metadata metadata) {} void setMediaItems(List mediaItems, boolean resetPosition); /** - * Clears the playlist and adds the specified {@link MediaItem MediaItems}. + * Clears the playlist and adds the specified {@linkplain MediaItem media items}. * - * @param mediaItems The new {@link MediaItem MediaItems}. + * @param mediaItems The new {@linkplain MediaItem media items}. * @param startIndex The {@link MediaItem} index to start playback from. If {@link C#INDEX_UNSET} * is passed, the current position is not reset. * @param startPositionMs The position in milliseconds to start playback from. If {@link @@ -1650,7 +1686,7 @@ default void onMetadata(Metadata metadata) {} /** * Adds a list of media items to the end of the playlist. * - * @param mediaItems The {@link MediaItem MediaItems} to add. + * @param mediaItems The {@linkplain MediaItem media items} to add. */ void addMediaItems(List mediaItems); @@ -1659,7 +1695,7 @@ default void onMetadata(Metadata metadata) {} * * @param index The index at which to add the media items. If the index is larger than the size of * the playlist, the media items are added to the end of the playlist. - * @param mediaItems The {@link MediaItem MediaItems} to add. + * @param mediaItems The {@linkplain MediaItem media items} to add. */ void addMediaItems(int index, List mediaItems); @@ -1756,9 +1792,9 @@ default void onMetadata(Metadata metadata) {} void prepare(); /** - * Returns the current {@link State playback state} of the player. + * Returns the current {@linkplain State playback state} of the player. * - * @return The current {@link State playback state}. + * @return The current {@linkplain State playback state}. * @see Listener#onPlaybackStateChanged(int) */ @State @@ -1768,7 +1804,7 @@ default void onMetadata(Metadata metadata) {} * Returns the reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code * true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed. * - * @return The current {@link PlaybackSuppressionReason playback suppression reason}. + * @return The current {@link PlaybackSuppressionReason}. * @see Listener#onPlaybackSuppressionReasonChanged(int) */ @PlaybackSuppressionReason @@ -1806,11 +1842,11 @@ default void onMetadata(Metadata metadata) {} /** * Resumes playback as soon as {@link #getPlaybackState()} == {@link #STATE_READY}. Equivalent to - * {@code setPlayWhenReady(true)}. + * {@link #setPlayWhenReady(boolean) setPlayWhenReady(true)}. */ void play(); - /** Pauses playback. Equivalent to {@code setPlayWhenReady(false)}. */ + /** Pauses playback. Equivalent to {@link #setPlayWhenReady(boolean) setPlayWhenReady(false)}. */ void pause(); /** @@ -2265,7 +2301,7 @@ default void onMetadata(Metadata metadata) {} @Nullable MediaItem getCurrentMediaItem(); - /** Returns the number of {@link MediaItem media items} in the playlist. */ + /** Returns the number of {@linkplain MediaItem media items} in the playlist. */ int getMediaItemCount(); /** Returns the {@link MediaItem} at the given index. */ @@ -2298,7 +2334,7 @@ default void onMetadata(Metadata metadata) {} /** * Returns an estimate of the total buffered duration from the current position, in milliseconds. - * This includes pre-buffered data for subsequent ads and {@link MediaItem media items}. + * This includes pre-buffered data for subsequent ads and {@linkplain MediaItem media items}. */ long getTotalBufferedDuration(); From 3d9fd60d54a11994d6c62eebc82078c7a8f5f5fa Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 20 Dec 2022 16:30:52 +0000 Subject: [PATCH 081/141] Document the relationship between Player methods and available commands #minor-release PiperOrigin-RevId: 496668378 (cherry picked from commit d8c964cfe65bef4693056b052802ac1bee3ec56e) --- .../java/androidx/media3/common/Player.java | 579 ++++++++++++++++-- 1 file changed, 524 insertions(+), 55 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index d974a10a448..b3f024192a8 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -1413,6 +1413,9 @@ default void onMetadata(Metadata metadata) {} *

      The currently available commands can be inspected with {@link #getAvailableCommands()} and * {@link #isCommandAvailable(int)}. * + *

      See the documentation of each command constant for the details of which methods it permits + * calling. + * *

      One of the following values: * *

        @@ -1489,21 +1492,62 @@ default void onMetadata(Metadata metadata) {} COMMAND_GET_TRACKS, }) @interface Command {} - /** Command to start, pause or resume playback. */ + /** + * Command to start, pause or resume playback. + * + *

        The following methods must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}: + * + *

          + *
        • {@link #play()} + *
        • {@link #pause()} + *
        • {@link #setPlayWhenReady(boolean)} + *
        + */ int COMMAND_PLAY_PAUSE = 1; - /** Command to prepare the player. */ + + /** + * Command to prepare the player. + * + *

        The {@link #prepare()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_PREPARE = 2; - /** Command to stop playback. */ + + /** + * Command to stop playback. + * + *

        The {@link #stop()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_STOP = 3; - /** Command to seek to the default position of the current {@link MediaItem}. */ + + /** + * Command to seek to the default position of the current {@link MediaItem}. + * + *

        The {@link #seekToDefaultPosition()} method must only be called if this command is + * {@linkplain #isCommandAvailable(int) available}. + */ int COMMAND_SEEK_TO_DEFAULT_POSITION = 4; - /** Command to seek anywhere inside the current {@link MediaItem}. */ + + /** + * Command to seek anywhere inside the current {@link MediaItem}. + * + *

        The {@link #seekTo(long)} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM = 5; /** * @deprecated Use {@link #COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM} instead. */ @UnstableApi @Deprecated int COMMAND_SEEK_IN_CURRENT_WINDOW = COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM; - /** Command to seek to the default position of the previous {@link MediaItem}. */ + + /** + * Command to seek to the default position of the previous {@link MediaItem}. + * + *

        The {@link #seekToPreviousMediaItem()} method must only be called if this command is + * {@linkplain #isCommandAvailable(int) available}. + */ int COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM = 6; /** * @deprecated Use {@link #COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM} instead. @@ -1513,9 +1557,17 @@ default void onMetadata(Metadata metadata) {} /** * Command to seek to an earlier position in the current {@link MediaItem} or the default position * of the previous {@link MediaItem}. + * + *

        The {@link #seekToPrevious()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. */ int COMMAND_SEEK_TO_PREVIOUS = 7; - /** Command to seek to the default position of the next {@link MediaItem}. */ + /** + * Command to seek to the default position of the next {@link MediaItem}. + * + *

        The {@link #seekToNextMediaItem()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_SEEK_TO_NEXT_MEDIA_ITEM = 8; /** * @deprecated Use {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} instead. @@ -1524,57 +1576,259 @@ default void onMetadata(Metadata metadata) {} /** * Command to seek to a later position in the current {@link MediaItem} or the default position of * the next {@link MediaItem}. + * + *

        The {@link #seekToNext()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. */ int COMMAND_SEEK_TO_NEXT = 9; - /** Command to seek anywhere in any {@link MediaItem}. */ + + /** + * Command to seek anywhere in any {@link MediaItem}. + * + *

        The following methods must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}: + * + *

          + *
        • {@link #seekTo(int, long)} + *
        • {@link #seekToDefaultPosition(int)} + *
        + */ int COMMAND_SEEK_TO_MEDIA_ITEM = 10; /** * @deprecated Use {@link #COMMAND_SEEK_TO_MEDIA_ITEM} instead. */ @UnstableApi @Deprecated int COMMAND_SEEK_TO_WINDOW = COMMAND_SEEK_TO_MEDIA_ITEM; - /** Command to seek back by a fixed increment inside the current {@link MediaItem}. */ + /** + * Command to seek back by a fixed increment inside the current {@link MediaItem}. + * + *

        The {@link #seekBack()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_SEEK_BACK = 11; - /** Command to seek forward by a fixed increment inside the current {@link MediaItem}. */ + /** + * Command to seek forward by a fixed increment inside the current {@link MediaItem}. + * + *

        The {@link #seekForward()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_SEEK_FORWARD = 12; - /** Command to set the playback speed and pitch. */ + + /** + * Command to set the playback speed and pitch. + * + *

        The following methods must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}: + * + *

          + *
        • {@link #setPlaybackParameters(PlaybackParameters)} + *
        • {@link #setPlaybackSpeed(float)} + *
        + */ int COMMAND_SET_SPEED_AND_PITCH = 13; - /** Command to enable shuffling. */ + + /** + * Command to enable shuffling. + * + *

        The {@link #setShuffleModeEnabled(boolean)} method must only be called if this command is + * {@linkplain #isCommandAvailable(int) available}. + */ int COMMAND_SET_SHUFFLE_MODE = 14; - /** Command to set the repeat mode. */ + + /** + * Command to set the repeat mode. + * + *

        The {@link #setRepeatMode(int)} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_SET_REPEAT_MODE = 15; - /** Command to get the currently playing {@link MediaItem}. */ + + /** + * Command to get the currently playing {@link MediaItem}. + * + *

        The {@link #getCurrentMediaItem()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_GET_CURRENT_MEDIA_ITEM = 16; - /** Command to get the information about the current timeline. */ + + /** + * Command to get the information about the current timeline. + * + *

        The following methods must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}: + * + *

          + *
        • {@link #getCurrentTimeline()} + *
        • {@link #getCurrentMediaItemIndex()} + *
        • {@link #getCurrentPeriodIndex()} + *
        • {@link #getMediaItemCount()} + *
        • {@link #getMediaItemAt(int)} + *
        • {@link #getNextMediaItemIndex()} + *
        • {@link #getPreviousMediaItemIndex()} + *
        • {@link #hasPreviousMediaItem()} + *
        • {@link #hasNextMediaItem()} + *
        • {@link #getCurrentAdGroupIndex()} + *
        • {@link #getCurrentAdIndexInAdGroup()} + *
        + */ int COMMAND_GET_TIMELINE = 17; - /** Command to get metadata related to the playlist and current {@link MediaItem}. */ + + /** + * Command to get metadata related to the playlist and current {@link MediaItem}. + * + *

        The following methods must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}: + * + *

          + *
        • {@link #getMediaMetadata()} + *
        • {@link #getPlaylistMetadata()} + *
        + */ // TODO(b/263132691): Rename this to COMMAND_GET_METADATA int COMMAND_GET_MEDIA_ITEMS_METADATA = 18; - /** Command to set the playlist metadata. */ + + /** + * Command to set the playlist metadata. + * + *

        The {@link #setPlaylistMetadata(MediaMetadata)} method must only be called if this command + * is {@linkplain #isCommandAvailable(int) available}. + */ // TODO(b/263132691): Rename this to COMMAND_SET_PLAYLIST_METADATA int COMMAND_SET_MEDIA_ITEMS_METADATA = 19; - /** Command to set a {@link MediaItem}. */ + + /** + * Command to set a {@link MediaItem}. + * + *

        The following methods must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}: + * + *

          + *
        • {@link #setMediaItem(MediaItem)} + *
        • {@link #setMediaItem(MediaItem, boolean)} + *
        • {@link #setMediaItem(MediaItem, long)} + *
        + */ int COMMAND_SET_MEDIA_ITEM = 31; - /** Command to change the {@linkplain MediaItem media items} in the playlist. */ + /** + * Command to change the {@linkplain MediaItem media items} in the playlist. + * + *

        The following methods must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}: + * + *

          + *
        • {@link #addMediaItem(MediaItem)} + *
        • {@link #addMediaItem(int, MediaItem)} + *
        • {@link #addMediaItems(List)} + *
        • {@link #addMediaItems(int, List)} + *
        • {@link #clearMediaItems()} + *
        • {@link #moveMediaItem(int, int)} + *
        • {@link #moveMediaItems(int, int, int)} + *
        • {@link #removeMediaItem(int)} + *
        • {@link #removeMediaItems(int, int)} + *
        • {@link #setMediaItems(List)} + *
        • {@link #setMediaItems(List, boolean)} + *
        • {@link #setMediaItems(List, int, long)} + *
        + */ int COMMAND_CHANGE_MEDIA_ITEMS = 20; - /** Command to get the player current {@link AudioAttributes}. */ + + /** + * Command to get the player current {@link AudioAttributes}. + * + *

        The {@link #getAudioAttributes()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_GET_AUDIO_ATTRIBUTES = 21; - /** Command to get the player volume. */ + + /** + * Command to get the player volume. + * + *

        The {@link #getVolume()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_GET_VOLUME = 22; - /** Command to get the device volume and whether it is muted. */ + + /** + * Command to get the device volume and whether it is muted. + * + *

        The following methods must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}: + * + *

          + *
        • {@link #getDeviceVolume()} + *
        • {@link #isDeviceMuted()} + *
        + */ int COMMAND_GET_DEVICE_VOLUME = 23; - /** Command to set the player volume. */ + + /** + * Command to set the player volume. + * + *

        The {@link #setVolume(float)} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_SET_VOLUME = 24; - /** Command to set the device volume. */ + /** + * Command to set the device volume. + * + *

        The {@link #setDeviceVolume(int)} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_SET_DEVICE_VOLUME = 25; - /** Command to increase and decrease the device volume and mute it. */ + + /** + * Command to increase and decrease the device volume and mute it. + * + *

        The following methods must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}: + * + *

          + *
        • {@link #increaseDeviceVolume()} + *
        • {@link #decreaseDeviceVolume()} + *
        • {@link #setDeviceMuted(boolean)} + *
        + */ int COMMAND_ADJUST_DEVICE_VOLUME = 26; - /** Command to set and clear the surface on which to render the video. */ + + /** + * Command to set and clear the surface on which to render the video. + * + *

        The following methods must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}: + * + *

          + *
        • {@link #setVideoSurface(Surface)} + *
        • {@link #clearVideoSurface()} + *
        • {@link #clearVideoSurface(Surface)} + *
        • {@link #setVideoSurfaceHolder(SurfaceHolder)} + *
        • {@link #clearVideoSurfaceHolder(SurfaceHolder)} + *
        • {@link #setVideoSurfaceView(SurfaceView)} + *
        • {@link #clearVideoSurfaceView(SurfaceView)} + *
        + */ int COMMAND_SET_VIDEO_SURFACE = 27; - /** Command to get the text that should currently be displayed by the player. */ + + /** + * Command to get the text that should currently be displayed by the player. + * + *

        The {@link #getCurrentCues()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_GET_TEXT = 28; - /** Command to set the player's track selection parameters. */ + + /** + * Command to set the player's track selection parameters. + * + *

        The {@link #setTrackSelectionParameters(TrackSelectionParameters)} method must only be + * called if this command is {@linkplain #isCommandAvailable(int) available}. + */ int COMMAND_SET_TRACK_SELECTION_PARAMETERS = 29; - /** Command to get details of the current track selection. */ + + /** + * Command to get details of the current track selection. + * + *

        The {@link #getCurrentTracks()} method must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}. + */ int COMMAND_GET_TRACKS = 30; /** Represents an invalid {@link Command}. */ @@ -1612,6 +1866,9 @@ default void onMetadata(Metadata metadata) {} * Clears the playlist, adds the specified {@linkplain MediaItem media items} and resets the * position to the default position. * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + * * @param mediaItems The new {@linkplain MediaItem media items}. */ void setMediaItems(List mediaItems); @@ -1619,6 +1876,9 @@ default void onMetadata(Metadata metadata) {} /** * Clears the playlist and adds the specified {@linkplain MediaItem media items}. * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + * * @param mediaItems The new {@linkplain MediaItem media items}. * @param resetPosition Whether the playback position should be reset to the default position in * the first {@link Timeline.Window}. If false, playback will start from the position defined @@ -1629,6 +1889,9 @@ default void onMetadata(Metadata metadata) {} /** * Clears the playlist and adds the specified {@linkplain MediaItem media items}. * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + * * @param mediaItems The new {@linkplain MediaItem media items}. * @param startIndex The {@link MediaItem} index to start playback from. If {@link C#INDEX_UNSET} * is passed, the current position is not reset. @@ -1645,6 +1908,9 @@ default void onMetadata(Metadata metadata) {} * Clears the playlist, adds the specified {@link MediaItem} and resets the position to the * default position. * + *

        This method must only be called if {@link #COMMAND_SET_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. + * * @param mediaItem The new {@link MediaItem}. */ void setMediaItem(MediaItem mediaItem); @@ -1652,6 +1918,9 @@ default void onMetadata(Metadata metadata) {} /** * Clears the playlist and adds the specified {@link MediaItem}. * + *

        This method must only be called if {@link #COMMAND_SET_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. + * * @param mediaItem The new {@link MediaItem}. * @param startPositionMs The position in milliseconds to start playback from. */ @@ -1660,6 +1929,9 @@ default void onMetadata(Metadata metadata) {} /** * Clears the playlist and adds the specified {@link MediaItem}. * + *

        This method must only be called if {@link #COMMAND_SET_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. + * * @param mediaItem The new {@link MediaItem}. * @param resetPosition Whether the playback position should be reset to the default position. If * false, playback will start from the position defined by {@link #getCurrentMediaItemIndex()} @@ -1670,6 +1942,9 @@ default void onMetadata(Metadata metadata) {} /** * Adds a media item to the end of the playlist. * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + * * @param mediaItem The {@link MediaItem} to add. */ void addMediaItem(MediaItem mediaItem); @@ -1677,6 +1952,9 @@ default void onMetadata(Metadata metadata) {} /** * Adds a media item at the given index of the playlist. * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + * * @param index The index at which to add the media item. If the index is larger than the size of * the playlist, the media item is added to the end of the playlist. * @param mediaItem The {@link MediaItem} to add. @@ -1686,6 +1964,9 @@ default void onMetadata(Metadata metadata) {} /** * Adds a list of media items to the end of the playlist. * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + * * @param mediaItems The {@linkplain MediaItem media items} to add. */ void addMediaItems(List mediaItems); @@ -1693,6 +1974,9 @@ default void onMetadata(Metadata metadata) {} /** * Adds a list of media items at the given index of the playlist. * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + * * @param index The index at which to add the media items. If the index is larger than the size of * the playlist, the media items are added to the end of the playlist. * @param mediaItems The {@linkplain MediaItem media items} to add. @@ -1702,6 +1986,9 @@ default void onMetadata(Metadata metadata) {} /** * Moves the media item at the current index to the new index. * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + * * @param currentIndex The current index of the media item to move. If the index is larger than * the size of the playlist, the request is ignored. * @param newIndex The new index of the media item. If the new index is larger than the size of @@ -1712,6 +1999,9 @@ default void onMetadata(Metadata metadata) {} /** * Moves the media item range to the new index. * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + * * @param fromIndex The start of the range to move. If the index is larger than the size of the * playlist, the request is ignored. * @param toIndex The first item not to be included in the range (exclusive). If the index is @@ -1725,6 +2015,9 @@ default void onMetadata(Metadata metadata) {} /** * Removes the media item at the given index of the playlist. * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + * * @param index The index at which to remove the media item. If the index is larger than the size * of the playlist, the request is ignored. */ @@ -1733,6 +2026,9 @@ default void onMetadata(Metadata metadata) {} /** * Removes a range of media items from the playlist. * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + * * @param fromIndex The index at which to start removing media items. If the index is larger than * the size of the playlist, the request is ignored. * @param toIndex The index of the first item to be kept (exclusive). If the index is larger than @@ -1740,7 +2036,12 @@ default void onMetadata(Metadata metadata) {} */ void removeMediaItems(int fromIndex, int toIndex); - /** Clears the playlist. */ + /** + * Clears the playlist. + * + *

        This method must only be called if {@link #COMMAND_CHANGE_MEDIA_ITEMS} is {@linkplain + * #getAvailableCommands() available}. + */ void clearMediaItems(); /** @@ -1748,13 +2049,6 @@ default void onMetadata(Metadata metadata) {} * *

        This method does not execute the command. * - *

        Executing a command that is not available (for example, calling {@link - * #seekToNextMediaItem()} if {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} is unavailable) will - * neither throw an exception nor generate a {@link #getPlayerError()} player error}. - * - *

        {@link #COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM} and {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} - * are unavailable if there is no such {@link MediaItem}. - * * @param command A {@link Command}. * @return Whether the {@link Command} is available. * @see Listener#onAvailableCommandsChanged(Commands) @@ -1771,13 +2065,6 @@ default void onMetadata(Metadata metadata) {} * Listener#onAvailableCommandsChanged(Commands)} to get an update when the available commands * change. * - *

        Executing a command that is not available (for example, calling {@link - * #seekToNextMediaItem()} if {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} is unavailable) will - * neither throw an exception nor generate a {@link #getPlayerError()} player error}. - * - *

        {@link #COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM} and {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} - * are unavailable if there is no such {@link MediaItem}. - * * @return The currently available {@link Commands}. * @see Listener#onAvailableCommandsChanged */ @@ -1786,6 +2073,9 @@ default void onMetadata(Metadata metadata) {} /** * Prepares the player. * + *

        This method must only be called if {@link #COMMAND_PREPARE} is {@linkplain + * #getAvailableCommands() available}. + * *

        This will move the player out of {@link #STATE_IDLE idle state} and the player will start * loading media and acquire resources needed for playback. */ @@ -1843,10 +2133,18 @@ default void onMetadata(Metadata metadata) {} /** * Resumes playback as soon as {@link #getPlaybackState()} == {@link #STATE_READY}. Equivalent to * {@link #setPlayWhenReady(boolean) setPlayWhenReady(true)}. + * + *

        This method must only be called if {@link #COMMAND_PLAY_PAUSE} is {@linkplain + * #getAvailableCommands() available}. */ void play(); - /** Pauses playback. Equivalent to {@link #setPlayWhenReady(boolean) setPlayWhenReady(false)}. */ + /** + * Pauses playback. Equivalent to {@link #setPlayWhenReady(boolean) setPlayWhenReady(false)}. + * + *

        This method must only be called if {@link #COMMAND_PLAY_PAUSE} is {@linkplain + * #getAvailableCommands() available}. + */ void pause(); /** @@ -1854,6 +2152,9 @@ default void onMetadata(Metadata metadata) {} * *

        If the player is already in the ready state then this method pauses and resumes playback. * + *

        This method must only be called if {@link #COMMAND_PLAY_PAUSE} is {@linkplain + * #getAvailableCommands() available}. + * * @param playWhenReady Whether playback should proceed when ready. */ void setPlayWhenReady(boolean playWhenReady); @@ -1869,6 +2170,9 @@ default void onMetadata(Metadata metadata) {} /** * Sets the {@link RepeatMode} to be used for playback. * + *

        This method must only be called if {@link #COMMAND_SET_REPEAT_MODE} is {@linkplain + * #getAvailableCommands() available}. + * * @param repeatMode The repeat mode. */ void setRepeatMode(@RepeatMode int repeatMode); @@ -1885,6 +2189,9 @@ default void onMetadata(Metadata metadata) {} /** * Sets whether shuffling of media items is enabled. * + *

        This method must only be called if {@link #COMMAND_SET_SHUFFLE_MODE} is {@linkplain + * #getAvailableCommands() available}. + * * @param shuffleModeEnabled Whether shuffling is enabled. */ void setShuffleModeEnabled(boolean shuffleModeEnabled); @@ -1908,6 +2215,9 @@ default void onMetadata(Metadata metadata) {} * Seeks to the default position associated with the current {@link MediaItem}. The position can * depend on the type of media being played. For live streams it will typically be the live edge. * For other streams it will typically be the start. + * + *

        This method must only be called if {@link #COMMAND_SEEK_TO_DEFAULT_POSITION} is {@linkplain + * #getAvailableCommands() available}. */ void seekToDefaultPosition(); @@ -1916,6 +2226,9 @@ default void onMetadata(Metadata metadata) {} * depend on the type of media being played. For live streams it will typically be the live edge. * For other streams it will typically be the start. * + *

        This method must only be called if {@link #COMMAND_SEEK_TO_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. + * * @param mediaItemIndex The index of the {@link MediaItem} whose associated default position * should be seeked to. If the index is larger than the size of the playlist, the request is * ignored. @@ -1925,6 +2238,9 @@ default void onMetadata(Metadata metadata) {} /** * Seeks to a position specified in milliseconds in the current {@link MediaItem}. * + *

        This method must only be called if {@link #COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM} is + * {@linkplain #getAvailableCommands() available}. + * * @param positionMs The seek position in the current {@link MediaItem}, or {@link C#TIME_UNSET} * to seek to the media item's default position. */ @@ -1933,6 +2249,9 @@ default void onMetadata(Metadata metadata) {} /** * Seeks to a position specified in milliseconds in the specified {@link MediaItem}. * + *

        This method must only be called if {@link #COMMAND_SEEK_TO_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. + * * @param mediaItemIndex The index of the {@link MediaItem}. If the index is larger than the size * of the playlist, the request is ignored. * @param positionMs The seek position in the specified {@link MediaItem}, or {@link C#TIME_UNSET} @@ -1950,6 +2269,9 @@ default void onMetadata(Metadata metadata) {} /** * Seeks back in the current {@link MediaItem} by {@link #getSeekBackIncrement()} milliseconds. + * + *

        This method must only be called if {@link #COMMAND_SEEK_BACK} is {@linkplain + * #getAvailableCommands() available}. */ void seekBack(); @@ -1964,6 +2286,9 @@ default void onMetadata(Metadata metadata) {} /** * Seeks forward in the current {@link MediaItem} by {@link #getSeekForwardIncrement()} * milliseconds. + * + *

        This method must only be called if {@link #COMMAND_SEEK_FORWARD} is {@linkplain + * #getAvailableCommands() available}. */ void seekForward(); @@ -2013,6 +2338,9 @@ default void onMetadata(Metadata metadata) {} *

        Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when * the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more * details. + * + *

        This method must only be called if {@link #COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM} is + * {@linkplain #getAvailableCommands() available}. */ void seekToPreviousMediaItem(); @@ -2044,6 +2372,9 @@ default void onMetadata(Metadata metadata) {} * MediaItem}. *

      • Otherwise, seeks to 0 in the current {@link MediaItem}. *
      + * + *

      This method must only be called if {@link #COMMAND_SEEK_TO_PREVIOUS} is {@linkplain + * #getAvailableCommands() available}. */ void seekToPrevious(); @@ -2093,6 +2424,9 @@ default void onMetadata(Metadata metadata) {} *

      Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when * the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more * details. + * + *

      This method must only be called if {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. */ void seekToNextMediaItem(); @@ -2108,6 +2442,9 @@ default void onMetadata(Metadata metadata) {} * has not ended, seeks to the live edge of the current {@link MediaItem}. *

    • Otherwise, does nothing. *
    + * + *

    This method must only be called if {@link #COMMAND_SEEK_TO_NEXT} is {@linkplain + * #getAvailableCommands() available}. */ void seekToNext(); @@ -2119,6 +2456,9 @@ default void onMetadata(Metadata metadata) {} * Listener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the currently * active playback parameters change. * + *

    This method must only be called if {@link #COMMAND_SET_SPEED_AND_PITCH} is {@linkplain + * #getAvailableCommands() available}. + * * @param playbackParameters The playback parameters. */ void setPlaybackParameters(PlaybackParameters playbackParameters); @@ -2129,6 +2469,9 @@ default void onMetadata(Metadata metadata) {} *

    This is equivalent to {@code * setPlaybackParameters(getPlaybackParameters().withSpeed(speed))}. * + *

    This method must only be called if {@link #COMMAND_SET_SPEED_AND_PITCH} is {@linkplain + * #getAvailableCommands() available}. + * * @param speed The linear factor by which playback will be sped up. Must be higher than 0. 1 is * normal speed, 2 is twice as fast, 0.5 is half normal speed. */ @@ -2152,6 +2495,9 @@ default void onMetadata(Metadata metadata) {} * *

    Calling this method does not clear the playlist, reset the playback position or the playback * error. + * + *

    This method must only be called if {@link #COMMAND_STOP} is {@linkplain + * #getAvailableCommands() available}. */ void stop(); @@ -2168,11 +2514,15 @@ default void onMetadata(Metadata metadata) {} * Releases the player. This method must be called when the player is no longer required. The * player must not be used after calling this method. */ + // TODO(b/261158047): Document that COMMAND_RELEASE must be available once it exists. void release(); /** * Returns the current tracks. * + *

    This method must only be called if {@link #COMMAND_GET_TRACKS} is {@linkplain + * #getAvailableCommands() available}. + * * @see Listener#onTracksChanged(Tracks) */ Tracks getCurrentTracks(); @@ -2200,6 +2550,9 @@ default void onMetadata(Metadata metadata) {} * .setMaxVideoSizeSd() * .build()) * } + * + *

    This method must only be called if {@link #COMMAND_SET_TRACK_SELECTION_PARAMETERS} is + * {@linkplain #getAvailableCommands() available}. */ void setTrackSelectionParameters(TrackSelectionParameters parameters); @@ -2212,16 +2565,27 @@ default void onMetadata(Metadata metadata) {} * metadata that has been parsed from the media and output via {@link * Listener#onMetadata(Metadata)}. If a field is populated in the {@link MediaItem#mediaMetadata}, * it will be prioritised above the same field coming from static or timed metadata. + * + *

    This method must only be called if {@link #COMMAND_GET_MEDIA_ITEMS_METADATA} is {@linkplain + * #getAvailableCommands() available}. */ MediaMetadata getMediaMetadata(); /** * Returns the playlist {@link MediaMetadata}, as set by {@link * #setPlaylistMetadata(MediaMetadata)}, or {@link MediaMetadata#EMPTY} if not supported. + * + *

    This method must only be called if {@link #COMMAND_GET_MEDIA_ITEMS_METADATA} is {@linkplain + * #getAvailableCommands() available}. */ MediaMetadata getPlaylistMetadata(); - /** Sets the playlist {@link MediaMetadata}. */ + /** + * Sets the playlist {@link MediaMetadata}. + * + *

    This method must only be called if {@link #COMMAND_SET_MEDIA_ITEMS_METADATA} is {@linkplain + * #getAvailableCommands() available}. + */ void setPlaylistMetadata(MediaMetadata mediaMetadata); /** @@ -2234,11 +2598,19 @@ default void onMetadata(Metadata metadata) {} /** * Returns the current {@link Timeline}. Never null, but may be empty. * + *

    This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + * #getAvailableCommands() available}. + * * @see Listener#onTimelineChanged(Timeline, int) */ Timeline getCurrentTimeline(); - /** Returns the index of the period currently being played. */ + /** + * Returns the index of the period currently being played. + * + *

    This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + * #getAvailableCommands() available}. + */ int getCurrentPeriodIndex(); /** @@ -2252,6 +2624,9 @@ default void onMetadata(Metadata metadata) {} * Returns the index of the current {@link MediaItem} in the {@link #getCurrentTimeline() * timeline}, or the prospective index if the {@link #getCurrentTimeline() current timeline} is * empty. + * + *

    This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + * #getAvailableCommands() available}. */ int getCurrentMediaItemIndex(); @@ -2271,6 +2646,9 @@ default void onMetadata(Metadata metadata) {} *

    Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when * the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more * details. + * + *

    This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + * #getAvailableCommands() available}. */ int getNextMediaItemIndex(); @@ -2290,21 +2668,37 @@ default void onMetadata(Metadata metadata) {} *

    Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when * the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more * details. + * + *

    This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + * #getAvailableCommands() available}. */ int getPreviousMediaItemIndex(); /** * Returns the currently playing {@link MediaItem}. May be null if the timeline is empty. * + *

    This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. + * * @see Listener#onMediaItemTransition(MediaItem, int) */ @Nullable MediaItem getCurrentMediaItem(); - /** Returns the number of {@linkplain MediaItem media items} in the playlist. */ + /** + * Returns the number of {@linkplain MediaItem media items} in the playlist. + * + *

    This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + * #getAvailableCommands() available}. + */ int getMediaItemCount(); - /** Returns the {@link MediaItem} at the given index. */ + /** + * Returns the {@link MediaItem} at the given index. + * + *

    This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + * #getAvailableCommands() available}. + */ MediaItem getMediaItemAt(int index); /** @@ -2402,12 +2796,18 @@ default void onMetadata(Metadata metadata) {} /** * If {@link #isPlayingAd()} returns true, returns the index of the ad group in the period * currently being played. Returns {@link C#INDEX_UNSET} otherwise. + * + *

    This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + * #getAvailableCommands() available}. */ int getCurrentAdGroupIndex(); /** * If {@link #isPlayingAd()} returns true, returns the index of the ad in its ad group. Returns * {@link C#INDEX_UNSET} otherwise. + * + *

    This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + * #getAvailableCommands() available}. */ int getCurrentAdIndexInAdGroup(); @@ -2432,13 +2832,21 @@ default void onMetadata(Metadata metadata) {} */ long getContentBufferedPosition(); - /** Returns the attributes for audio playback. */ + /** + * Returns the attributes for audio playback. + * + *

    This method must only be called if {@link #COMMAND_GET_AUDIO_ATTRIBUTES} is {@linkplain + * #getAvailableCommands() available}. + */ AudioAttributes getAudioAttributes(); /** * Sets the audio volume, valid values are between 0 (silence) and 1 (unity gain, signal * unchanged), inclusive. * + *

    This method must only be called if {@link #COMMAND_SET_VOLUME} is {@linkplain + * #getAvailableCommands() available}. + * * @param volume Linear output gain to apply to all audio channels. */ void setVolume(@FloatRange(from = 0, to = 1.0) float volume); @@ -2446,6 +2854,9 @@ default void onMetadata(Metadata metadata) {} /** * Returns the audio volume, with 0 being silence and 1 being unity gain (signal unchanged). * + *

    This method must only be called if {@link #COMMAND_GET_VOLUME} is {@linkplain + * #getAvailableCommands() available}. + * * @return The linear gain applied to all audio channels. */ @FloatRange(from = 0, to = 1.0) @@ -2454,6 +2865,9 @@ default void onMetadata(Metadata metadata) {} /** * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} * currently set on the player. + * + *

    This method must only be called if {@link #COMMAND_SET_VIDEO_SURFACE} is {@linkplain + * #getAvailableCommands() available}. */ void clearVideoSurface(); @@ -2461,6 +2875,9 @@ default void onMetadata(Metadata metadata) {} * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. * Else does nothing. * + *

    This method must only be called if {@link #COMMAND_SET_VIDEO_SURFACE} is {@linkplain + * #getAvailableCommands() available}. + * * @param surface The surface to clear. */ void clearVideoSurface(@Nullable Surface surface); @@ -2476,6 +2893,9 @@ default void onMetadata(Metadata metadata) {} * this method, since passing the holder allows the player to track the lifecycle of the surface * automatically. * + *

    This method must only be called if {@link #COMMAND_SET_VIDEO_SURFACE} is {@linkplain + * #getAvailableCommands() available}. + * * @param surface The {@link Surface}. */ void setVideoSurface(@Nullable Surface surface); @@ -2487,6 +2907,9 @@ default void onMetadata(Metadata metadata) {} *

    The thread that calls the {@link SurfaceHolder.Callback} methods must be the thread * associated with {@link #getApplicationLooper()}. * + *

    This method must only be called if {@link #COMMAND_SET_VIDEO_SURFACE} is {@linkplain + * #getAvailableCommands() available}. + * * @param surfaceHolder The surface holder. */ void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder); @@ -2495,6 +2918,9 @@ default void onMetadata(Metadata metadata) {} * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being * rendered if it matches the one passed. Else does nothing. * + *

    This method must only be called if {@link #COMMAND_SET_VIDEO_SURFACE} is {@linkplain + * #getAvailableCommands() available}. + * * @param surfaceHolder The surface holder to clear. */ void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder); @@ -2506,6 +2932,9 @@ default void onMetadata(Metadata metadata) {} *

    The thread that calls the {@link SurfaceHolder.Callback} methods must be the thread * associated with {@link #getApplicationLooper()}. * + *

    This method must only be called if {@link #COMMAND_SET_VIDEO_SURFACE} is {@linkplain + * #getAvailableCommands() available}. + * * @param surfaceView The surface view. */ void setVideoSurfaceView(@Nullable SurfaceView surfaceView); @@ -2514,6 +2943,9 @@ default void onMetadata(Metadata metadata) {} * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one passed. * Else does nothing. * + *

    This method must only be called if {@link #COMMAND_SET_VIDEO_SURFACE} is {@linkplain + * #getAvailableCommands() available}. + * * @param surfaceView The texture view to clear. */ void clearVideoSurfaceView(@Nullable SurfaceView surfaceView); @@ -2525,6 +2957,9 @@ default void onMetadata(Metadata metadata) {} *

    The thread that calls the {@link TextureView.SurfaceTextureListener} methods must be the * thread associated with {@link #getApplicationLooper()}. * + *

    This method must only be called if {@link #COMMAND_SET_VIDEO_SURFACE} is {@linkplain + * #getAvailableCommands() available}. + * * @param textureView The texture view. */ void setVideoTextureView(@Nullable TextureView textureView); @@ -2533,6 +2968,9 @@ default void onMetadata(Metadata metadata) {} * Clears the {@link TextureView} onto which video is being rendered if it matches the one passed. * Else does nothing. * + *

    This method must only be called if {@link #COMMAND_SET_VIDEO_SURFACE} is {@linkplain + * #getAvailableCommands() available}. + * * @param textureView The texture view to clear. */ void clearVideoTextureView(@Nullable TextureView textureView); @@ -2555,7 +2993,12 @@ default void onMetadata(Metadata metadata) {} @UnstableApi Size getSurfaceSize(); - /** Returns the current {@link CueGroup}. */ + /** + * Returns the current {@link CueGroup}. + * + *

    This method must only be called if {@link #COMMAND_GET_TEXT} is {@linkplain + * #getAvailableCommands() available}. + */ CueGroup getCurrentCues(); /** Gets the device information. */ @@ -2571,26 +3014,52 @@ default void onMetadata(Metadata metadata) {} * *

    For devices with {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote playback}, the volume of the * remote device is returned. + * + *

    This method must only be called if {@link #COMMAND_GET_DEVICE_VOLUME} is {@linkplain + * #getAvailableCommands() available}. */ @IntRange(from = 0) int getDeviceVolume(); - /** Gets whether the device is muted or not. */ + /** + * Gets whether the device is muted or not. + * + *

    This method must only be called if {@link #COMMAND_GET_DEVICE_VOLUME} is {@linkplain + * #getAvailableCommands() available}. + */ boolean isDeviceMuted(); /** * Sets the volume of the device. * + *

    This method must only be called if {@link #COMMAND_SET_DEVICE_VOLUME} is {@linkplain + * #getAvailableCommands() available}. + * * @param volume The volume to set. */ void setDeviceVolume(@IntRange(from = 0) int volume); - /** Increases the volume of the device. */ + /** + * Increases the volume of the device. + * + *

    This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME} is {@linkplain + * #getAvailableCommands() available}. + */ void increaseDeviceVolume(); - /** Decreases the volume of the device. */ + /** + * Decreases the volume of the device. + * + *

    This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME} is {@linkplain + * #getAvailableCommands() available}. + */ void decreaseDeviceVolume(); - /** Sets the mute state of the device. */ + /** + * Sets the mute state of the device. + * + *

    This method must only be called if {@link #COMMAND_ADJUST_DEVICE_VOLUME} is {@linkplain + * #getAvailableCommands() available}. + */ void setDeviceMuted(boolean muted); } From bc829695bc53c91f260093c8a29855c5e9b320f1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 21 Dec 2022 10:52:31 +0000 Subject: [PATCH 082/141] Add error messages to correctness assertions in SimpleBasePlayer Users of this class may run into these assertions when creating the State and they need to check the source code to understand why the State is invalid. Adding error messages to all our correctness assertions helps to understand the root cause more easily. PiperOrigin-RevId: 496875109 (cherry picked from commit 6c98f238e45d19a14041d58f5938f3399da04eb5) --- .../media3/common/SimpleBasePlayer.java | 100 +++++++++++------- 1 file changed, 64 insertions(+), 36 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index 6ab13a7a373..842f63912be 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -549,7 +549,7 @@ public Builder setTimedMetadata(Metadata timedMetadata) { public Builder setPlaylist(List playlist) { HashSet uids = new HashSet<>(); for (int i = 0; i < playlist.size(); i++) { - checkArgument(uids.add(playlist.get(i).uid)); + checkArgument(uids.add(playlist.get(i).uid), "Duplicate MediaItemData UID in playlist"); } this.playlist = ImmutableList.copyOf(playlist); this.timeline = new PlaylistTimeline(this.playlist); @@ -882,16 +882,20 @@ private State(Builder builder) { if (builder.timeline.isEmpty()) { checkArgument( builder.playbackState == Player.STATE_IDLE - || builder.playbackState == Player.STATE_ENDED); + || builder.playbackState == Player.STATE_ENDED, + "Empty playlist only allowed in STATE_IDLE or STATE_ENDED"); checkArgument( builder.currentAdGroupIndex == C.INDEX_UNSET - && builder.currentAdIndexInAdGroup == C.INDEX_UNSET); + && builder.currentAdIndexInAdGroup == C.INDEX_UNSET, + "Ads not allowed if playlist is empty"); } else { int mediaItemIndex = builder.currentMediaItemIndex; if (mediaItemIndex == C.INDEX_UNSET) { mediaItemIndex = 0; // TODO: Use shuffle order to find first index. } else { - checkArgument(builder.currentMediaItemIndex < builder.timeline.getWindowCount()); + checkArgument( + builder.currentMediaItemIndex < builder.timeline.getWindowCount(), + "currentMediaItemIndex must be less than playlist.size()"); } if (builder.currentAdGroupIndex != C.INDEX_UNSET) { Timeline.Period period = new Timeline.Period(); @@ -904,19 +908,25 @@ private State(Builder builder) { getPeriodIndexFromWindowPosition( builder.timeline, mediaItemIndex, contentPositionMs, window, period); builder.timeline.getPeriod(periodIndex, period); - checkArgument(builder.currentAdGroupIndex < period.getAdGroupCount()); + checkArgument( + builder.currentAdGroupIndex < period.getAdGroupCount(), + "PeriodData has less ad groups than adGroupIndex"); int adCountInGroup = period.getAdCountInAdGroup(builder.currentAdGroupIndex); if (adCountInGroup != C.LENGTH_UNSET) { - checkArgument(builder.currentAdIndexInAdGroup < adCountInGroup); + checkArgument( + builder.currentAdIndexInAdGroup < adCountInGroup, + "Ad group has less ads than adIndexInGroupIndex"); } } } if (builder.playerError != null) { - checkArgument(builder.playbackState == Player.STATE_IDLE); + checkArgument( + builder.playbackState == Player.STATE_IDLE, "Player error only allowed in STATE_IDLE"); } if (builder.playbackState == Player.STATE_IDLE || builder.playbackState == Player.STATE_ENDED) { - checkArgument(!builder.isLoading); + checkArgument( + !builder.isLoading, "isLoading only allowed when not in STATE_IDLE or STATE_ENDED"); } PositionSupplier contentPositionMsSupplier = builder.contentPositionMsSupplier; if (builder.contentPositionMs != null) { @@ -1494,9 +1504,12 @@ public Builder setIsPlaceholder(boolean isPlaceholder) { public Builder setPeriods(List periods) { int periodCount = periods.size(); for (int i = 0; i < periodCount - 1; i++) { - checkArgument(periods.get(i).durationUs != C.TIME_UNSET); + checkArgument( + periods.get(i).durationUs != C.TIME_UNSET, "Periods other than last need a duration"); for (int j = i + 1; j < periodCount; j++) { - checkArgument(!periods.get(i).uid.equals(periods.get(j).uid)); + checkArgument( + !periods.get(i).uid.equals(periods.get(j).uid), + "Duplicate PeriodData UIDs in period list"); } } this.periods = ImmutableList.copyOf(periods); @@ -1575,16 +1588,26 @@ public MediaItemData build() { private MediaItemData(Builder builder) { if (builder.liveConfiguration == null) { - checkArgument(builder.presentationStartTimeMs == C.TIME_UNSET); - checkArgument(builder.windowStartTimeMs == C.TIME_UNSET); - checkArgument(builder.elapsedRealtimeEpochOffsetMs == C.TIME_UNSET); + checkArgument( + builder.presentationStartTimeMs == C.TIME_UNSET, + "presentationStartTimeMs can only be set if liveConfiguration != null"); + checkArgument( + builder.windowStartTimeMs == C.TIME_UNSET, + "windowStartTimeMs can only be set if liveConfiguration != null"); + checkArgument( + builder.elapsedRealtimeEpochOffsetMs == C.TIME_UNSET, + "elapsedRealtimeEpochOffsetMs can only be set if liveConfiguration != null"); } else if (builder.presentationStartTimeMs != C.TIME_UNSET && builder.windowStartTimeMs != C.TIME_UNSET) { - checkArgument(builder.windowStartTimeMs >= builder.presentationStartTimeMs); + checkArgument( + builder.windowStartTimeMs >= builder.presentationStartTimeMs, + "windowStartTimeMs can't be less than presentationStartTimeMs"); } int periodCount = builder.periods.size(); if (builder.durationUs != C.TIME_UNSET) { - checkArgument(builder.defaultPositionUs <= builder.durationUs); + checkArgument( + builder.defaultPositionUs <= builder.durationUs, + "defaultPositionUs can't be greater than durationUs"); } this.uid = builder.uid; this.tracks = builder.tracks; @@ -2782,7 +2805,7 @@ protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { */ @ForOverride protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_PLAY_PAUSE"); } /** @@ -2795,7 +2818,7 @@ protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { */ @ForOverride protected ListenableFuture handlePrepare() { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_PREPARE"); } /** @@ -2808,7 +2831,7 @@ protected ListenableFuture handlePrepare() { */ @ForOverride protected ListenableFuture handleStop() { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_STOP"); } /** @@ -2820,7 +2843,7 @@ protected ListenableFuture handleStop() { // TODO(b/261158047): Add that this method will only be called if COMMAND_RELEASE is available. @ForOverride protected ListenableFuture handleRelease() { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_RELEASE"); } /** @@ -2834,7 +2857,7 @@ protected ListenableFuture handleRelease() { */ @ForOverride protected ListenableFuture handleSetRepeatMode(@RepeatMode int repeatMode) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_SET_REPEAT_MODE"); } /** @@ -2848,7 +2871,7 @@ protected ListenableFuture handleSetRepeatMode(@RepeatMode int repeatMode) { */ @ForOverride protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEnabled) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_SET_SHUFFLE_MODE"); } /** @@ -2862,7 +2885,7 @@ protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEna */ @ForOverride protected ListenableFuture handleSetPlaybackParameters(PlaybackParameters playbackParameters) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_SET_SPEED_AND_PITCH"); } /** @@ -2877,7 +2900,8 @@ protected ListenableFuture handleSetPlaybackParameters(PlaybackParameters pla @ForOverride protected ListenableFuture handleSetTrackSelectionParameters( TrackSelectionParameters trackSelectionParameters) { - throw new IllegalStateException(); + throw new IllegalStateException( + "Missing implementation to handle COMMAND_SET_TRACK_SELECTION_PARAMETERS"); } /** @@ -2891,7 +2915,8 @@ protected ListenableFuture handleSetTrackSelectionParameters( */ @ForOverride protected ListenableFuture handleSetPlaylistMetadata(MediaMetadata playlistMetadata) { - throw new IllegalStateException(); + throw new IllegalStateException( + "Missing implementation to handle COMMAND_SET_MEDIA_ITEMS_METADATA"); } /** @@ -2906,7 +2931,7 @@ protected ListenableFuture handleSetPlaylistMetadata(MediaMetadata playlistMe */ @ForOverride protected ListenableFuture handleSetVolume(@FloatRange(from = 0, to = 1.0) float volume) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_SET_VOLUME"); } /** @@ -2920,7 +2945,7 @@ protected ListenableFuture handleSetVolume(@FloatRange(from = 0, to = 1.0) fl */ @ForOverride protected ListenableFuture handleSetDeviceVolume(@IntRange(from = 0) int deviceVolume) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_SET_DEVICE_VOLUME"); } /** @@ -2933,7 +2958,8 @@ protected ListenableFuture handleSetDeviceVolume(@IntRange(from = 0) int devi */ @ForOverride protected ListenableFuture handleIncreaseDeviceVolume() { - throw new IllegalStateException(); + throw new IllegalStateException( + "Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME"); } /** @@ -2946,7 +2972,8 @@ protected ListenableFuture handleIncreaseDeviceVolume() { */ @ForOverride protected ListenableFuture handleDecreaseDeviceVolume() { - throw new IllegalStateException(); + throw new IllegalStateException( + "Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME"); } /** @@ -2960,7 +2987,8 @@ protected ListenableFuture handleDecreaseDeviceVolume() { */ @ForOverride protected ListenableFuture handleSetDeviceMuted(boolean muted) { - throw new IllegalStateException(); + throw new IllegalStateException( + "Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME"); } /** @@ -2975,7 +3003,7 @@ protected ListenableFuture handleSetDeviceMuted(boolean muted) { */ @ForOverride protected ListenableFuture handleSetVideoOutput(Object videoOutput) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_SET_VIDEO_SURFACE"); } /** @@ -2992,7 +3020,7 @@ protected ListenableFuture handleSetVideoOutput(Object videoOutput) { */ @ForOverride protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutput) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_SET_VIDEO_SURFACE"); } /** @@ -3013,7 +3041,7 @@ protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutpu @ForOverride protected ListenableFuture handleSetMediaItems( List mediaItems, int startIndex, long startPositionMs) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_SET_MEDIA_ITEM(S)"); } /** @@ -3029,7 +3057,7 @@ protected ListenableFuture handleSetMediaItems( */ @ForOverride protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_CHANGE_MEDIA_ITEMS"); } /** @@ -3049,7 +3077,7 @@ protected ListenableFuture handleAddMediaItems(int index, List med */ @ForOverride protected ListenableFuture handleMoveMediaItems(int fromIndex, int toIndex, int newIndex) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_CHANGE_MEDIA_ITEMS"); } /** @@ -3066,7 +3094,7 @@ protected ListenableFuture handleMoveMediaItems(int fromIndex, int toIndex, i */ @ForOverride protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_CHANGE_MEDIA_ITEMS"); } /** @@ -3087,7 +3115,7 @@ protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) @ForOverride protected ListenableFuture handleSeek( int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle one of the COMMAND_SEEK_*"); } @RequiresNonNull("state") From 7d3375c6ec09f13fafe6d9f55b8a975f349cc4f3 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 21 Dec 2022 10:58:36 +0000 Subject: [PATCH 083/141] Fix recursive loop when registering controller visibility listeners There are two overloads of this method due to a type 'rename' from `PlayerControlView.VisibilityListener` to `PlayerView.ControllerVisibilityListener`. Currently when you call one overload it passes `null` to the other one (to clear the other listener). Unfortunately this results in it clearing itself, because it receives a null call back! This change tweaks the documentation to clarify that the 'other' listener is only cleared if you pass a non-null listener in. This solves the recursive problem, and allows the 'legacy' visibility listener to be successfully registered. Issue: androidx/media#229 #minor-release PiperOrigin-RevId: 496876397 (cherry picked from commit 4087a011e21aba2c27e3ae890f74a65812c6f4ce) --- RELEASENOTES.md | 5 +++++ .../main/java/androidx/media3/ui/PlayerView.java | 14 ++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f0c4f888924..7e9f8fd2b59 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,11 @@ `Subtitle.getEventTime` if a subtitle file contains no cues. * SubRip: Add support for UTF-16 files if they start with a byte order mark. +* UI: + * Fix the deprecated + `PlayerView.setControllerVisibilityListener(PlayerControlView.VisibilityListener)` + to ensure visibility changes are passed to the registered listener + ([#229](https://github.com/androidx/media/issues/229)). * Session: * Add abstract `SimpleBasePlayer` to help implement the `Player` interface for custom players. diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java index 6731d040a3e..b91d14cb5fc 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java @@ -887,8 +887,8 @@ public void setControllerHideDuringAds(boolean controllerHideDuringAds) { /** * Sets the {@link PlayerControlView.VisibilityListener}. * - *

    Removes any listener set by {@link - * #setControllerVisibilityListener(PlayerControlView.VisibilityListener)}. + *

    If {@code listener} is non-null then any listener set by {@link + * #setControllerVisibilityListener(PlayerControlView.VisibilityListener)} is removed. * * @param listener The listener to be notified about visibility changes, or null to remove the * current listener. @@ -896,14 +896,16 @@ public void setControllerHideDuringAds(boolean controllerHideDuringAds) { @SuppressWarnings("deprecation") // Clearing the legacy listener. public void setControllerVisibilityListener(@Nullable ControllerVisibilityListener listener) { this.controllerVisibilityListener = listener; - setControllerVisibilityListener((PlayerControlView.VisibilityListener) null); + if (listener != null) { + setControllerVisibilityListener((PlayerControlView.VisibilityListener) null); + } } /** * Sets the {@link PlayerControlView.VisibilityListener}. * - *

    Removes any listener set by {@link - * #setControllerVisibilityListener(ControllerVisibilityListener)}. + *

    If {@code listener} is non-null then any listener set by {@link + * #setControllerVisibilityListener(ControllerVisibilityListener)} is removed. * * @deprecated Use {@link #setControllerVisibilityListener(ControllerVisibilityListener)} instead. */ @@ -923,8 +925,8 @@ public void setControllerVisibilityListener( this.legacyControllerVisibilityListener = listener; if (listener != null) { controller.addVisibilityListener(listener); + setControllerVisibilityListener((ControllerVisibilityListener) null); } - setControllerVisibilityListener((ControllerVisibilityListener) null); } /** From 11b0baa140ccf211f5946c87d09114c52bc58911 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 21 Dec 2022 16:00:14 +0000 Subject: [PATCH 084/141] Update migration script Issue: google/ExoPlayer#10854 PiperOrigin-RevId: 496922055 (cherry picked from commit 50090e39273356bee9b8da6b2f4a4dba1206f9a8) --- github/media3-migration.sh | 386 ------------------------------------- 1 file changed, 386 deletions(-) delete mode 100644 github/media3-migration.sh diff --git a/github/media3-migration.sh b/github/media3-migration.sh deleted file mode 100644 index f80ac4dfa35..00000000000 --- a/github/media3-migration.sh +++ /dev/null @@ -1,386 +0,0 @@ -#!/bin/bash -# Copyright (C) 2022 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -## -shopt -s extglob - -PACKAGE_MAPPINGS='com.google.android.exoplayer2 androidx.media3.exoplayer -com.google.android.exoplayer2.analytics androidx.media3.exoplayer.analytics -com.google.android.exoplayer2.audio androidx.media3.exoplayer.audio -com.google.android.exoplayer2.castdemo androidx.media3.demo.cast -com.google.android.exoplayer2.database androidx.media3.database -com.google.android.exoplayer2.decoder androidx.media3.decoder -com.google.android.exoplayer2.demo androidx.media3.demo.main -com.google.android.exoplayer2.drm androidx.media3.exoplayer.drm -com.google.android.exoplayer2.ext.av1 androidx.media3.decoder.av1 -com.google.android.exoplayer2.ext.cast androidx.media3.cast -com.google.android.exoplayer2.ext.cronet androidx.media3.datasource.cronet -com.google.android.exoplayer2.ext.ffmpeg androidx.media3.decoder.ffmpeg -com.google.android.exoplayer2.ext.flac androidx.media3.decoder.flac -com.google.android.exoplayer2.ext.ima androidx.media3.exoplayer.ima -com.google.android.exoplayer2.ext.leanback androidx.media3.ui.leanback -com.google.android.exoplayer2.ext.okhttp androidx.media3.datasource.okhttp -com.google.android.exoplayer2.ext.opus androidx.media3.decoder.opus -com.google.android.exoplayer2.ext.rtmp androidx.media3.datasource.rtmp -com.google.android.exoplayer2.ext.vp9 androidx.media3.decoder.vp9 -com.google.android.exoplayer2.ext.workmanager androidx.media3.exoplayer.workmanager -com.google.android.exoplayer2.extractor androidx.media3.extractor -com.google.android.exoplayer2.gldemo androidx.media3.demo.gl -com.google.android.exoplayer2.mediacodec androidx.media3.exoplayer.mediacodec -com.google.android.exoplayer2.metadata androidx.media3.extractor.metadata -com.google.android.exoplayer2.offline androidx.media3.exoplayer.offline -com.google.android.exoplayer2.playbacktests androidx.media3.test.exoplayer.playback -com.google.android.exoplayer2.robolectric androidx.media3.test.utils.robolectric -com.google.android.exoplayer2.scheduler androidx.media3.exoplayer.scheduler -com.google.android.exoplayer2.source androidx.media3.exoplayer.source -com.google.android.exoplayer2.source.dash androidx.media3.exoplayer.dash -com.google.android.exoplayer2.source.hls androidx.media3.exoplayer.hls -com.google.android.exoplayer2.source.rtsp androidx.media3.exoplayer.rtsp -com.google.android.exoplayer2.source.smoothstreaming androidx.media3.exoplayer.smoothstreaming -com.google.android.exoplayer2.surfacedemo androidx.media3.demo.surface -com.google.android.exoplayer2.testdata androidx.media3.test.data -com.google.android.exoplayer2.testutil androidx.media3.test.utils -com.google.android.exoplayer2.text androidx.media3.extractor.text -com.google.android.exoplayer2.trackselection androidx.media3.exoplayer.trackselection -com.google.android.exoplayer2.transformer androidx.media3.transformer -com.google.android.exoplayer2.transformerdemo androidx.media3.demo.transformer -com.google.android.exoplayer2.ui androidx.media3.ui -com.google.android.exoplayer2.upstream androidx.media3.datasource -com.google.android.exoplayer2.upstream.cache androidx.media3.datasource.cache -com.google.android.exoplayer2.upstream.crypto androidx.media3.exoplayer.upstream.crypto -com.google.android.exoplayer2.util androidx.media3.common.util -com.google.android.exoplayer2.util androidx.media3.exoplayer.util -com.google.android.exoplayer2.video androidx.media3.exoplayer.video' - - -CLASS_RENAMINGS='com.google.android.exoplayer2.ui.StyledPlayerView androidx.media3.ui.PlayerView -StyledPlayerView PlayerView -com.google.android.exoplayer2.ui.StyledPlayerControlView androidx.media3.ui.PlayerControlView -StyledPlayerControlView PlayerControlView -com.google.android.exoplayer2.ExoPlayerLibraryInfo androidx.media3.common.MediaLibraryInfo -ExoPlayerLibraryInfo MediaLibraryInfo -com.google.android.exoplayer2.SimpleExoPlayer androidx.media3.exoplayer.ExoPlayer -SimpleExoPlayer ExoPlayer' - -CLASS_MAPPINGS='com.google.android.exoplayer2.text.span androidx.media3.common.text HorizontalTextInVerticalContextSpan LanguageFeatureSpan RubySpan SpanUtil TextAnnotation TextEmphasisSpan -com.google.android.exoplayer2.text androidx.media3.common.text CueGroup Cue -com.google.android.exoplayer2.text androidx.media3.exoplayer.text ExoplayerCuesDecoder SubtitleDecoderFactory TextOutput TextRenderer -com.google.android.exoplayer2.upstream.crypto androidx.media3.datasource AesCipherDataSource AesCipherDataSink AesFlushingCipher -com.google.android.exoplayer2.util androidx.media3.common.util AtomicFile Assertions BundleableUtil BundleUtil Clock ClosedSource CodecSpecificDataUtil ColorParser ConditionVariable Consumer CopyOnWriteMultiset EGLSurfaceTexture GlProgram GlUtil HandlerWrapper LibraryLoader ListenerSet Log LongArray MediaFormatUtil NetworkTypeObserver NonNullApi NotificationUtil ParsableBitArray ParsableByteArray RepeatModeUtil RunnableFutureTask SystemClock SystemHandlerWrapper TimedValueQueue TimestampAdjuster TraceUtil UnknownNull UnstableApi UriUtil Util XmlPullParserUtil -com.google.android.exoplayer2.util androidx.media3.common ErrorMessageProvider FlagSet FileTypes MimeTypes PriorityTaskManager -com.google.android.exoplayer2.metadata androidx.media3.common Metadata -com.google.android.exoplayer2.metadata androidx.media3.exoplayer.metadata MetadataDecoderFactory MetadataOutput MetadataRenderer -com.google.android.exoplayer2.audio androidx.media3.common AudioAttributes AuxEffectInfo -com.google.android.exoplayer2.ui androidx.media3.common AdOverlayInfo AdViewProvider -com.google.android.exoplayer2.source.ads androidx.media3.common AdPlaybackState -com.google.android.exoplayer2.source androidx.media3.common MediaPeriodId TrackGroup -com.google.android.exoplayer2.offline androidx.media3.common StreamKey -com.google.android.exoplayer2.ui androidx.media3.exoplayer.offline DownloadNotificationHelper -com.google.android.exoplayer2.trackselection androidx.media3.common TrackSelectionParameters TrackSelectionOverride -com.google.android.exoplayer2.video androidx.media3.common ColorInfo VideoSize -com.google.android.exoplayer2.upstream androidx.media3.common DataReader -com.google.android.exoplayer2.upstream androidx.media3.exoplayer.upstream Allocation Allocator BandwidthMeter CachedRegionTracker DefaultAllocator DefaultBandwidthMeter DefaultLoadErrorHandlingPolicy Loader LoaderErrorThrower ParsingLoadable SlidingPercentile TimeToFirstByteEstimator -com.google.android.exoplayer2.audio androidx.media3.extractor AacUtil Ac3Util Ac4Util DtsUtil MpegAudioUtil OpusUtil WavUtil -com.google.android.exoplayer2.util androidx.media3.extractor NalUnitUtil ParsableNalUnitBitArray -com.google.android.exoplayer2.video androidx.media3.extractor AvcConfig DolbyVisionConfig HevcConfig -com.google.android.exoplayer2.decoder androidx.media3.exoplayer DecoderCounters DecoderReuseEvaluation -com.google.android.exoplayer2.util androidx.media3.exoplayer MediaClock StandaloneMediaClock -com.google.android.exoplayer2 androidx.media3.exoplayer FormatHolder PlayerMessage -com.google.android.exoplayer2 androidx.media3.common BasePlayer BundleListRetriever Bundleable ControlDispatcher C DefaultControlDispatcher DeviceInfo ErrorMessageProvider ExoPlayerLibraryInfo Format ForwardingPlayer HeartRating IllegalSeekPositionException MediaItem MediaMetadata ParserException PercentageRating PlaybackException PlaybackParameters Player PositionInfo Rating StarRating ThumbRating Timeline Tracks -com.google.android.exoplayer2.drm androidx.media3.common DrmInitData' - -DEPENDENCY_MAPPINGS='exoplayer media3-exoplayer -exoplayer-common media3-common -exoplayer-core media3-exoplayer -exoplayer-dash media3-exoplayer-dash -exoplayer-database media3-database -exoplayer-datasource media-datasource -exoplayer-decoder media3-decoder -exoplayer-extractor media3-extractor -exoplayer-hls media3-exoplayer-hls -exoplayer-robolectricutils media3-test-utils-robolectric -exoplayer-rtsp media3-exoplayer-rtsp -exoplayer-smoothstreaming media3-exoplayer-smoothstreaming -exoplayer-testutils media3-test-utils -exoplayer-transformer media3-transformer -exoplayer-ui media3-ui -extension-cast media3-cast -extension-cronet media3-datasource-cronet -extension-ima media3-exoplayer-ima -extension-leanback media3-ui-leanback -extension-okhttp media3-datasource-okhttp -extension-rtmp media3-datasource-rtmp -extension-workmanager media3-exoplayer-workmanager' - -# Rewrites classes, packages and dependencies from the legacy ExoPlayer package structure -# to androidx.media3 structure. - -MEDIA3_VERSION="1.0.0-beta02" -LEGACY_PEER_VERSION="2.18.1" - -function usage() { - echo "usage: $0 [-p|-c|-d|-v]|[-m|-l [-x ] [-f] PROJECT_ROOT]" - echo " PROJECT_ROOT: path to your project root (location of 'gradlew')" - echo " -p: list package mappings and then exit" - echo " -c: list class mappings (precedence over package mappings) and then exit" - echo " -d: list dependency mappings and then exit" - echo " -m: migrate packages, classes and dependencies to AndroidX Media3" - echo " -l: list files that will be considered for rewrite and then exit" - echo " -x: exclude the path from the list of file to be changed: 'app/src/test'" - echo " -f: force the action even when validation fails" - echo " -v: print the exoplayer2/media3 version strings of this script and exit" - echo " --noclean : Do not call './gradlew clean' in project directory." - echo " -h, --help: show this help text" -} - -function print_pairs { - while read -r line; - do - IFS=' ' read -ra PAIR <<< "$line" - printf "%-55s %-30s\n" "${PAIR[0]}" "${PAIR[1]}" - done <<< "$(echo "$@")" -} - -function print_class_mappings { - while read -r mapping; - do - old=$(echo "$mapping" | cut -d ' ' -f1) - new=$(echo "$mapping" | cut -d ' ' -f2) - classes=$(echo "$mapping" | cut -d ' ' -f3-) - for clazz in $classes; - do - printf "%-80s %-30s\n" "$old.$clazz" "$new.$clazz" - done - done <<< "$(echo "$CLASS_MAPPINGS" | sort)" -} - -ERROR_COUNTER=0 -VALIDATION_ERRORS='' - -function add_validation_error { - let ERROR_COUNTER++ - VALIDATION_ERRORS+="\033[31m[$ERROR_COUNTER] ->\033[0m ${1}" -} - -function validate_exoplayer_version() { - has_exoplayer_dependency='' - while read -r file; - do - local version - version=$(grep -m 1 "com\.google\.android\.exoplayer:" "$file" | cut -d ":" -f3 | tr -d \" | tr -d \') - if [[ ! -z $version ]] && [[ ! "$version" =~ $LEGACY_PEER_VERSION ]]; - then - add_validation_error "The version does not match '$LEGACY_PEER_VERSION'. \ -Update to '$LEGACY_PEER_VERSION' or use the migration script matching your \ -current version. Current version '$version' found in\n $file\n" - fi - done <<< "$(find . -type f -name "build.gradle")" -} - -function validate_string_not_contained { - local pattern=$1 # regex - local failure_message=$2 - while read -r file; - do - if grep -q -e "$pattern" "$file"; - then - add_validation_error "$failure_message:\n $file\n" - fi - done <<< "$files" -} - -function validate_string_patterns { - validate_string_not_contained \ - 'com\.google\.android\.exoplayer2\..*\*' \ - 'Replace wildcard import statements with fully qualified import statements'; - validate_string_not_contained \ - 'com\.google\.android\.exoplayer2\.ui\.PlayerView' \ - 'Migrate PlayerView to StyledPlayerView before migrating'; - validate_string_not_contained \ - 'LegacyPlayerView' \ - 'Migrate LegacyPlayerView to StyledPlayerView before migrating'; - validate_string_not_contained \ - 'com\.google\.android\.exoplayer2\.ext\.mediasession' \ - 'The MediaSessionConnector is integrated in androidx.media3.session.MediaSession' -} - -SED_CMD_INPLACE='sed -i ' -if [[ "$OSTYPE" == "darwin"* ]]; then - SED_CMD_INPLACE="sed -i '' " -fi - -MIGRATE_FILES='1' -LIST_FILES_ONLY='1' -PRINT_CLASS_MAPPINGS='1' -PRINT_PACKAGE_MAPPINGS='1' -PRINT_DEPENDENCY_MAPPINGS='1' -PRINT_VERSION='1' -NO_CLEAN='1' -FORCE='1' -IGNORE_VERSION='1' -EXCLUDED_PATHS='' - -while [[ $1 =~ ^-.* ]]; -do - case "$1" in - -m ) MIGRATE_FILES='';; - -l ) LIST_FILES_ONLY='';; - -c ) PRINT_CLASS_MAPPINGS='';; - -p ) PRINT_PACKAGE_MAPPINGS='';; - -d ) PRINT_DEPENDENCY_MAPPINGS='';; - -v ) PRINT_VERSION='';; - -f ) FORCE='';; - -x ) shift; EXCLUDED_PATHS="$(printf "%s\n%s" $EXCLUDED_PATHS $1)";; - --noclean ) NO_CLEAN='';; - * ) usage && exit 1;; - esac - shift -done - -if [[ -z $PRINT_DEPENDENCY_MAPPINGS ]]; -then - print_pairs "$DEPENDENCY_MAPPINGS" - exit 0 -elif [[ -z $PRINT_PACKAGE_MAPPINGS ]]; -then - print_pairs "$PACKAGE_MAPPINGS" - exit 0 -elif [[ -z $PRINT_CLASS_MAPPINGS ]]; -then - print_class_mappings - exit 0 -elif [[ -z $PRINT_VERSION ]]; -then - echo "$LEGACY_PEER_VERSION -> $MEDIA3_VERSION. This script is written to migrate from ExoPlayer $LEGACY_PEER_VERSION to AndroidX Media3 $MEDIA3_VERSION" - exit 0 -elif [[ -z $1 ]]; -then - usage - exit 1 -fi - -if [[ ! -f $1/gradlew ]]; -then - echo "directory seems not to exist or is not a gradle project (missing 'gradlew')" - usage - exit 1 -fi - -PROJECT_ROOT=$1 -cd "$PROJECT_ROOT" - -# Create the set of files to transform -exclusion="/build/|/.idea/|/res/drawable|/res/color|/res/mipmap|/res/values|" -if [[ ! -z $EXCLUDED_PATHS ]]; -then - while read -r path; - do - exclusion="$exclusion./$path|" - done <<< "$EXCLUDED_PATHS" -fi -files=$(find . -name '*\.java' -o -name '*\.kt' -o -name '*\.xml' | grep -Ev "'$exclusion'") - -# Validate project and exit in case of validation errors -validate_string_patterns -validate_exoplayer_version "$PROJECT_ROOT" -if [[ ! -z $FORCE && ! -z "$VALIDATION_ERRORS" ]]; -then - echo "=============================================" - echo "Validation errors (use -f to force execution)" - echo "---------------------------------------------" - echo -e "$VALIDATION_ERRORS" - exit 1 -fi - -if [[ -z $LIST_FILES_ONLY ]]; -then - echo "$files" | cut -c 3- - find . -type f -name 'build\.gradle' | cut -c 3- - exit 0 -fi - -# start migration after successful validation or when forced to disregard validation -# errors - -if [[ ! -z "$MIGRATE_FILES" ]]; -then - echo "nothing to do" - usage - exit 0 -fi - -PWD=$(pwd) -if [[ ! -z $NO_CLEAN ]]; -then - cd "$PROJECT_ROOT" - ./gradlew clean - cd "$PWD" -fi - -# create expressions for class renamings -renaming_expressions='' -while read -r renaming; -do - src=$(echo "$renaming" | cut -d ' ' -f1 | sed -e 's/\./\\\./g') - dest=$(echo "$renaming" | cut -d ' ' -f2) - renaming_expressions+="-e s/$src/$dest/g " -done <<< "$CLASS_RENAMINGS" - -# create expressions for class mappings -classes_expressions='' -while read -r mapping; -do - src=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g') - dest=$(echo "$mapping" | cut -d ' ' -f2) - classes=$(echo "$mapping" | cut -d ' ' -f3-) - for clazz in $classes; - do - classes_expressions+="-e s/$src\.$clazz/$dest.$clazz/g " - done -done <<< "$CLASS_MAPPINGS" - -# create expressions for package mappings -packages_expressions='' -while read -r mapping; -do - src=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g') - dest=$(echo "$mapping" | cut -d ' ' -f2) - packages_expressions+="-e s/$src/$dest/g " -done <<< "$PACKAGE_MAPPINGS" - -# do search and replace with expressions in each selected file -while read -r file; -do - echo "migrating $file" - expr="$renaming_expressions $classes_expressions $packages_expressions" - $SED_CMD_INPLACE $expr $file -done <<< "$files" - -# create expressions for dependencies in gradle files -EXOPLAYER_GROUP="com\.google\.android\.exoplayer" -MEDIA3_GROUP="androidx.media3" -dependency_expressions="" -while read -r mapping -do - OLD=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g') - NEW=$(echo "$mapping" | cut -d ' ' -f2) - dependency_expressions="$dependency_expressions -e s/$EXOPLAYER_GROUP:$OLD:.*\"/$MEDIA3_GROUP:$NEW:$MEDIA3_VERSION\"/g -e s/$EXOPLAYER_GROUP:$OLD:.*'/$MEDIA3_GROUP:$NEW:$MEDIA3_VERSION'/" -done <<< "$DEPENDENCY_MAPPINGS" - -## do search and replace for dependencies in gradle files -while read -r build_file; -do - echo "migrating build file $build_file" - $SED_CMD_INPLACE $dependency_expressions $build_file -done <<< "$(find . -type f -name 'build\.gradle')" From 13b72c478a250a01383ab48c0e662d5cd4fcbfd4 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 21 Dec 2022 18:04:42 +0000 Subject: [PATCH 085/141] Bump IMA SDK version to 3.29.0 Issue: google/ExoPlayer#10845 PiperOrigin-RevId: 496947392 (cherry picked from commit 63352e97e99cc6ab2e063906be63392ea8b984b3) --- RELEASENOTES.md | 2 ++ libraries/exoplayer_ima/build.gradle | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7e9f8fd2b59..14685340d98 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -45,6 +45,8 @@ next release. * Cast extension * Bump Cast SDK version to 21.2.0. +* IMA extension + * Bump IMA SDK version to 3.29.0. ### 1.0.0-beta03 (2022-11-22) diff --git a/libraries/exoplayer_ima/build.gradle b/libraries/exoplayer_ima/build.gradle index cc8d1ef0e71..446fe90a297 100644 --- a/libraries/exoplayer_ima/build.gradle +++ b/libraries/exoplayer_ima/build.gradle @@ -25,7 +25,7 @@ android { } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.28.1' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.29.0' implementation project(modulePrefix + 'lib-exoplayer') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion From e07c887bcd97a1bf2ce98502e285b14de6f42b25 Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 21 Dec 2022 18:10:19 +0000 Subject: [PATCH 086/141] Check `MediaMetadata` bundle to verify keys are skipped Added another check in test to make sure we don't add keys to bundle for fields with `null` values. PiperOrigin-RevId: 496948705 (cherry picked from commit 13c93a3dd693e86e6d5208aff45105000858363f) --- .../androidx/media3/common/MediaMetadataTest.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java index bde20bc6037..d2810ddd1b6 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java @@ -107,12 +107,17 @@ public void populate_populatesEveryField() { } @Test - public void createMinimalMediaMetadata_roundTripViaBundle_yieldsEqualInstance() { + public void toBundleSkipsDefaultValues_fromBundleRestoresThem() { MediaMetadata mediaMetadata = new MediaMetadata.Builder().build(); - MediaMetadata mediaMetadataFromBundle = - MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle()); + Bundle mediaMetadataBundle = mediaMetadata.toBundle(); + + // check Bundle created above, contains no keys. + assertThat(mediaMetadataBundle.keySet()).isEmpty(); + + MediaMetadata mediaMetadataFromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadataBundle); + // check object retrieved from mediaMetadataBundle is equal to mediaMetadata. assertThat(mediaMetadataFromBundle).isEqualTo(mediaMetadata); // Extras is not implemented in MediaMetadata.equals(Object o). assertThat(mediaMetadataFromBundle.extras).isNull(); From 0f8b861923178858a15e91c271f7034e4fc620dc Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 21 Dec 2022 21:35:56 +0000 Subject: [PATCH 087/141] Optimise bundling for `AdPlaybackState` using `AdPlaybackState.NONE` Did not do this optimisation for `AdPlaybackState.AdGroup` as its length is zero for `AdPlaybackState` with no ads. No need to pass default values while fetching keys, which we always set in `AdPlaybackState.AdGroup.toBundle()`. PiperOrigin-RevId: 496995048 (cherry picked from commit 7fc2cdbe1bdf9968a1e73a670e5e32454090e1bd) --- .../media3/common/AdPlaybackState.java | 32 ++++++++++----- .../media3/common/AdPlaybackStateTest.java | 40 ++++++++++++++++++- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java index 8efa8f218d8..ec9f23199d7 100644 --- a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java +++ b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java @@ -507,9 +507,8 @@ public Bundle toBundle() { @SuppressWarnings("nullness:type.argument") private static AdGroup fromBundle(Bundle bundle) { long timeUs = bundle.getLong(keyForField(FIELD_TIME_US)); - int count = bundle.getInt(keyForField(FIELD_COUNT), /* defaultValue= */ C.LENGTH_UNSET); - int originalCount = - bundle.getInt(keyForField(FIELD_ORIGINAL_COUNT), /* defaultValue= */ C.LENGTH_UNSET); + int count = bundle.getInt(keyForField(FIELD_COUNT)); + int originalCount = bundle.getInt(keyForField(FIELD_ORIGINAL_COUNT)); @Nullable ArrayList<@NullableType Uri> uriList = bundle.getParcelableArrayList(keyForField(FIELD_URIS)); @Nullable @@ -1152,10 +1151,18 @@ public Bundle toBundle() { for (AdGroup adGroup : adGroups) { adGroupBundleList.add(adGroup.toBundle()); } - bundle.putParcelableArrayList(keyForField(FIELD_AD_GROUPS), adGroupBundleList); - bundle.putLong(keyForField(FIELD_AD_RESUME_POSITION_US), adResumePositionUs); - bundle.putLong(keyForField(FIELD_CONTENT_DURATION_US), contentDurationUs); - bundle.putInt(keyForField(FIELD_REMOVED_AD_GROUP_COUNT), removedAdGroupCount); + if (!adGroupBundleList.isEmpty()) { + bundle.putParcelableArrayList(keyForField(FIELD_AD_GROUPS), adGroupBundleList); + } + if (adResumePositionUs != NONE.adResumePositionUs) { + bundle.putLong(keyForField(FIELD_AD_RESUME_POSITION_US), adResumePositionUs); + } + if (contentDurationUs != NONE.contentDurationUs) { + bundle.putLong(keyForField(FIELD_CONTENT_DURATION_US), contentDurationUs); + } + if (removedAdGroupCount != NONE.removedAdGroupCount) { + bundle.putInt(keyForField(FIELD_REMOVED_AD_GROUP_COUNT), removedAdGroupCount); + } return bundle; } @@ -1180,10 +1187,15 @@ private static AdPlaybackState fromBundle(Bundle bundle) { } } long adResumePositionUs = - bundle.getLong(keyForField(FIELD_AD_RESUME_POSITION_US), /* defaultValue= */ 0); + bundle.getLong( + keyForField(FIELD_AD_RESUME_POSITION_US), /* defaultValue= */ NONE.adResumePositionUs); long contentDurationUs = - bundle.getLong(keyForField(FIELD_CONTENT_DURATION_US), /* defaultValue= */ C.TIME_UNSET); - int removedAdGroupCount = bundle.getInt(keyForField(FIELD_REMOVED_AD_GROUP_COUNT)); + bundle.getLong( + keyForField(FIELD_CONTENT_DURATION_US), /* defaultValue= */ NONE.contentDurationUs); + int removedAdGroupCount = + bundle.getInt( + keyForField(FIELD_REMOVED_AD_GROUP_COUNT), + /* defaultValue= */ NONE.removedAdGroupCount); return new AdPlaybackState( /* adsId= */ null, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); } diff --git a/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java b/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java index d398cd5b0fa..29d6383971d 100644 --- a/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.fail; import android.net.Uri; +import android.os.Bundle; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Assert; import org.junit.Test; @@ -402,7 +403,44 @@ public void withResetAdGroup_resetsAdsInFinalStates() { } @Test - public void roundTripViaBundle_yieldsEqualFieldsExceptAdsId() { + public void adPlaybackStateWithNoAds_checkValues() { + AdPlaybackState adPlaybackStateWithNoAds = AdPlaybackState.NONE; + + // Please refrain from altering these values since doing so would cause issues with backwards + // compatibility. + assertThat(adPlaybackStateWithNoAds.adsId).isNull(); + assertThat(adPlaybackStateWithNoAds.adGroupCount).isEqualTo(0); + assertThat(adPlaybackStateWithNoAds.adResumePositionUs).isEqualTo(0); + assertThat(adPlaybackStateWithNoAds.contentDurationUs).isEqualTo(C.TIME_UNSET); + assertThat(adPlaybackStateWithNoAds.removedAdGroupCount).isEqualTo(0); + } + + @Test + public void adPlaybackStateWithNoAds_toBundleSkipsDefaultValues_fromBundleRestoresThem() { + AdPlaybackState adPlaybackStateWithNoAds = AdPlaybackState.NONE; + + Bundle adPlaybackStateWithNoAdsBundle = adPlaybackStateWithNoAds.toBundle(); + + // check Bundle created above, contains no keys. + assertThat(adPlaybackStateWithNoAdsBundle.keySet()).isEmpty(); + + AdPlaybackState adPlaybackStateWithNoAdsFromBundle = + AdPlaybackState.CREATOR.fromBundle(adPlaybackStateWithNoAdsBundle); + + // check object retrieved from adPlaybackStateWithNoAdsBundle is equal to AdPlaybackState.NONE + assertThat(adPlaybackStateWithNoAdsFromBundle.adsId).isEqualTo(adPlaybackStateWithNoAds.adsId); + assertThat(adPlaybackStateWithNoAdsFromBundle.adGroupCount) + .isEqualTo(adPlaybackStateWithNoAds.adGroupCount); + assertThat(adPlaybackStateWithNoAdsFromBundle.adResumePositionUs) + .isEqualTo(adPlaybackStateWithNoAds.adResumePositionUs); + assertThat(adPlaybackStateWithNoAdsFromBundle.contentDurationUs) + .isEqualTo(adPlaybackStateWithNoAds.contentDurationUs); + assertThat(adPlaybackStateWithNoAdsFromBundle.removedAdGroupCount) + .isEqualTo(adPlaybackStateWithNoAds.removedAdGroupCount); + } + + @Test + public void createAdPlaybackState_roundTripViaBundle_yieldsEqualFieldsExceptAdsId() { AdPlaybackState originalState = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US) .withRemovedAdGroupCount(1) From a94aa8dbd99fc5ddec6ef25bb4c8ad7b3ca39e6f Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 22 Dec 2022 15:25:26 +0000 Subject: [PATCH 088/141] Fix order of playback controls in RTL layout Issue: androidx/media#227 #minor-release PiperOrigin-RevId: 497159283 (cherry picked from commit 80603427abbd0da09f21e381cc10a5d47fb6d780) --- RELEASENOTES.md | 3 +++ libraries/ui/src/main/res/layout/exo_player_control_view.xml | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 14685340d98..24760d16f95 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -30,6 +30,9 @@ `PlayerView.setControllerVisibilityListener(PlayerControlView.VisibilityListener)` to ensure visibility changes are passed to the registered listener ([#229](https://github.com/androidx/media/issues/229)). + * Fix the ordering of the center player controls in `PlayerView` when + using a right-to-left (RTL) layout + ([#227](https://github.com/androidx/media/issues/227)). * Session: * Add abstract `SimpleBasePlayer` to help implement the `Player` interface for custom players. diff --git a/libraries/ui/src/main/res/layout/exo_player_control_view.xml b/libraries/ui/src/main/res/layout/exo_player_control_view.xml index 0a5ad9a21da..c412079eb59 100644 --- a/libraries/ui/src/main/res/layout/exo_player_control_view.xml +++ b/libraries/ui/src/main/res/layout/exo_player_control_view.xml @@ -131,7 +131,8 @@ android:background="@android:color/transparent" android:gravity="center" android:padding="@dimen/exo_styled_controls_padding" - android:clipToPadding="false"> + android:clipToPadding="false" + android:layoutDirection="ltr"> From 70156dce4fea95fbccb46b91e5c4f6e3e4d3a64c Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 22 Dec 2022 15:28:27 +0000 Subject: [PATCH 089/141] Enable RTL support in the demo app We might as well keep this enabled by default, rather than having to manually toggle it on to investigate RTL issues like Issue: androidx/media#227. PiperOrigin-RevId: 497159744 (cherry picked from commit 69583d0ac1fa1ab1a1e250774fc1414550625967) --- demos/main/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 76fc35d287f..401d73a8e61 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -35,6 +35,7 @@ android:largeHeap="true" android:allowBackup="false" android:requestLegacyExternalStorage="true" + android:supportsRtl="true" android:name="androidx.multidex.MultiDexApplication" tools:targetApi="29"> From d67df79d1ec852475acfd722a67a496f7d495332 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 22 Dec 2022 17:36:53 +0000 Subject: [PATCH 090/141] Remove player listener on the application thread of the player PiperOrigin-RevId: 497183220 (cherry picked from commit fc22f89fdea4aad4819a59d4819f0857a5596869) --- RELEASENOTES.md | 2 ++ .../ima/ImaServerSideAdInsertionMediaSource.java | 11 ++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 24760d16f95..bbcbf00ebac 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -49,6 +49,8 @@ * Cast extension * Bump Cast SDK version to 21.2.0. * IMA extension + * Remove player listener of the `ImaServerSideAdInsertionMediaSource` on + the application thread to avoid threading issues. * Bump IMA SDK version to 3.29.0. ### 1.0.0-beta03 (2022-11-22) diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index 150c852a91e..2f328e1c157 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -36,6 +36,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.util.Pair; import android.view.ViewGroup; import androidx.annotation.IntDef; @@ -495,7 +496,8 @@ private ImaServerSideAdInsertionMediaSource( this.applicationAdEventListener = applicationAdEventListener; this.applicationAdErrorListener = applicationAdErrorListener; componentListener = new ComponentListener(); - mainHandler = Util.createHandlerForCurrentLooper(); + Assertions.checkArgument(player.getApplicationLooper() == Looper.getMainLooper()); + mainHandler = new Handler(Looper.getMainLooper()); Uri streamRequestUri = checkNotNull(mediaItem.localConfiguration).uri; isLiveStream = ImaServerSideAdInsertionUriBuilder.isLiveStream(streamRequestUri); adsId = ImaServerSideAdInsertionUriBuilder.getAdsId(streamRequestUri); @@ -572,8 +574,11 @@ protected void releaseSourceInternal() { super.releaseSourceInternal(); if (loader != null) { loader.release(); - player.removeListener(componentListener); - mainHandler.post(() -> setStreamManager(/* streamManager= */ null)); + mainHandler.post( + () -> { + player.removeListener(componentListener); + setStreamManager(/* streamManager= */ null); + }); loader = null; } } From 7da071ad378735dbe4fd916ff3bb277433b29ce3 Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 4 Jan 2023 13:51:24 +0000 Subject: [PATCH 091/141] Check bundles in `MediaItem` to verify keys are skipped Added another check in each of these tests to make sure we don't add keys to bundle for fields with default values. Also fixed comments of similar changes in `AdPlaybackStateTest` and `MediaMetadataTest`. PiperOrigin-RevId: 499463581 (cherry picked from commit 0512164fdd570a2047f51be719aae75ebcbf9255) --- .../media3/common/AdPlaybackStateTest.java | 3 +-- .../androidx/media3/common/MediaItemTest.java | 27 +++++++++++++++---- .../media3/common/MediaMetadataTest.java | 3 +-- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java b/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java index 29d6383971d..6a07dce3dc7 100644 --- a/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/AdPlaybackStateTest.java @@ -421,13 +421,12 @@ public void adPlaybackStateWithNoAds_toBundleSkipsDefaultValues_fromBundleRestor Bundle adPlaybackStateWithNoAdsBundle = adPlaybackStateWithNoAds.toBundle(); - // check Bundle created above, contains no keys. + // Check that default values are skipped when bundling. assertThat(adPlaybackStateWithNoAdsBundle.keySet()).isEmpty(); AdPlaybackState adPlaybackStateWithNoAdsFromBundle = AdPlaybackState.CREATOR.fromBundle(adPlaybackStateWithNoAdsBundle); - // check object retrieved from adPlaybackStateWithNoAdsBundle is equal to AdPlaybackState.NONE assertThat(adPlaybackStateWithNoAdsFromBundle.adsId).isEqualTo(adPlaybackStateWithNoAds.adsId); assertThat(adPlaybackStateWithNoAdsFromBundle.adGroupCount) .isEqualTo(adPlaybackStateWithNoAds.adGroupCount); diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java index 30b5853e5f8..3f557ca8d2a 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java @@ -375,12 +375,18 @@ public void createDefaultClippingConfigurationInstance_checksDefaultValues() { } @Test - public void createDefaultClippingConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() { + public void + createDefaultClippingConfigurationInstance_toBundleSkipsDefaultValues_fromBundleRestoresThem() { MediaItem.ClippingConfiguration clippingConfiguration = new MediaItem.ClippingConfiguration.Builder().build(); + Bundle clippingConfigurationBundle = clippingConfiguration.toBundle(); + + // Check that default values are skipped when bundling. + assertThat(clippingConfigurationBundle.keySet()).isEmpty(); + MediaItem.ClippingConfiguration clippingConfigurationFromBundle = - MediaItem.ClippingConfiguration.CREATOR.fromBundle(clippingConfiguration.toBundle()); + MediaItem.ClippingConfiguration.CREATOR.fromBundle(clippingConfigurationBundle); assertThat(clippingConfigurationFromBundle).isEqualTo(clippingConfiguration); } @@ -558,12 +564,18 @@ public void createDefaultLiveConfigurationInstance_checksDefaultValues() { } @Test - public void createDefaultLiveConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() { + public void + createDefaultLiveConfigurationInstance_toBundleSkipsDefaultValues_fromBundleRestoresThem() { MediaItem.LiveConfiguration liveConfiguration = new MediaItem.LiveConfiguration.Builder().build(); + Bundle liveConfigurationBundle = liveConfiguration.toBundle(); + + // Check that default values are skipped when bundling. + assertThat(liveConfigurationBundle.keySet()).isEmpty(); + MediaItem.LiveConfiguration liveConfigurationFromBundle = - MediaItem.LiveConfiguration.CREATOR.fromBundle(liveConfiguration.toBundle()); + MediaItem.LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle); assertThat(liveConfigurationFromBundle).isEqualTo(liveConfiguration); } @@ -832,9 +844,14 @@ public void createDefaultMediaItemInstance_checksDefaultValues() { } @Test - public void createDefaultMediaItemInstance_roundTripViaBundle_yieldsEqualInstance() { + public void createDefaultMediaItemInstance_toBundleSkipsDefaultValues_fromBundleRestoresThem() { MediaItem mediaItem = new MediaItem.Builder().build(); + Bundle mediaItemBundle = mediaItem.toBundle(); + + // Check that default values are skipped when bundling. + assertThat(mediaItemBundle.keySet()).isEmpty(); + MediaItem mediaItemFromBundle = MediaItem.CREATOR.fromBundle(mediaItem.toBundle()); assertThat(mediaItemFromBundle).isEqualTo(mediaItem); diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java index d2810ddd1b6..904c55ee157 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaMetadataTest.java @@ -112,12 +112,11 @@ public void toBundleSkipsDefaultValues_fromBundleRestoresThem() { Bundle mediaMetadataBundle = mediaMetadata.toBundle(); - // check Bundle created above, contains no keys. + // Check that default values are skipped when bundling. assertThat(mediaMetadataBundle.keySet()).isEmpty(); MediaMetadata mediaMetadataFromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadataBundle); - // check object retrieved from mediaMetadataBundle is equal to mediaMetadata. assertThat(mediaMetadataFromBundle).isEqualTo(mediaMetadata); // Extras is not implemented in MediaMetadata.equals(Object o). assertThat(mediaMetadataFromBundle.extras).isNull(); From 21996be448e5c333886cf135d8ba75502db20be6 Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 4 Jan 2023 17:55:58 +0000 Subject: [PATCH 092/141] Optimise bundling for `Timeline.Window` and `Timeline.Period` Improves the time taken to construct playerInfo from its bundle from ~400 ms to ~300 ms. Also made `Timeline.Window.toBundle(boolean excludeMediaItem)` public as it was required to assert a condition in tests. PiperOrigin-RevId: 499512353 (cherry picked from commit 790e27d929906a36438af5b42ef62a13e4719045) --- .../java/androidx/media3/common/Timeline.java | 91 ++++++++++++++----- .../androidx/media3/common/TimelineTest.java | 50 +++++++++- 2 files changed, 116 insertions(+), 25 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java index 679df19aae3..d95b27f2bce 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java +++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java @@ -454,27 +454,60 @@ public int hashCode() { private static final int FIELD_LAST_PERIOD_INDEX = 12; private static final int FIELD_POSITION_IN_FIRST_PERIOD_US = 13; - private final Bundle toBundle(boolean excludeMediaItem) { + /** + * Returns a {@link Bundle} representing the information stored in this object. + * + *

    It omits the {@link #uid} and {@link #manifest} fields. The {@link #uid} of an instance + * restored by {@link #CREATOR} will be a fake {@link Object} and the {@link #manifest} of the + * instance will be {@code null}. + * + * @param excludeMediaItem Whether to exclude {@link #mediaItem} of window. + */ + @UnstableApi + public Bundle toBundle(boolean excludeMediaItem) { Bundle bundle = new Bundle(); - bundle.putBundle( - keyForField(FIELD_MEDIA_ITEM), - excludeMediaItem ? MediaItem.EMPTY.toBundle() : mediaItem.toBundle()); - bundle.putLong(keyForField(FIELD_PRESENTATION_START_TIME_MS), presentationStartTimeMs); - bundle.putLong(keyForField(FIELD_WINDOW_START_TIME_MS), windowStartTimeMs); - bundle.putLong( - keyForField(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS), elapsedRealtimeEpochOffsetMs); - bundle.putBoolean(keyForField(FIELD_IS_SEEKABLE), isSeekable); - bundle.putBoolean(keyForField(FIELD_IS_DYNAMIC), isDynamic); + if (!excludeMediaItem) { + bundle.putBundle(keyForField(FIELD_MEDIA_ITEM), mediaItem.toBundle()); + } + if (presentationStartTimeMs != C.TIME_UNSET) { + bundle.putLong(keyForField(FIELD_PRESENTATION_START_TIME_MS), presentationStartTimeMs); + } + if (windowStartTimeMs != C.TIME_UNSET) { + bundle.putLong(keyForField(FIELD_WINDOW_START_TIME_MS), windowStartTimeMs); + } + if (elapsedRealtimeEpochOffsetMs != C.TIME_UNSET) { + bundle.putLong( + keyForField(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS), elapsedRealtimeEpochOffsetMs); + } + if (isSeekable) { + bundle.putBoolean(keyForField(FIELD_IS_SEEKABLE), isSeekable); + } + if (isDynamic) { + bundle.putBoolean(keyForField(FIELD_IS_DYNAMIC), isDynamic); + } + @Nullable MediaItem.LiveConfiguration liveConfiguration = this.liveConfiguration; if (liveConfiguration != null) { bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle()); } - bundle.putBoolean(keyForField(FIELD_IS_PLACEHOLDER), isPlaceholder); - bundle.putLong(keyForField(FIELD_DEFAULT_POSITION_US), defaultPositionUs); - bundle.putLong(keyForField(FIELD_DURATION_US), durationUs); - bundle.putInt(keyForField(FIELD_FIRST_PERIOD_INDEX), firstPeriodIndex); - bundle.putInt(keyForField(FIELD_LAST_PERIOD_INDEX), lastPeriodIndex); - bundle.putLong(keyForField(FIELD_POSITION_IN_FIRST_PERIOD_US), positionInFirstPeriodUs); + if (isPlaceholder) { + bundle.putBoolean(keyForField(FIELD_IS_PLACEHOLDER), isPlaceholder); + } + if (defaultPositionUs != 0) { + bundle.putLong(keyForField(FIELD_DEFAULT_POSITION_US), defaultPositionUs); + } + if (durationUs != C.TIME_UNSET) { + bundle.putLong(keyForField(FIELD_DURATION_US), durationUs); + } + if (firstPeriodIndex != 0) { + bundle.putInt(keyForField(FIELD_FIRST_PERIOD_INDEX), firstPeriodIndex); + } + if (lastPeriodIndex != 0) { + bundle.putInt(keyForField(FIELD_LAST_PERIOD_INDEX), lastPeriodIndex); + } + if (positionInFirstPeriodUs != 0) { + bundle.putLong(keyForField(FIELD_POSITION_IN_FIRST_PERIOD_US), positionInFirstPeriodUs); + } return bundle; } @@ -504,7 +537,7 @@ private static Window fromBundle(Bundle bundle) { @Nullable Bundle mediaItemBundle = bundle.getBundle(keyForField(FIELD_MEDIA_ITEM)); @Nullable MediaItem mediaItem = - mediaItemBundle != null ? MediaItem.CREATOR.fromBundle(mediaItemBundle) : null; + mediaItemBundle != null ? MediaItem.CREATOR.fromBundle(mediaItemBundle) : MediaItem.EMPTY; long presentationStartTimeMs = bundle.getLong( keyForField(FIELD_PRESENTATION_START_TIME_MS), /* defaultValue= */ C.TIME_UNSET); @@ -936,16 +969,25 @@ public int hashCode() { *

    It omits the {@link #id} and {@link #uid} fields so these fields of an instance restored * by {@link #CREATOR} will always be {@code null}. */ - // TODO(b/166765820): See if missing fields would be okay and add them to the Bundle otherwise. @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_WINDOW_INDEX), windowIndex); - bundle.putLong(keyForField(FIELD_DURATION_US), durationUs); - bundle.putLong(keyForField(FIELD_POSITION_IN_WINDOW_US), positionInWindowUs); - bundle.putBoolean(keyForField(FIELD_PLACEHOLDER), isPlaceholder); - bundle.putBundle(keyForField(FIELD_AD_PLAYBACK_STATE), adPlaybackState.toBundle()); + if (windowIndex != 0) { + bundle.putInt(keyForField(FIELD_WINDOW_INDEX), windowIndex); + } + if (durationUs != C.TIME_UNSET) { + bundle.putLong(keyForField(FIELD_DURATION_US), durationUs); + } + if (positionInWindowUs != 0) { + bundle.putLong(keyForField(FIELD_POSITION_IN_WINDOW_US), positionInWindowUs); + } + if (isPlaceholder) { + bundle.putBoolean(keyForField(FIELD_PLACEHOLDER), isPlaceholder); + } + if (!adPlaybackState.equals(AdPlaybackState.NONE)) { + bundle.putBundle(keyForField(FIELD_AD_PLAYBACK_STATE), adPlaybackState.toBundle()); + } return bundle; } @@ -962,7 +1004,8 @@ private static Period fromBundle(Bundle bundle) { bundle.getLong(keyForField(FIELD_DURATION_US), /* defaultValue= */ C.TIME_UNSET); long positionInWindowUs = bundle.getLong(keyForField(FIELD_POSITION_IN_WINDOW_US), /* defaultValue= */ 0); - boolean isPlaceholder = bundle.getBoolean(keyForField(FIELD_PLACEHOLDER)); + boolean isPlaceholder = + bundle.getBoolean(keyForField(FIELD_PLACEHOLDER), /* defaultValue= */ false); @Nullable Bundle adPlaybackStateBundle = bundle.getBundle(keyForField(FIELD_AD_PLAYBACK_STATE)); AdPlaybackState adPlaybackState = diff --git a/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java b/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java index 6844330e14f..111652b38b9 100644 --- a/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; +import android.os.Bundle; import androidx.annotation.Nullable; import androidx.media3.common.MediaItem.LiveConfiguration; import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder; @@ -267,7 +268,9 @@ public void roundTripViaBundle_ofTimeline_yieldsEqualInstanceExceptIdsAndManifes /* durationUs= */ 2, /* defaultPositionUs= */ 22, /* windowOffsetInFirstPeriodUs= */ 222, - ImmutableList.of(AdPlaybackState.NONE), + ImmutableList.of( + new AdPlaybackState( + /* adsId= */ null, /* adGroupTimesUs...= */ 10_000, 20_000)), new MediaItem.Builder().setMediaId("mediaId2").build()), new TimelineWindowDefinition( /* periodCount= */ 3, @@ -334,6 +337,31 @@ public void roundTripViaBundle_ofEmptyTimeline_returnsEmptyTimeline() { TimelineAsserts.assertEmpty(Timeline.CREATOR.fromBundle(Timeline.EMPTY.toBundle())); } + @Test + public void window_toBundleSkipsDefaultValues_fromBundleRestoresThem() { + Timeline.Window window = new Timeline.Window(); + // Please refrain from altering these default values since doing so would cause issues with + // backwards compatibility. + window.presentationStartTimeMs = C.TIME_UNSET; + window.windowStartTimeMs = C.TIME_UNSET; + window.elapsedRealtimeEpochOffsetMs = C.TIME_UNSET; + window.durationUs = C.TIME_UNSET; + window.mediaItem = new MediaItem.Builder().build(); + + Bundle windowBundle = window.toBundle(); + + // Check that default values are skipped when bundling. MediaItem key is not added to the bundle + // only when excludeMediaItem is true. + assertThat(windowBundle.keySet()).hasSize(1); + assertThat(window.toBundle(/* excludeMediaItem= */ true).keySet()).isEmpty(); + + Timeline.Window restoredWindow = Timeline.Window.CREATOR.fromBundle(windowBundle); + + assertThat(restoredWindow.manifest).isNull(); + TimelineAsserts.assertWindowEqualsExceptUidAndManifest( + /* expectedWindow= */ window, /* actualWindow= */ restoredWindow); + } + @Test public void roundTripViaBundle_ofWindow_yieldsEqualInstanceExceptUidAndManifest() { Timeline.Window window = new Timeline.Window(); @@ -367,6 +395,26 @@ public void roundTripViaBundle_ofWindow_yieldsEqualInstanceExceptUidAndManifest( /* expectedWindow= */ window, /* actualWindow= */ restoredWindow); } + @Test + public void period_toBundleSkipsDefaultValues_fromBundleRestoresThem() { + Timeline.Period period = new Timeline.Period(); + // Please refrain from altering these default values since doing so would cause issues with + // backwards compatibility. + period.durationUs = C.TIME_UNSET; + + Bundle periodBundle = period.toBundle(); + + // Check that default values are skipped when bundling. + assertThat(periodBundle.keySet()).isEmpty(); + + Timeline.Period restoredPeriod = Timeline.Period.CREATOR.fromBundle(periodBundle); + + assertThat(restoredPeriod.id).isNull(); + assertThat(restoredPeriod.uid).isNull(); + TimelineAsserts.assertPeriodEqualsExceptIds( + /* expectedPeriod= */ period, /* actualPeriod= */ restoredPeriod); + } + @Test public void roundTripViaBundle_ofPeriod_yieldsEqualInstanceExceptIds() { Timeline.Period period = new Timeline.Period(); From 4e7ccd7ffd6de6ee41e3ad5299ac94723f269525 Mon Sep 17 00:00:00 2001 From: Googler Date: Wed, 4 Jan 2023 18:35:06 +0000 Subject: [PATCH 093/141] Throw a ParserException instead of a NullPointerException if the sample table (stbl) is missing a required sample description (stsd). As per the javadoc for AtomParsers.parseTrack, ParserException should be "thrown if the trak atom can't be parsed." PiperOrigin-RevId: 499522748 (cherry picked from commit d8ea770e9ba6eed0bdce0b359c54a55be0844fd3) --- RELEASENOTES.md | 3 +++ .../java/androidx/media3/extractor/mp4/AtomParsers.java | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bbcbf00ebac..62c32363322 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,9 @@ for seeking. * Use theme when loading drawables on API 21+ ([#220](https://github.com/androidx/media/issues/220)). + * Throw a ParserException instead of a NullPointerException if the sample + * table (stbl) is missing a required sample description (stsd) when + * parsing trak atoms. * Audio: * Use the compressed audio format bitrate to calculate the min buffer size for `AudioTrack` in direct playbacks (passthrough). diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java index 4543d32819b..5e290bc67d0 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java @@ -43,6 +43,7 @@ import androidx.media3.extractor.HevcConfig; import androidx.media3.extractor.OpusUtil; import androidx.media3.extractor.metadata.mp4.SmtaMetadataEntry; +import androidx.media3.extractor.mp4.Atom.LeafAtom; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Ints; @@ -308,9 +309,14 @@ private static Track parseTrak( Pair mdhdData = parseMdhd(checkNotNull(mdia.getLeafAtomOfType(Atom.TYPE_mdhd)).data); + LeafAtom stsd = stbl.getLeafAtomOfType(Atom.TYPE_stsd); + if (stsd == null) { + throw ParserException.createForMalformedContainer( + "Malformed sample table (stbl) missing sample description (stsd)", /* cause= */ null); + } StsdData stsdData = parseStsd( - checkNotNull(stbl.getLeafAtomOfType(Atom.TYPE_stsd)).data, + stsd.data, tkhdData.id, tkhdData.rotationDegrees, mdhdData.second, From 2cfd05f125ddfcf585c8470c078ec404ffe78c3e Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 5 Jan 2023 17:20:43 +0000 Subject: [PATCH 094/141] Fix typo in `DefaultTrackSelector.Parameters` field PiperOrigin-RevId: 499905136 (cherry picked from commit b63e1da861d662f02d9a5888aaefb4a1b3347e40) --- .../exoplayer/trackselection/DefaultTrackSelector.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index 54bac6c44cc..c3c8992476d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -845,7 +845,7 @@ private Builder(Bundle bundle) { // Audio setExceedAudioConstraintsIfNecessary( bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY), + Parameters.keyForField(Parameters.FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY), defaultValue.exceedAudioConstraintsIfNecessary)); setAllowAudioMixedMimeTypeAdaptiveness( bundle.getBoolean( @@ -1878,7 +1878,7 @@ public int hashCode() { private static final int FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS = FIELD_CUSTOM_ID_BASE + 1; private static final int FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS = FIELD_CUSTOM_ID_BASE + 2; - private static final int FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY = FIELD_CUSTOM_ID_BASE + 3; + private static final int FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY = FIELD_CUSTOM_ID_BASE + 3; private static final int FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS = FIELD_CUSTOM_ID_BASE + 4; private static final int FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS = @@ -1920,7 +1920,7 @@ public Bundle toBundle() { allowVideoMixedDecoderSupportAdaptiveness); // Audio bundle.putBoolean( - keyForField(FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY), + keyForField(FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY), exceedAudioConstraintsIfNecessary); bundle.putBoolean( keyForField(FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS), From 96eb8968a8eb9cf0cf719243f4bc3f16ae468e77 Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 5 Jan 2023 22:04:57 +0000 Subject: [PATCH 095/141] Initialise fields used for bundling as String directly Initialising the fields as Integer and then getting a String on compute time is slow. Instead we directly initialise these fields as String. Improves the time taken in bundling PlayerInfo further to less than 200ms from ~300ms. Also modified a test to improve productive coverage. PiperOrigin-RevId: 500003935 (cherry picked from commit 578f2de48f795ad90aafdad645c62fcdbd686e0a) --- .../media3/common/AdPlaybackState.java | 113 +++---- .../media3/common/AudioAttributes.java | 64 ++-- .../androidx/media3/common/ColorInfo.java | 47 +-- .../androidx/media3/common/DeviceInfo.java | 30 +- .../java/androidx/media3/common/Format.java | 237 ++++++--------- .../androidx/media3/common/HeartRating.java | 35 +-- .../androidx/media3/common/MediaItem.java | 185 ++++-------- .../androidx/media3/common/MediaMetadata.java | 281 ++++++++---------- .../media3/common/PercentageRating.java | 29 +- .../media3/common/PlaybackException.java | 45 +-- .../media3/common/PlaybackParameters.java | 29 +- .../java/androidx/media3/common/Player.java | 82 ++--- .../java/androidx/media3/common/Rating.java | 16 +- .../androidx/media3/common/StarRating.java | 37 +-- .../androidx/media3/common/ThumbRating.java | 36 +-- .../java/androidx/media3/common/Timeline.java | 208 ++++--------- .../androidx/media3/common/TrackGroup.java | 31 +- .../media3/common/TrackSelectionOverride.java | 29 +- .../common/TrackSelectionParameters.java | 202 +++++-------- .../java/androidx/media3/common/Tracks.java | 69 ++--- .../androidx/media3/common/VideoSize.java | 50 +--- .../java/androidx/media3/common/text/Cue.java | 154 ++++------ .../androidx/media3/common/text/CueGroup.java | 30 +- .../androidx/media3/common/util/Util.java | 10 + .../androidx/media3/common/MediaItemTest.java | 5 + .../exoplayer/ExoPlaybackException.java | 43 +-- .../exoplayer/source/TrackGroupArray.java | 27 +- .../trackselection/DefaultTrackSelector.java | 179 +++++------ .../ImaServerSideAdInsertionMediaSource.java | 23 +- .../media3/session/CommandButton.java | 62 ++-- .../media3/session/ConnectionRequest.java | 54 +--- .../media3/session/ConnectionState.java | 87 ++---- .../media3/session/LibraryResult.java | 54 ++-- .../media3/session/MediaLibraryService.java | 47 +-- .../androidx/media3/session/PlayerInfo.java | 267 ++++++----------- .../media3/session/SessionCommand.java | 30 +- .../media3/session/SessionCommands.java | 23 +- .../media3/session/SessionPositionInfo.java | 93 ++---- .../media3/session/SessionResult.java | 32 +- .../androidx/media3/session/SessionToken.java | 27 +- .../media3/session/SessionTokenImplBase.java | 86 ++---- .../session/SessionTokenImplLegacy.java | 65 ++-- 42 files changed, 1107 insertions(+), 2146 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java index ec9f23199d7..5bc8f9d0a91 100644 --- a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java +++ b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java @@ -459,44 +459,29 @@ private static long[] copyDurationsUsWithSpaceForAdCount(long[] durationsUs, int // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_TIME_US, - FIELD_COUNT, - FIELD_URIS, - FIELD_STATES, - FIELD_DURATIONS_US, - FIELD_CONTENT_RESUME_OFFSET_US, - FIELD_IS_SERVER_SIDE_INSERTED, - FIELD_ORIGINAL_COUNT - }) - private @interface FieldNumber {} - - private static final int FIELD_TIME_US = 0; - private static final int FIELD_COUNT = 1; - private static final int FIELD_URIS = 2; - private static final int FIELD_STATES = 3; - private static final int FIELD_DURATIONS_US = 4; - private static final int FIELD_CONTENT_RESUME_OFFSET_US = 5; - private static final int FIELD_IS_SERVER_SIDE_INSERTED = 6; - private static final int FIELD_ORIGINAL_COUNT = 7; + private static final String FIELD_TIME_US = Util.intToStringMaxRadix(0); + private static final String FIELD_COUNT = Util.intToStringMaxRadix(1); + private static final String FIELD_URIS = Util.intToStringMaxRadix(2); + private static final String FIELD_STATES = Util.intToStringMaxRadix(3); + private static final String FIELD_DURATIONS_US = Util.intToStringMaxRadix(4); + private static final String FIELD_CONTENT_RESUME_OFFSET_US = Util.intToStringMaxRadix(5); + private static final String FIELD_IS_SERVER_SIDE_INSERTED = Util.intToStringMaxRadix(6); + private static final String FIELD_ORIGINAL_COUNT = Util.intToStringMaxRadix(7); // putParcelableArrayList actually supports null elements. @SuppressWarnings("nullness:argument") @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putLong(keyForField(FIELD_TIME_US), timeUs); - bundle.putInt(keyForField(FIELD_COUNT), count); - bundle.putInt(keyForField(FIELD_ORIGINAL_COUNT), originalCount); + bundle.putLong(FIELD_TIME_US, timeUs); + bundle.putInt(FIELD_COUNT, count); + bundle.putInt(FIELD_ORIGINAL_COUNT, originalCount); bundle.putParcelableArrayList( - keyForField(FIELD_URIS), new ArrayList<@NullableType Uri>(Arrays.asList(uris))); - bundle.putIntArray(keyForField(FIELD_STATES), states); - bundle.putLongArray(keyForField(FIELD_DURATIONS_US), durationsUs); - bundle.putLong(keyForField(FIELD_CONTENT_RESUME_OFFSET_US), contentResumeOffsetUs); - bundle.putBoolean(keyForField(FIELD_IS_SERVER_SIDE_INSERTED), isServerSideInserted); + FIELD_URIS, new ArrayList<@NullableType Uri>(Arrays.asList(uris))); + bundle.putIntArray(FIELD_STATES, states); + bundle.putLongArray(FIELD_DURATIONS_US, durationsUs); + bundle.putLong(FIELD_CONTENT_RESUME_OFFSET_US, contentResumeOffsetUs); + bundle.putBoolean(FIELD_IS_SERVER_SIDE_INSERTED, isServerSideInserted); return bundle; } @@ -506,17 +491,16 @@ public Bundle toBundle() { // getParcelableArrayList may have null elements. @SuppressWarnings("nullness:type.argument") private static AdGroup fromBundle(Bundle bundle) { - long timeUs = bundle.getLong(keyForField(FIELD_TIME_US)); - int count = bundle.getInt(keyForField(FIELD_COUNT)); - int originalCount = bundle.getInt(keyForField(FIELD_ORIGINAL_COUNT)); - @Nullable - ArrayList<@NullableType Uri> uriList = bundle.getParcelableArrayList(keyForField(FIELD_URIS)); + long timeUs = bundle.getLong(FIELD_TIME_US); + int count = bundle.getInt(FIELD_COUNT); + int originalCount = bundle.getInt(FIELD_ORIGINAL_COUNT); + @Nullable ArrayList<@NullableType Uri> uriList = bundle.getParcelableArrayList(FIELD_URIS); @Nullable @AdState - int[] states = bundle.getIntArray(keyForField(FIELD_STATES)); - @Nullable long[] durationsUs = bundle.getLongArray(keyForField(FIELD_DURATIONS_US)); - long contentResumeOffsetUs = bundle.getLong(keyForField(FIELD_CONTENT_RESUME_OFFSET_US)); - boolean isServerSideInserted = bundle.getBoolean(keyForField(FIELD_IS_SERVER_SIDE_INSERTED)); + int[] states = bundle.getIntArray(FIELD_STATES); + @Nullable long[] durationsUs = bundle.getLongArray(FIELD_DURATIONS_US); + long contentResumeOffsetUs = bundle.getLong(FIELD_CONTENT_RESUME_OFFSET_US); + boolean isServerSideInserted = bundle.getBoolean(FIELD_IS_SERVER_SIDE_INSERTED); return new AdGroup( timeUs, count, @@ -527,10 +511,6 @@ private static AdGroup fromBundle(Bundle bundle) { contentResumeOffsetUs, isServerSideInserted); } - - private static String keyForField(@AdGroup.FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } /** @@ -1121,21 +1101,10 @@ private boolean isPositionBeforeAdGroup( // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_AD_GROUPS, - FIELD_AD_RESUME_POSITION_US, - FIELD_CONTENT_DURATION_US, - FIELD_REMOVED_AD_GROUP_COUNT - }) - private @interface FieldNumber {} - - private static final int FIELD_AD_GROUPS = 1; - private static final int FIELD_AD_RESUME_POSITION_US = 2; - private static final int FIELD_CONTENT_DURATION_US = 3; - private static final int FIELD_REMOVED_AD_GROUP_COUNT = 4; + private static final String FIELD_AD_GROUPS = Util.intToStringMaxRadix(1); + private static final String FIELD_AD_RESUME_POSITION_US = Util.intToStringMaxRadix(2); + private static final String FIELD_CONTENT_DURATION_US = Util.intToStringMaxRadix(3); + private static final String FIELD_REMOVED_AD_GROUP_COUNT = Util.intToStringMaxRadix(4); /** * {@inheritDoc} @@ -1152,16 +1121,16 @@ public Bundle toBundle() { adGroupBundleList.add(adGroup.toBundle()); } if (!adGroupBundleList.isEmpty()) { - bundle.putParcelableArrayList(keyForField(FIELD_AD_GROUPS), adGroupBundleList); + bundle.putParcelableArrayList(FIELD_AD_GROUPS, adGroupBundleList); } if (adResumePositionUs != NONE.adResumePositionUs) { - bundle.putLong(keyForField(FIELD_AD_RESUME_POSITION_US), adResumePositionUs); + bundle.putLong(FIELD_AD_RESUME_POSITION_US, adResumePositionUs); } if (contentDurationUs != NONE.contentDurationUs) { - bundle.putLong(keyForField(FIELD_CONTENT_DURATION_US), contentDurationUs); + bundle.putLong(FIELD_CONTENT_DURATION_US, contentDurationUs); } if (removedAdGroupCount != NONE.removedAdGroupCount) { - bundle.putInt(keyForField(FIELD_REMOVED_AD_GROUP_COUNT), removedAdGroupCount); + bundle.putInt(FIELD_REMOVED_AD_GROUP_COUNT, removedAdGroupCount); } return bundle; } @@ -1174,9 +1143,7 @@ public Bundle toBundle() { public static final Bundleable.Creator CREATOR = AdPlaybackState::fromBundle; private static AdPlaybackState fromBundle(Bundle bundle) { - @Nullable - ArrayList adGroupBundleList = - bundle.getParcelableArrayList(keyForField(FIELD_AD_GROUPS)); + @Nullable ArrayList adGroupBundleList = bundle.getParcelableArrayList(FIELD_AD_GROUPS); @Nullable AdGroup[] adGroups; if (adGroupBundleList == null) { adGroups = new AdGroup[0]; @@ -1187,23 +1154,15 @@ private static AdPlaybackState fromBundle(Bundle bundle) { } } long adResumePositionUs = - bundle.getLong( - keyForField(FIELD_AD_RESUME_POSITION_US), /* defaultValue= */ NONE.adResumePositionUs); + bundle.getLong(FIELD_AD_RESUME_POSITION_US, /* defaultValue= */ NONE.adResumePositionUs); long contentDurationUs = - bundle.getLong( - keyForField(FIELD_CONTENT_DURATION_US), /* defaultValue= */ NONE.contentDurationUs); + bundle.getLong(FIELD_CONTENT_DURATION_US, /* defaultValue= */ NONE.contentDurationUs); int removedAdGroupCount = - bundle.getInt( - keyForField(FIELD_REMOVED_AD_GROUP_COUNT), - /* defaultValue= */ NONE.removedAdGroupCount); + bundle.getInt(FIELD_REMOVED_AD_GROUP_COUNT, /* defaultValue= */ NONE.removedAdGroupCount); return new AdPlaybackState( /* adsId= */ null, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); } - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } - private static AdGroup[] createEmptyAdGroups(long[] adGroupTimesUs) { AdGroup[] adGroups = new AdGroup[adGroupTimesUs.length]; for (int i = 0; i < adGroups.length; i++) { diff --git a/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java b/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java index 11a8ef15bd0..6406baf87a8 100644 --- a/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java +++ b/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java @@ -15,20 +15,13 @@ */ package androidx.media3.common; -import static java.lang.annotation.ElementType.TYPE_USE; - import android.os.Bundle; import androidx.annotation.DoNotInline; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** * Attributes for audio playback, which configure the underlying platform {@link @@ -205,33 +198,21 @@ public int hashCode() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_CONTENT_TYPE, - FIELD_FLAGS, - FIELD_USAGE, - FIELD_ALLOWED_CAPTURE_POLICY, - FIELD_SPATIALIZATION_BEHAVIOR - }) - private @interface FieldNumber {} - - private static final int FIELD_CONTENT_TYPE = 0; - private static final int FIELD_FLAGS = 1; - private static final int FIELD_USAGE = 2; - private static final int FIELD_ALLOWED_CAPTURE_POLICY = 3; - private static final int FIELD_SPATIALIZATION_BEHAVIOR = 4; + private static final String FIELD_CONTENT_TYPE = Util.intToStringMaxRadix(0); + private static final String FIELD_FLAGS = Util.intToStringMaxRadix(1); + private static final String FIELD_USAGE = Util.intToStringMaxRadix(2); + private static final String FIELD_ALLOWED_CAPTURE_POLICY = Util.intToStringMaxRadix(3); + private static final String FIELD_SPATIALIZATION_BEHAVIOR = Util.intToStringMaxRadix(4); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_CONTENT_TYPE), contentType); - bundle.putInt(keyForField(FIELD_FLAGS), flags); - bundle.putInt(keyForField(FIELD_USAGE), usage); - bundle.putInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY), allowedCapturePolicy); - bundle.putInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR), spatializationBehavior); + bundle.putInt(FIELD_CONTENT_TYPE, contentType); + bundle.putInt(FIELD_FLAGS, flags); + bundle.putInt(FIELD_USAGE, usage); + bundle.putInt(FIELD_ALLOWED_CAPTURE_POLICY, allowedCapturePolicy); + bundle.putInt(FIELD_SPATIALIZATION_BEHAVIOR, spatializationBehavior); return bundle; } @@ -240,29 +221,24 @@ public Bundle toBundle() { public static final Creator CREATOR = bundle -> { Builder builder = new Builder(); - if (bundle.containsKey(keyForField(FIELD_CONTENT_TYPE))) { - builder.setContentType(bundle.getInt(keyForField(FIELD_CONTENT_TYPE))); + if (bundle.containsKey(FIELD_CONTENT_TYPE)) { + builder.setContentType(bundle.getInt(FIELD_CONTENT_TYPE)); } - if (bundle.containsKey(keyForField(FIELD_FLAGS))) { - builder.setFlags(bundle.getInt(keyForField(FIELD_FLAGS))); + if (bundle.containsKey(FIELD_FLAGS)) { + builder.setFlags(bundle.getInt(FIELD_FLAGS)); } - if (bundle.containsKey(keyForField(FIELD_USAGE))) { - builder.setUsage(bundle.getInt(keyForField(FIELD_USAGE))); + if (bundle.containsKey(FIELD_USAGE)) { + builder.setUsage(bundle.getInt(FIELD_USAGE)); } - if (bundle.containsKey(keyForField(FIELD_ALLOWED_CAPTURE_POLICY))) { - builder.setAllowedCapturePolicy(bundle.getInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY))); + if (bundle.containsKey(FIELD_ALLOWED_CAPTURE_POLICY)) { + builder.setAllowedCapturePolicy(bundle.getInt(FIELD_ALLOWED_CAPTURE_POLICY)); } - if (bundle.containsKey(keyForField(FIELD_SPATIALIZATION_BEHAVIOR))) { - builder.setSpatializationBehavior( - bundle.getInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR))); + if (bundle.containsKey(FIELD_SPATIALIZATION_BEHAVIOR)) { + builder.setSpatializationBehavior(bundle.getInt(FIELD_SPATIALIZATION_BEHAVIOR)); } return builder.build(); }; - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } - @RequiresApi(29) private static final class Api29 { @DoNotInline diff --git a/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java b/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java index aae29250d18..034ada4fe80 100644 --- a/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java @@ -15,16 +15,10 @@ */ package androidx.media3.common; -import static java.lang.annotation.ElementType.TYPE_USE; - import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import androidx.media3.common.util.Util; import java.util.Arrays; import org.checkerframework.dataflow.qual.Pure; @@ -183,41 +177,26 @@ public int hashCode() { // Bundleable implementation - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_COLOR_SPACE, - FIELD_COLOR_RANGE, - FIELD_COLOR_TRANSFER, - FIELD_HDR_STATIC_INFO, - }) - private @interface FieldNumber {} - - private static final int FIELD_COLOR_SPACE = 0; - private static final int FIELD_COLOR_RANGE = 1; - private static final int FIELD_COLOR_TRANSFER = 2; - private static final int FIELD_HDR_STATIC_INFO = 3; + private static final String FIELD_COLOR_SPACE = Util.intToStringMaxRadix(0); + private static final String FIELD_COLOR_RANGE = Util.intToStringMaxRadix(1); + private static final String FIELD_COLOR_TRANSFER = Util.intToStringMaxRadix(2); + private static final String FIELD_HDR_STATIC_INFO = Util.intToStringMaxRadix(3); @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_COLOR_SPACE), colorSpace); - bundle.putInt(keyForField(FIELD_COLOR_RANGE), colorRange); - bundle.putInt(keyForField(FIELD_COLOR_TRANSFER), colorTransfer); - bundle.putByteArray(keyForField(FIELD_HDR_STATIC_INFO), hdrStaticInfo); + bundle.putInt(FIELD_COLOR_SPACE, colorSpace); + bundle.putInt(FIELD_COLOR_RANGE, colorRange); + bundle.putInt(FIELD_COLOR_TRANSFER, colorTransfer); + bundle.putByteArray(FIELD_HDR_STATIC_INFO, hdrStaticInfo); return bundle; } public static final Creator CREATOR = bundle -> new ColorInfo( - bundle.getInt(keyForField(FIELD_COLOR_SPACE), Format.NO_VALUE), - bundle.getInt(keyForField(FIELD_COLOR_RANGE), Format.NO_VALUE), - bundle.getInt(keyForField(FIELD_COLOR_TRANSFER), Format.NO_VALUE), - bundle.getByteArray(keyForField(FIELD_HDR_STATIC_INFO))); - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } + bundle.getInt(FIELD_COLOR_SPACE, Format.NO_VALUE), + bundle.getInt(FIELD_COLOR_RANGE, Format.NO_VALUE), + bundle.getInt(FIELD_COLOR_TRANSFER, Format.NO_VALUE), + bundle.getByteArray(FIELD_HDR_STATIC_INFO)); } diff --git a/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java b/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java index 2daeb92ef27..c75fcb7cc9f 100644 --- a/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/DeviceInfo.java @@ -21,6 +21,7 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -87,23 +88,17 @@ public int hashCode() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_PLAYBACK_TYPE, FIELD_MIN_VOLUME, FIELD_MAX_VOLUME}) - private @interface FieldNumber {} - - private static final int FIELD_PLAYBACK_TYPE = 0; - private static final int FIELD_MIN_VOLUME = 1; - private static final int FIELD_MAX_VOLUME = 2; + private static final String FIELD_PLAYBACK_TYPE = Util.intToStringMaxRadix(0); + private static final String FIELD_MIN_VOLUME = Util.intToStringMaxRadix(1); + private static final String FIELD_MAX_VOLUME = Util.intToStringMaxRadix(2); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_PLAYBACK_TYPE), playbackType); - bundle.putInt(keyForField(FIELD_MIN_VOLUME), minVolume); - bundle.putInt(keyForField(FIELD_MAX_VOLUME), maxVolume); + bundle.putInt(FIELD_PLAYBACK_TYPE, playbackType); + bundle.putInt(FIELD_MIN_VOLUME, minVolume); + bundle.putInt(FIELD_MAX_VOLUME, maxVolume); return bundle; } @@ -112,14 +107,9 @@ public Bundle toBundle() { public static final Creator CREATOR = bundle -> { int playbackType = - bundle.getInt( - keyForField(FIELD_PLAYBACK_TYPE), /* defaultValue= */ PLAYBACK_TYPE_LOCAL); - int minVolume = bundle.getInt(keyForField(FIELD_MIN_VOLUME), /* defaultValue= */ 0); - int maxVolume = bundle.getInt(keyForField(FIELD_MAX_VOLUME), /* defaultValue= */ 0); + bundle.getInt(FIELD_PLAYBACK_TYPE, /* defaultValue= */ PLAYBACK_TYPE_LOCAL); + int minVolume = bundle.getInt(FIELD_MIN_VOLUME, /* defaultValue= */ 0); + int maxVolume = bundle.getInt(FIELD_MAX_VOLUME, /* defaultValue= */ 0); return new DeviceInfo(playbackType, minVolume, maxVolume); }; - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/Format.java b/libraries/common/src/main/java/androidx/media3/common/Format.java index 8e08993f1aa..450585d1f10 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Format.java +++ b/libraries/common/src/main/java/androidx/media3/common/Format.java @@ -15,20 +15,13 @@ */ package androidx.media3.common; -import static java.lang.annotation.ElementType.TYPE_USE; - import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.base.Joiner; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -1476,73 +1469,37 @@ public static String toLogString(@Nullable Format format) { } // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_ID, - FIELD_LABEL, - FIELD_LANGUAGE, - FIELD_SELECTION_FLAGS, - FIELD_ROLE_FLAGS, - FIELD_AVERAGE_BITRATE, - FIELD_PEAK_BITRATE, - FIELD_CODECS, - FIELD_METADATA, - FIELD_CONTAINER_MIME_TYPE, - FIELD_SAMPLE_MIME_TYPE, - FIELD_MAX_INPUT_SIZE, - FIELD_INITIALIZATION_DATA, - FIELD_DRM_INIT_DATA, - FIELD_SUBSAMPLE_OFFSET_US, - FIELD_WIDTH, - FIELD_HEIGHT, - FIELD_FRAME_RATE, - FIELD_ROTATION_DEGREES, - FIELD_PIXEL_WIDTH_HEIGHT_RATIO, - FIELD_PROJECTION_DATA, - FIELD_STEREO_MODE, - FIELD_COLOR_INFO, - FIELD_CHANNEL_COUNT, - FIELD_SAMPLE_RATE, - FIELD_PCM_ENCODING, - FIELD_ENCODER_DELAY, - FIELD_ENCODER_PADDING, - FIELD_ACCESSIBILITY_CHANNEL, - FIELD_CRYPTO_TYPE, - }) - private @interface FieldNumber {} - - private static final int FIELD_ID = 0; - private static final int FIELD_LABEL = 1; - private static final int FIELD_LANGUAGE = 2; - private static final int FIELD_SELECTION_FLAGS = 3; - private static final int FIELD_ROLE_FLAGS = 4; - private static final int FIELD_AVERAGE_BITRATE = 5; - private static final int FIELD_PEAK_BITRATE = 6; - private static final int FIELD_CODECS = 7; - private static final int FIELD_METADATA = 8; - private static final int FIELD_CONTAINER_MIME_TYPE = 9; - private static final int FIELD_SAMPLE_MIME_TYPE = 10; - private static final int FIELD_MAX_INPUT_SIZE = 11; - private static final int FIELD_INITIALIZATION_DATA = 12; - private static final int FIELD_DRM_INIT_DATA = 13; - private static final int FIELD_SUBSAMPLE_OFFSET_US = 14; - private static final int FIELD_WIDTH = 15; - private static final int FIELD_HEIGHT = 16; - private static final int FIELD_FRAME_RATE = 17; - private static final int FIELD_ROTATION_DEGREES = 18; - private static final int FIELD_PIXEL_WIDTH_HEIGHT_RATIO = 19; - private static final int FIELD_PROJECTION_DATA = 20; - private static final int FIELD_STEREO_MODE = 21; - private static final int FIELD_COLOR_INFO = 22; - private static final int FIELD_CHANNEL_COUNT = 23; - private static final int FIELD_SAMPLE_RATE = 24; - private static final int FIELD_PCM_ENCODING = 25; - private static final int FIELD_ENCODER_DELAY = 26; - private static final int FIELD_ENCODER_PADDING = 27; - private static final int FIELD_ACCESSIBILITY_CHANNEL = 28; - private static final int FIELD_CRYPTO_TYPE = 29; + + private static final String FIELD_ID = Util.intToStringMaxRadix(0); + private static final String FIELD_LABEL = Util.intToStringMaxRadix(1); + private static final String FIELD_LANGUAGE = Util.intToStringMaxRadix(2); + private static final String FIELD_SELECTION_FLAGS = Util.intToStringMaxRadix(3); + private static final String FIELD_ROLE_FLAGS = Util.intToStringMaxRadix(4); + private static final String FIELD_AVERAGE_BITRATE = Util.intToStringMaxRadix(5); + private static final String FIELD_PEAK_BITRATE = Util.intToStringMaxRadix(6); + private static final String FIELD_CODECS = Util.intToStringMaxRadix(7); + private static final String FIELD_METADATA = Util.intToStringMaxRadix(8); + private static final String FIELD_CONTAINER_MIME_TYPE = Util.intToStringMaxRadix(9); + private static final String FIELD_SAMPLE_MIME_TYPE = Util.intToStringMaxRadix(10); + private static final String FIELD_MAX_INPUT_SIZE = Util.intToStringMaxRadix(11); + private static final String FIELD_INITIALIZATION_DATA = Util.intToStringMaxRadix(12); + private static final String FIELD_DRM_INIT_DATA = Util.intToStringMaxRadix(13); + private static final String FIELD_SUBSAMPLE_OFFSET_US = Util.intToStringMaxRadix(14); + private static final String FIELD_WIDTH = Util.intToStringMaxRadix(15); + private static final String FIELD_HEIGHT = Util.intToStringMaxRadix(16); + private static final String FIELD_FRAME_RATE = Util.intToStringMaxRadix(17); + private static final String FIELD_ROTATION_DEGREES = Util.intToStringMaxRadix(18); + private static final String FIELD_PIXEL_WIDTH_HEIGHT_RATIO = Util.intToStringMaxRadix(19); + private static final String FIELD_PROJECTION_DATA = Util.intToStringMaxRadix(20); + private static final String FIELD_STEREO_MODE = Util.intToStringMaxRadix(21); + private static final String FIELD_COLOR_INFO = Util.intToStringMaxRadix(22); + private static final String FIELD_CHANNEL_COUNT = Util.intToStringMaxRadix(23); + private static final String FIELD_SAMPLE_RATE = Util.intToStringMaxRadix(24); + private static final String FIELD_PCM_ENCODING = Util.intToStringMaxRadix(25); + private static final String FIELD_ENCODER_DELAY = Util.intToStringMaxRadix(26); + private static final String FIELD_ENCODER_PADDING = Util.intToStringMaxRadix(27); + private static final String FIELD_ACCESSIBILITY_CHANNEL = Util.intToStringMaxRadix(28); + private static final String FIELD_CRYPTO_TYPE = Util.intToStringMaxRadix(29); @UnstableApi @Override @@ -1557,51 +1514,51 @@ public Bundle toBundle() { @UnstableApi public Bundle toBundle(boolean excludeMetadata) { Bundle bundle = new Bundle(); - bundle.putString(keyForField(FIELD_ID), id); - bundle.putString(keyForField(FIELD_LABEL), label); - bundle.putString(keyForField(FIELD_LANGUAGE), language); - bundle.putInt(keyForField(FIELD_SELECTION_FLAGS), selectionFlags); - bundle.putInt(keyForField(FIELD_ROLE_FLAGS), roleFlags); - bundle.putInt(keyForField(FIELD_AVERAGE_BITRATE), averageBitrate); - bundle.putInt(keyForField(FIELD_PEAK_BITRATE), peakBitrate); - bundle.putString(keyForField(FIELD_CODECS), codecs); + bundle.putString(FIELD_ID, id); + bundle.putString(FIELD_LABEL, label); + bundle.putString(FIELD_LANGUAGE, language); + bundle.putInt(FIELD_SELECTION_FLAGS, selectionFlags); + bundle.putInt(FIELD_ROLE_FLAGS, roleFlags); + bundle.putInt(FIELD_AVERAGE_BITRATE, averageBitrate); + bundle.putInt(FIELD_PEAK_BITRATE, peakBitrate); + bundle.putString(FIELD_CODECS, codecs); if (!excludeMetadata) { // TODO (internal ref: b/239701618) - bundle.putParcelable(keyForField(FIELD_METADATA), metadata); + bundle.putParcelable(FIELD_METADATA, metadata); } // Container specific. - bundle.putString(keyForField(FIELD_CONTAINER_MIME_TYPE), containerMimeType); + bundle.putString(FIELD_CONTAINER_MIME_TYPE, containerMimeType); // Sample specific. - bundle.putString(keyForField(FIELD_SAMPLE_MIME_TYPE), sampleMimeType); - bundle.putInt(keyForField(FIELD_MAX_INPUT_SIZE), maxInputSize); + bundle.putString(FIELD_SAMPLE_MIME_TYPE, sampleMimeType); + bundle.putInt(FIELD_MAX_INPUT_SIZE, maxInputSize); for (int i = 0; i < initializationData.size(); i++) { bundle.putByteArray(keyForInitializationData(i), initializationData.get(i)); } // DrmInitData doesn't need to be Bundleable as it's only used in the playing process to // initialize the decoder. - bundle.putParcelable(keyForField(FIELD_DRM_INIT_DATA), drmInitData); - bundle.putLong(keyForField(FIELD_SUBSAMPLE_OFFSET_US), subsampleOffsetUs); + bundle.putParcelable(FIELD_DRM_INIT_DATA, drmInitData); + bundle.putLong(FIELD_SUBSAMPLE_OFFSET_US, subsampleOffsetUs); // Video specific. - bundle.putInt(keyForField(FIELD_WIDTH), width); - bundle.putInt(keyForField(FIELD_HEIGHT), height); - bundle.putFloat(keyForField(FIELD_FRAME_RATE), frameRate); - bundle.putInt(keyForField(FIELD_ROTATION_DEGREES), rotationDegrees); - bundle.putFloat(keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), pixelWidthHeightRatio); - bundle.putByteArray(keyForField(FIELD_PROJECTION_DATA), projectionData); - bundle.putInt(keyForField(FIELD_STEREO_MODE), stereoMode); + bundle.putInt(FIELD_WIDTH, width); + bundle.putInt(FIELD_HEIGHT, height); + bundle.putFloat(FIELD_FRAME_RATE, frameRate); + bundle.putInt(FIELD_ROTATION_DEGREES, rotationDegrees); + bundle.putFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, pixelWidthHeightRatio); + bundle.putByteArray(FIELD_PROJECTION_DATA, projectionData); + bundle.putInt(FIELD_STEREO_MODE, stereoMode); if (colorInfo != null) { - bundle.putBundle(keyForField(FIELD_COLOR_INFO), colorInfo.toBundle()); + bundle.putBundle(FIELD_COLOR_INFO, colorInfo.toBundle()); } // Audio specific. - bundle.putInt(keyForField(FIELD_CHANNEL_COUNT), channelCount); - bundle.putInt(keyForField(FIELD_SAMPLE_RATE), sampleRate); - bundle.putInt(keyForField(FIELD_PCM_ENCODING), pcmEncoding); - bundle.putInt(keyForField(FIELD_ENCODER_DELAY), encoderDelay); - bundle.putInt(keyForField(FIELD_ENCODER_PADDING), encoderPadding); + bundle.putInt(FIELD_CHANNEL_COUNT, channelCount); + bundle.putInt(FIELD_SAMPLE_RATE, sampleRate); + bundle.putInt(FIELD_PCM_ENCODING, pcmEncoding); + bundle.putInt(FIELD_ENCODER_DELAY, encoderDelay); + bundle.putInt(FIELD_ENCODER_PADDING, encoderPadding); // Text specific. - bundle.putInt(keyForField(FIELD_ACCESSIBILITY_CHANNEL), accessibilityChannel); + bundle.putInt(FIELD_ACCESSIBILITY_CHANNEL, accessibilityChannel); // Source specific. - bundle.putInt(keyForField(FIELD_CRYPTO_TYPE), cryptoType); + bundle.putInt(FIELD_CRYPTO_TYPE, cryptoType); return bundle; } @@ -1612,28 +1569,22 @@ private static Format fromBundle(Bundle bundle) { Builder builder = new Builder(); BundleableUtil.ensureClassLoader(bundle); builder - .setId(defaultIfNull(bundle.getString(keyForField(FIELD_ID)), DEFAULT.id)) - .setLabel(defaultIfNull(bundle.getString(keyForField(FIELD_LABEL)), DEFAULT.label)) - .setLanguage(defaultIfNull(bundle.getString(keyForField(FIELD_LANGUAGE)), DEFAULT.language)) - .setSelectionFlags( - bundle.getInt(keyForField(FIELD_SELECTION_FLAGS), DEFAULT.selectionFlags)) - .setRoleFlags(bundle.getInt(keyForField(FIELD_ROLE_FLAGS), DEFAULT.roleFlags)) - .setAverageBitrate( - bundle.getInt(keyForField(FIELD_AVERAGE_BITRATE), DEFAULT.averageBitrate)) - .setPeakBitrate(bundle.getInt(keyForField(FIELD_PEAK_BITRATE), DEFAULT.peakBitrate)) - .setCodecs(defaultIfNull(bundle.getString(keyForField(FIELD_CODECS)), DEFAULT.codecs)) - .setMetadata( - defaultIfNull(bundle.getParcelable(keyForField(FIELD_METADATA)), DEFAULT.metadata)) + .setId(defaultIfNull(bundle.getString(FIELD_ID), DEFAULT.id)) + .setLabel(defaultIfNull(bundle.getString(FIELD_LABEL), DEFAULT.label)) + .setLanguage(defaultIfNull(bundle.getString(FIELD_LANGUAGE), DEFAULT.language)) + .setSelectionFlags(bundle.getInt(FIELD_SELECTION_FLAGS, DEFAULT.selectionFlags)) + .setRoleFlags(bundle.getInt(FIELD_ROLE_FLAGS, DEFAULT.roleFlags)) + .setAverageBitrate(bundle.getInt(FIELD_AVERAGE_BITRATE, DEFAULT.averageBitrate)) + .setPeakBitrate(bundle.getInt(FIELD_PEAK_BITRATE, DEFAULT.peakBitrate)) + .setCodecs(defaultIfNull(bundle.getString(FIELD_CODECS), DEFAULT.codecs)) + .setMetadata(defaultIfNull(bundle.getParcelable(FIELD_METADATA), DEFAULT.metadata)) // Container specific. .setContainerMimeType( - defaultIfNull( - bundle.getString(keyForField(FIELD_CONTAINER_MIME_TYPE)), - DEFAULT.containerMimeType)) + defaultIfNull(bundle.getString(FIELD_CONTAINER_MIME_TYPE), DEFAULT.containerMimeType)) // Sample specific. .setSampleMimeType( - defaultIfNull( - bundle.getString(keyForField(FIELD_SAMPLE_MIME_TYPE)), DEFAULT.sampleMimeType)) - .setMaxInputSize(bundle.getInt(keyForField(FIELD_MAX_INPUT_SIZE), DEFAULT.maxInputSize)); + defaultIfNull(bundle.getString(FIELD_SAMPLE_MIME_TYPE), DEFAULT.sampleMimeType)) + .setMaxInputSize(bundle.getInt(FIELD_MAX_INPUT_SIZE, DEFAULT.maxInputSize)); List initializationData = new ArrayList<>(); for (int i = 0; ; i++) { @@ -1645,47 +1596,39 @@ private static Format fromBundle(Bundle bundle) { } builder .setInitializationData(initializationData) - .setDrmInitData(bundle.getParcelable(keyForField(FIELD_DRM_INIT_DATA))) - .setSubsampleOffsetUs( - bundle.getLong(keyForField(FIELD_SUBSAMPLE_OFFSET_US), DEFAULT.subsampleOffsetUs)) + .setDrmInitData(bundle.getParcelable(FIELD_DRM_INIT_DATA)) + .setSubsampleOffsetUs(bundle.getLong(FIELD_SUBSAMPLE_OFFSET_US, DEFAULT.subsampleOffsetUs)) // Video specific. - .setWidth(bundle.getInt(keyForField(FIELD_WIDTH), DEFAULT.width)) - .setHeight(bundle.getInt(keyForField(FIELD_HEIGHT), DEFAULT.height)) - .setFrameRate(bundle.getFloat(keyForField(FIELD_FRAME_RATE), DEFAULT.frameRate)) - .setRotationDegrees( - bundle.getInt(keyForField(FIELD_ROTATION_DEGREES), DEFAULT.rotationDegrees)) + .setWidth(bundle.getInt(FIELD_WIDTH, DEFAULT.width)) + .setHeight(bundle.getInt(FIELD_HEIGHT, DEFAULT.height)) + .setFrameRate(bundle.getFloat(FIELD_FRAME_RATE, DEFAULT.frameRate)) + .setRotationDegrees(bundle.getInt(FIELD_ROTATION_DEGREES, DEFAULT.rotationDegrees)) .setPixelWidthHeightRatio( - bundle.getFloat( - keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), DEFAULT.pixelWidthHeightRatio)) - .setProjectionData(bundle.getByteArray(keyForField(FIELD_PROJECTION_DATA))) - .setStereoMode(bundle.getInt(keyForField(FIELD_STEREO_MODE), DEFAULT.stereoMode)); - Bundle colorInfoBundle = bundle.getBundle(keyForField(FIELD_COLOR_INFO)); + bundle.getFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, DEFAULT.pixelWidthHeightRatio)) + .setProjectionData(bundle.getByteArray(FIELD_PROJECTION_DATA)) + .setStereoMode(bundle.getInt(FIELD_STEREO_MODE, DEFAULT.stereoMode)); + Bundle colorInfoBundle = bundle.getBundle(FIELD_COLOR_INFO); if (colorInfoBundle != null) { builder.setColorInfo(ColorInfo.CREATOR.fromBundle(colorInfoBundle)); } // Audio specific. builder - .setChannelCount(bundle.getInt(keyForField(FIELD_CHANNEL_COUNT), DEFAULT.channelCount)) - .setSampleRate(bundle.getInt(keyForField(FIELD_SAMPLE_RATE), DEFAULT.sampleRate)) - .setPcmEncoding(bundle.getInt(keyForField(FIELD_PCM_ENCODING), DEFAULT.pcmEncoding)) - .setEncoderDelay(bundle.getInt(keyForField(FIELD_ENCODER_DELAY), DEFAULT.encoderDelay)) - .setEncoderPadding( - bundle.getInt(keyForField(FIELD_ENCODER_PADDING), DEFAULT.encoderPadding)) + .setChannelCount(bundle.getInt(FIELD_CHANNEL_COUNT, DEFAULT.channelCount)) + .setSampleRate(bundle.getInt(FIELD_SAMPLE_RATE, DEFAULT.sampleRate)) + .setPcmEncoding(bundle.getInt(FIELD_PCM_ENCODING, DEFAULT.pcmEncoding)) + .setEncoderDelay(bundle.getInt(FIELD_ENCODER_DELAY, DEFAULT.encoderDelay)) + .setEncoderPadding(bundle.getInt(FIELD_ENCODER_PADDING, DEFAULT.encoderPadding)) // Text specific. .setAccessibilityChannel( - bundle.getInt(keyForField(FIELD_ACCESSIBILITY_CHANNEL), DEFAULT.accessibilityChannel)) + bundle.getInt(FIELD_ACCESSIBILITY_CHANNEL, DEFAULT.accessibilityChannel)) // Source specific. - .setCryptoType(bundle.getInt(keyForField(FIELD_CRYPTO_TYPE), DEFAULT.cryptoType)); + .setCryptoType(bundle.getInt(FIELD_CRYPTO_TYPE, DEFAULT.cryptoType)); return builder.build(); } - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } - private static String keyForInitializationData(int initialisationDataIndex) { - return keyForField(FIELD_INITIALIZATION_DATA) + return FIELD_INITIALIZATION_DATA + "_" + Integer.toString(initialisationDataIndex, Character.MAX_RADIX); } diff --git a/libraries/common/src/main/java/androidx/media3/common/HeartRating.java b/libraries/common/src/main/java/androidx/media3/common/HeartRating.java index 08a6b405f3e..22ca4bec1b9 100644 --- a/libraries/common/src/main/java/androidx/media3/common/HeartRating.java +++ b/libraries/common/src/main/java/androidx/media3/common/HeartRating.java @@ -16,17 +16,12 @@ package androidx.media3.common; import static androidx.media3.common.util.Assertions.checkArgument; -import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.base.Objects; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** * A rating expressed as "heart" or "no heart". It can be used to indicate whether the content is a @@ -81,22 +76,16 @@ public boolean equals(@Nullable Object obj) { private static final @RatingType int TYPE = RATING_TYPE_HEART; - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_RATING_TYPE, FIELD_RATED, FIELD_IS_HEART}) - private @interface FieldNumber {} - - private static final int FIELD_RATED = 1; - private static final int FIELD_IS_HEART = 2; + private static final String FIELD_RATED = Util.intToStringMaxRadix(1); + private static final String FIELD_IS_HEART = Util.intToStringMaxRadix(2); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE); - bundle.putBoolean(keyForField(FIELD_RATED), rated); - bundle.putBoolean(keyForField(FIELD_IS_HEART), isHeart); + bundle.putInt(FIELD_RATING_TYPE, TYPE); + bundle.putBoolean(FIELD_RATED, rated); + bundle.putBoolean(FIELD_IS_HEART, isHeart); return bundle; } @@ -104,16 +93,10 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = HeartRating::fromBundle; private static HeartRating fromBundle(Bundle bundle) { - checkArgument( - bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET) - == TYPE); - boolean isRated = bundle.getBoolean(keyForField(FIELD_RATED), /* defaultValue= */ false); + checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE); + boolean isRated = bundle.getBoolean(FIELD_RATED, /* defaultValue= */ false); return isRated - ? new HeartRating(bundle.getBoolean(keyForField(FIELD_IS_HEART), /* defaultValue= */ false)) + ? new HeartRating(bundle.getBoolean(FIELD_IS_HEART, /* defaultValue= */ false)) : new HeartRating(); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java index 68f3a828828..4db28ca61fd 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java @@ -17,11 +17,9 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; -import static java.lang.annotation.ElementType.TYPE_USE; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.annotation.Nullable; import androidx.media3.common.util.Assertions; @@ -31,10 +29,6 @@ import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.InlineMe; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -1304,42 +1298,30 @@ public int hashCode() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_TARGET_OFFSET_MS, - FIELD_MIN_OFFSET_MS, - FIELD_MAX_OFFSET_MS, - FIELD_MIN_PLAYBACK_SPEED, - FIELD_MAX_PLAYBACK_SPEED - }) - private @interface FieldNumber {} - - private static final int FIELD_TARGET_OFFSET_MS = 0; - private static final int FIELD_MIN_OFFSET_MS = 1; - private static final int FIELD_MAX_OFFSET_MS = 2; - private static final int FIELD_MIN_PLAYBACK_SPEED = 3; - private static final int FIELD_MAX_PLAYBACK_SPEED = 4; + private static final String FIELD_TARGET_OFFSET_MS = Util.intToStringMaxRadix(0); + private static final String FIELD_MIN_OFFSET_MS = Util.intToStringMaxRadix(1); + private static final String FIELD_MAX_OFFSET_MS = Util.intToStringMaxRadix(2); + private static final String FIELD_MIN_PLAYBACK_SPEED = Util.intToStringMaxRadix(3); + private static final String FIELD_MAX_PLAYBACK_SPEED = Util.intToStringMaxRadix(4); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); if (targetOffsetMs != UNSET.targetOffsetMs) { - bundle.putLong(keyForField(FIELD_TARGET_OFFSET_MS), targetOffsetMs); + bundle.putLong(FIELD_TARGET_OFFSET_MS, targetOffsetMs); } if (minOffsetMs != UNSET.minOffsetMs) { - bundle.putLong(keyForField(FIELD_MIN_OFFSET_MS), minOffsetMs); + bundle.putLong(FIELD_MIN_OFFSET_MS, minOffsetMs); } if (maxOffsetMs != UNSET.maxOffsetMs) { - bundle.putLong(keyForField(FIELD_MAX_OFFSET_MS), maxOffsetMs); + bundle.putLong(FIELD_MAX_OFFSET_MS, maxOffsetMs); } if (minPlaybackSpeed != UNSET.minPlaybackSpeed) { - bundle.putFloat(keyForField(FIELD_MIN_PLAYBACK_SPEED), minPlaybackSpeed); + bundle.putFloat(FIELD_MIN_PLAYBACK_SPEED, minPlaybackSpeed); } if (maxPlaybackSpeed != UNSET.maxPlaybackSpeed) { - bundle.putFloat(keyForField(FIELD_MAX_PLAYBACK_SPEED), maxPlaybackSpeed); + bundle.putFloat(FIELD_MAX_PLAYBACK_SPEED, maxPlaybackSpeed); } return bundle; } @@ -1349,22 +1331,13 @@ public Bundle toBundle() { public static final Creator CREATOR = bundle -> new LiveConfiguration( - bundle.getLong( - keyForField(FIELD_TARGET_OFFSET_MS), /* defaultValue= */ UNSET.targetOffsetMs), - bundle.getLong( - keyForField(FIELD_MIN_OFFSET_MS), /* defaultValue= */ UNSET.minOffsetMs), - bundle.getLong( - keyForField(FIELD_MAX_OFFSET_MS), /* defaultValue= */ UNSET.maxOffsetMs), + bundle.getLong(FIELD_TARGET_OFFSET_MS, /* defaultValue= */ UNSET.targetOffsetMs), + bundle.getLong(FIELD_MIN_OFFSET_MS, /* defaultValue= */ UNSET.minOffsetMs), + bundle.getLong(FIELD_MAX_OFFSET_MS, /* defaultValue= */ UNSET.maxOffsetMs), bundle.getFloat( - keyForField(FIELD_MIN_PLAYBACK_SPEED), - /* defaultValue= */ UNSET.minPlaybackSpeed), + FIELD_MIN_PLAYBACK_SPEED, /* defaultValue= */ UNSET.minPlaybackSpeed), bundle.getFloat( - keyForField(FIELD_MAX_PLAYBACK_SPEED), - /* defaultValue= */ UNSET.maxPlaybackSpeed)); - - private static String keyForField(@LiveConfiguration.FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } + FIELD_MAX_PLAYBACK_SPEED, /* defaultValue= */ UNSET.maxPlaybackSpeed)); } /** Properties for a text track. */ @@ -1756,43 +1729,30 @@ public int hashCode() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_START_POSITION_MS, - FIELD_END_POSITION_MS, - FIELD_RELATIVE_TO_LIVE_WINDOW, - FIELD_RELATIVE_TO_DEFAULT_POSITION, - FIELD_STARTS_AT_KEY_FRAME - }) - private @interface FieldNumber {} - - private static final int FIELD_START_POSITION_MS = 0; - private static final int FIELD_END_POSITION_MS = 1; - private static final int FIELD_RELATIVE_TO_LIVE_WINDOW = 2; - private static final int FIELD_RELATIVE_TO_DEFAULT_POSITION = 3; - private static final int FIELD_STARTS_AT_KEY_FRAME = 4; + private static final String FIELD_START_POSITION_MS = Util.intToStringMaxRadix(0); + private static final String FIELD_END_POSITION_MS = Util.intToStringMaxRadix(1); + private static final String FIELD_RELATIVE_TO_LIVE_WINDOW = Util.intToStringMaxRadix(2); + private static final String FIELD_RELATIVE_TO_DEFAULT_POSITION = Util.intToStringMaxRadix(3); + private static final String FIELD_STARTS_AT_KEY_FRAME = Util.intToStringMaxRadix(4); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); if (startPositionMs != UNSET.startPositionMs) { - bundle.putLong(keyForField(FIELD_START_POSITION_MS), startPositionMs); + bundle.putLong(FIELD_START_POSITION_MS, startPositionMs); } if (endPositionMs != UNSET.endPositionMs) { - bundle.putLong(keyForField(FIELD_END_POSITION_MS), endPositionMs); + bundle.putLong(FIELD_END_POSITION_MS, endPositionMs); } if (relativeToLiveWindow != UNSET.relativeToLiveWindow) { - bundle.putBoolean(keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), relativeToLiveWindow); + bundle.putBoolean(FIELD_RELATIVE_TO_LIVE_WINDOW, relativeToLiveWindow); } if (relativeToDefaultPosition != UNSET.relativeToDefaultPosition) { - bundle.putBoolean( - keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), relativeToDefaultPosition); + bundle.putBoolean(FIELD_RELATIVE_TO_DEFAULT_POSITION, relativeToDefaultPosition); } if (startsAtKeyFrame != UNSET.startsAtKeyFrame) { - bundle.putBoolean(keyForField(FIELD_STARTS_AT_KEY_FRAME), startsAtKeyFrame); + bundle.putBoolean(FIELD_STARTS_AT_KEY_FRAME, startsAtKeyFrame); } return bundle; } @@ -1804,29 +1764,21 @@ public Bundle toBundle() { new ClippingConfiguration.Builder() .setStartPositionMs( bundle.getLong( - keyForField(FIELD_START_POSITION_MS), - /* defaultValue= */ UNSET.startPositionMs)) + FIELD_START_POSITION_MS, /* defaultValue= */ UNSET.startPositionMs)) .setEndPositionMs( - bundle.getLong( - keyForField(FIELD_END_POSITION_MS), - /* defaultValue= */ UNSET.endPositionMs)) + bundle.getLong(FIELD_END_POSITION_MS, /* defaultValue= */ UNSET.endPositionMs)) .setRelativeToLiveWindow( bundle.getBoolean( - keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), + FIELD_RELATIVE_TO_LIVE_WINDOW, /* defaultValue= */ UNSET.relativeToLiveWindow)) .setRelativeToDefaultPosition( bundle.getBoolean( - keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), + FIELD_RELATIVE_TO_DEFAULT_POSITION, /* defaultValue= */ UNSET.relativeToDefaultPosition)) .setStartsAtKeyFrame( bundle.getBoolean( - keyForField(FIELD_STARTS_AT_KEY_FRAME), - /* defaultValue= */ UNSET.startsAtKeyFrame)) + FIELD_STARTS_AT_KEY_FRAME, /* defaultValue= */ UNSET.startsAtKeyFrame)) .buildClippingProperties(); - - private static String keyForField(@ClippingConfiguration.FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } /** @@ -1945,28 +1897,22 @@ public int hashCode() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_MEDIA_URI, FIELD_SEARCH_QUERY, FIELD_EXTRAS}) - private @interface FieldNumber {} - - private static final int FIELD_MEDIA_URI = 0; - private static final int FIELD_SEARCH_QUERY = 1; - private static final int FIELD_EXTRAS = 2; + private static final String FIELD_MEDIA_URI = Util.intToStringMaxRadix(0); + private static final String FIELD_SEARCH_QUERY = Util.intToStringMaxRadix(1); + private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(2); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); if (mediaUri != null) { - bundle.putParcelable(keyForField(FIELD_MEDIA_URI), mediaUri); + bundle.putParcelable(FIELD_MEDIA_URI, mediaUri); } if (searchQuery != null) { - bundle.putString(keyForField(FIELD_SEARCH_QUERY), searchQuery); + bundle.putString(FIELD_SEARCH_QUERY, searchQuery); } if (extras != null) { - bundle.putBundle(keyForField(FIELD_EXTRAS), extras); + bundle.putBundle(FIELD_EXTRAS, extras); } return bundle; } @@ -1976,14 +1922,10 @@ public Bundle toBundle() { public static final Creator CREATOR = bundle -> new RequestMetadata.Builder() - .setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI))) - .setSearchQuery(bundle.getString(keyForField(FIELD_SEARCH_QUERY))) - .setExtras(bundle.getBundle(keyForField(FIELD_EXTRAS))) + .setMediaUri(bundle.getParcelable(FIELD_MEDIA_URI)) + .setSearchQuery(bundle.getString(FIELD_SEARCH_QUERY)) + .setExtras(bundle.getBundle(FIELD_EXTRAS)) .build(); - - private static String keyForField(@RequestMetadata.FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } /** @@ -2079,24 +2021,11 @@ public int hashCode() { } // Bundleable implementation. - - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_MEDIA_ID, - FIELD_LIVE_CONFIGURATION, - FIELD_MEDIA_METADATA, - FIELD_CLIPPING_PROPERTIES, - FIELD_REQUEST_METADATA - }) - private @interface FieldNumber {} - - private static final int FIELD_MEDIA_ID = 0; - private static final int FIELD_LIVE_CONFIGURATION = 1; - private static final int FIELD_MEDIA_METADATA = 2; - private static final int FIELD_CLIPPING_PROPERTIES = 3; - private static final int FIELD_REQUEST_METADATA = 4; + private static final String FIELD_MEDIA_ID = Util.intToStringMaxRadix(0); + private static final String FIELD_LIVE_CONFIGURATION = Util.intToStringMaxRadix(1); + private static final String FIELD_MEDIA_METADATA = Util.intToStringMaxRadix(2); + private static final String FIELD_CLIPPING_PROPERTIES = Util.intToStringMaxRadix(3); + private static final String FIELD_REQUEST_METADATA = Util.intToStringMaxRadix(4); /** * {@inheritDoc} @@ -2109,19 +2038,19 @@ public int hashCode() { public Bundle toBundle() { Bundle bundle = new Bundle(); if (!mediaId.equals(DEFAULT_MEDIA_ID)) { - bundle.putString(keyForField(FIELD_MEDIA_ID), mediaId); + bundle.putString(FIELD_MEDIA_ID, mediaId); } if (!liveConfiguration.equals(LiveConfiguration.UNSET)) { - bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle()); + bundle.putBundle(FIELD_LIVE_CONFIGURATION, liveConfiguration.toBundle()); } if (!mediaMetadata.equals(MediaMetadata.EMPTY)) { - bundle.putBundle(keyForField(FIELD_MEDIA_METADATA), mediaMetadata.toBundle()); + bundle.putBundle(FIELD_MEDIA_METADATA, mediaMetadata.toBundle()); } if (!clippingConfiguration.equals(ClippingConfiguration.UNSET)) { - bundle.putBundle(keyForField(FIELD_CLIPPING_PROPERTIES), clippingConfiguration.toBundle()); + bundle.putBundle(FIELD_CLIPPING_PROPERTIES, clippingConfiguration.toBundle()); } if (!requestMetadata.equals(RequestMetadata.EMPTY)) { - bundle.putBundle(keyForField(FIELD_REQUEST_METADATA), requestMetadata.toBundle()); + bundle.putBundle(FIELD_REQUEST_METADATA, requestMetadata.toBundle()); } return bundle; } @@ -2135,31 +2064,29 @@ public Bundle toBundle() { @SuppressWarnings("deprecation") // Unbundling to ClippingProperties while it still exists. private static MediaItem fromBundle(Bundle bundle) { - String mediaId = checkNotNull(bundle.getString(keyForField(FIELD_MEDIA_ID), DEFAULT_MEDIA_ID)); - @Nullable - Bundle liveConfigurationBundle = bundle.getBundle(keyForField(FIELD_LIVE_CONFIGURATION)); + String mediaId = checkNotNull(bundle.getString(FIELD_MEDIA_ID, DEFAULT_MEDIA_ID)); + @Nullable Bundle liveConfigurationBundle = bundle.getBundle(FIELD_LIVE_CONFIGURATION); LiveConfiguration liveConfiguration; if (liveConfigurationBundle == null) { liveConfiguration = LiveConfiguration.UNSET; } else { liveConfiguration = LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle); } - @Nullable Bundle mediaMetadataBundle = bundle.getBundle(keyForField(FIELD_MEDIA_METADATA)); + @Nullable Bundle mediaMetadataBundle = bundle.getBundle(FIELD_MEDIA_METADATA); MediaMetadata mediaMetadata; if (mediaMetadataBundle == null) { mediaMetadata = MediaMetadata.EMPTY; } else { mediaMetadata = MediaMetadata.CREATOR.fromBundle(mediaMetadataBundle); } - @Nullable - Bundle clippingConfigurationBundle = bundle.getBundle(keyForField(FIELD_CLIPPING_PROPERTIES)); + @Nullable Bundle clippingConfigurationBundle = bundle.getBundle(FIELD_CLIPPING_PROPERTIES); ClippingProperties clippingConfiguration; if (clippingConfigurationBundle == null) { clippingConfiguration = ClippingProperties.UNSET; } else { clippingConfiguration = ClippingConfiguration.CREATOR.fromBundle(clippingConfigurationBundle); } - @Nullable Bundle requestMetadataBundle = bundle.getBundle(keyForField(FIELD_REQUEST_METADATA)); + @Nullable Bundle requestMetadataBundle = bundle.getBundle(FIELD_REQUEST_METADATA); RequestMetadata requestMetadata; if (requestMetadataBundle == null) { requestMetadata = RequestMetadata.EMPTY; @@ -2174,8 +2101,4 @@ private static MediaItem fromBundle(Bundle bundle) { mediaMetadata, requestMetadata); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java index 9f6b0f2035f..822932377fe 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java @@ -1103,184 +1103,143 @@ public int hashCode() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_TITLE, - FIELD_ARTIST, - FIELD_ALBUM_TITLE, - FIELD_ALBUM_ARTIST, - FIELD_DISPLAY_TITLE, - FIELD_SUBTITLE, - FIELD_DESCRIPTION, - FIELD_MEDIA_URI, - FIELD_USER_RATING, - FIELD_OVERALL_RATING, - FIELD_ARTWORK_DATA, - FIELD_ARTWORK_DATA_TYPE, - FIELD_ARTWORK_URI, - FIELD_TRACK_NUMBER, - FIELD_TOTAL_TRACK_COUNT, - FIELD_FOLDER_TYPE, - FIELD_IS_PLAYABLE, - FIELD_RECORDING_YEAR, - FIELD_RECORDING_MONTH, - FIELD_RECORDING_DAY, - FIELD_RELEASE_YEAR, - FIELD_RELEASE_MONTH, - FIELD_RELEASE_DAY, - FIELD_WRITER, - FIELD_COMPOSER, - FIELD_CONDUCTOR, - FIELD_DISC_NUMBER, - FIELD_TOTAL_DISC_COUNT, - FIELD_GENRE, - FIELD_COMPILATION, - FIELD_STATION, - FIELD_MEDIA_TYPE, - FIELD_IS_BROWSABLE, - FIELD_EXTRAS, - }) - private @interface FieldNumber {} - - private static final int FIELD_TITLE = 0; - private static final int FIELD_ARTIST = 1; - private static final int FIELD_ALBUM_TITLE = 2; - private static final int FIELD_ALBUM_ARTIST = 3; - private static final int FIELD_DISPLAY_TITLE = 4; - private static final int FIELD_SUBTITLE = 5; - private static final int FIELD_DESCRIPTION = 6; - private static final int FIELD_MEDIA_URI = 7; - private static final int FIELD_USER_RATING = 8; - private static final int FIELD_OVERALL_RATING = 9; - private static final int FIELD_ARTWORK_DATA = 10; - private static final int FIELD_ARTWORK_URI = 11; - private static final int FIELD_TRACK_NUMBER = 12; - private static final int FIELD_TOTAL_TRACK_COUNT = 13; - private static final int FIELD_FOLDER_TYPE = 14; - private static final int FIELD_IS_PLAYABLE = 15; - private static final int FIELD_RECORDING_YEAR = 16; - private static final int FIELD_RECORDING_MONTH = 17; - private static final int FIELD_RECORDING_DAY = 18; - private static final int FIELD_RELEASE_YEAR = 19; - private static final int FIELD_RELEASE_MONTH = 20; - private static final int FIELD_RELEASE_DAY = 21; - private static final int FIELD_WRITER = 22; - private static final int FIELD_COMPOSER = 23; - private static final int FIELD_CONDUCTOR = 24; - private static final int FIELD_DISC_NUMBER = 25; - private static final int FIELD_TOTAL_DISC_COUNT = 26; - private static final int FIELD_GENRE = 27; - private static final int FIELD_COMPILATION = 28; - private static final int FIELD_ARTWORK_DATA_TYPE = 29; - private static final int FIELD_STATION = 30; - private static final int FIELD_MEDIA_TYPE = 31; - private static final int FIELD_IS_BROWSABLE = 32; - private static final int FIELD_EXTRAS = 1000; + private static final String FIELD_TITLE = Util.intToStringMaxRadix(0); + private static final String FIELD_ARTIST = Util.intToStringMaxRadix(1); + private static final String FIELD_ALBUM_TITLE = Util.intToStringMaxRadix(2); + private static final String FIELD_ALBUM_ARTIST = Util.intToStringMaxRadix(3); + private static final String FIELD_DISPLAY_TITLE = Util.intToStringMaxRadix(4); + private static final String FIELD_SUBTITLE = Util.intToStringMaxRadix(5); + private static final String FIELD_DESCRIPTION = Util.intToStringMaxRadix(6); + // 7 is reserved to maintain backward compatibility for a previously defined field. + private static final String FIELD_USER_RATING = Util.intToStringMaxRadix(8); + private static final String FIELD_OVERALL_RATING = Util.intToStringMaxRadix(9); + private static final String FIELD_ARTWORK_DATA = Util.intToStringMaxRadix(10); + private static final String FIELD_ARTWORK_URI = Util.intToStringMaxRadix(11); + private static final String FIELD_TRACK_NUMBER = Util.intToStringMaxRadix(12); + private static final String FIELD_TOTAL_TRACK_COUNT = Util.intToStringMaxRadix(13); + private static final String FIELD_FOLDER_TYPE = Util.intToStringMaxRadix(14); + private static final String FIELD_IS_PLAYABLE = Util.intToStringMaxRadix(15); + private static final String FIELD_RECORDING_YEAR = Util.intToStringMaxRadix(16); + private static final String FIELD_RECORDING_MONTH = Util.intToStringMaxRadix(17); + private static final String FIELD_RECORDING_DAY = Util.intToStringMaxRadix(18); + private static final String FIELD_RELEASE_YEAR = Util.intToStringMaxRadix(19); + private static final String FIELD_RELEASE_MONTH = Util.intToStringMaxRadix(20); + private static final String FIELD_RELEASE_DAY = Util.intToStringMaxRadix(21); + private static final String FIELD_WRITER = Util.intToStringMaxRadix(22); + private static final String FIELD_COMPOSER = Util.intToStringMaxRadix(23); + private static final String FIELD_CONDUCTOR = Util.intToStringMaxRadix(24); + private static final String FIELD_DISC_NUMBER = Util.intToStringMaxRadix(25); + private static final String FIELD_TOTAL_DISC_COUNT = Util.intToStringMaxRadix(26); + private static final String FIELD_GENRE = Util.intToStringMaxRadix(27); + private static final String FIELD_COMPILATION = Util.intToStringMaxRadix(28); + private static final String FIELD_ARTWORK_DATA_TYPE = Util.intToStringMaxRadix(29); + private static final String FIELD_STATION = Util.intToStringMaxRadix(30); + private static final String FIELD_MEDIA_TYPE = Util.intToStringMaxRadix(31); + private static final String FIELD_IS_BROWSABLE = Util.intToStringMaxRadix(32); + private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(1000); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); if (title != null) { - bundle.putCharSequence(keyForField(FIELD_TITLE), title); + bundle.putCharSequence(FIELD_TITLE, title); } if (artist != null) { - bundle.putCharSequence(keyForField(FIELD_ARTIST), artist); + bundle.putCharSequence(FIELD_ARTIST, artist); } if (albumTitle != null) { - bundle.putCharSequence(keyForField(FIELD_ALBUM_TITLE), albumTitle); + bundle.putCharSequence(FIELD_ALBUM_TITLE, albumTitle); } if (albumArtist != null) { - bundle.putCharSequence(keyForField(FIELD_ALBUM_ARTIST), albumArtist); + bundle.putCharSequence(FIELD_ALBUM_ARTIST, albumArtist); } if (displayTitle != null) { - bundle.putCharSequence(keyForField(FIELD_DISPLAY_TITLE), displayTitle); + bundle.putCharSequence(FIELD_DISPLAY_TITLE, displayTitle); } if (subtitle != null) { - bundle.putCharSequence(keyForField(FIELD_SUBTITLE), subtitle); + bundle.putCharSequence(FIELD_SUBTITLE, subtitle); } if (description != null) { - bundle.putCharSequence(keyForField(FIELD_DESCRIPTION), description); + bundle.putCharSequence(FIELD_DESCRIPTION, description); } if (artworkData != null) { - bundle.putByteArray(keyForField(FIELD_ARTWORK_DATA), artworkData); + bundle.putByteArray(FIELD_ARTWORK_DATA, artworkData); } if (artworkUri != null) { - bundle.putParcelable(keyForField(FIELD_ARTWORK_URI), artworkUri); + bundle.putParcelable(FIELD_ARTWORK_URI, artworkUri); } if (writer != null) { - bundle.putCharSequence(keyForField(FIELD_WRITER), writer); + bundle.putCharSequence(FIELD_WRITER, writer); } if (composer != null) { - bundle.putCharSequence(keyForField(FIELD_COMPOSER), composer); + bundle.putCharSequence(FIELD_COMPOSER, composer); } if (conductor != null) { - bundle.putCharSequence(keyForField(FIELD_CONDUCTOR), conductor); + bundle.putCharSequence(FIELD_CONDUCTOR, conductor); } if (genre != null) { - bundle.putCharSequence(keyForField(FIELD_GENRE), genre); + bundle.putCharSequence(FIELD_GENRE, genre); } if (compilation != null) { - bundle.putCharSequence(keyForField(FIELD_COMPILATION), compilation); + bundle.putCharSequence(FIELD_COMPILATION, compilation); } if (station != null) { - bundle.putCharSequence(keyForField(FIELD_STATION), station); + bundle.putCharSequence(FIELD_STATION, station); } if (userRating != null) { - bundle.putBundle(keyForField(FIELD_USER_RATING), userRating.toBundle()); + bundle.putBundle(FIELD_USER_RATING, userRating.toBundle()); } if (overallRating != null) { - bundle.putBundle(keyForField(FIELD_OVERALL_RATING), overallRating.toBundle()); + bundle.putBundle(FIELD_OVERALL_RATING, overallRating.toBundle()); } if (trackNumber != null) { - bundle.putInt(keyForField(FIELD_TRACK_NUMBER), trackNumber); + bundle.putInt(FIELD_TRACK_NUMBER, trackNumber); } if (totalTrackCount != null) { - bundle.putInt(keyForField(FIELD_TOTAL_TRACK_COUNT), totalTrackCount); + bundle.putInt(FIELD_TOTAL_TRACK_COUNT, totalTrackCount); } if (folderType != null) { - bundle.putInt(keyForField(FIELD_FOLDER_TYPE), folderType); + bundle.putInt(FIELD_FOLDER_TYPE, folderType); } if (isBrowsable != null) { - bundle.putBoolean(keyForField(FIELD_IS_BROWSABLE), isBrowsable); + bundle.putBoolean(FIELD_IS_BROWSABLE, isBrowsable); } if (isPlayable != null) { - bundle.putBoolean(keyForField(FIELD_IS_PLAYABLE), isPlayable); + bundle.putBoolean(FIELD_IS_PLAYABLE, isPlayable); } if (recordingYear != null) { - bundle.putInt(keyForField(FIELD_RECORDING_YEAR), recordingYear); + bundle.putInt(FIELD_RECORDING_YEAR, recordingYear); } if (recordingMonth != null) { - bundle.putInt(keyForField(FIELD_RECORDING_MONTH), recordingMonth); + bundle.putInt(FIELD_RECORDING_MONTH, recordingMonth); } if (recordingDay != null) { - bundle.putInt(keyForField(FIELD_RECORDING_DAY), recordingDay); + bundle.putInt(FIELD_RECORDING_DAY, recordingDay); } if (releaseYear != null) { - bundle.putInt(keyForField(FIELD_RELEASE_YEAR), releaseYear); + bundle.putInt(FIELD_RELEASE_YEAR, releaseYear); } if (releaseMonth != null) { - bundle.putInt(keyForField(FIELD_RELEASE_MONTH), releaseMonth); + bundle.putInt(FIELD_RELEASE_MONTH, releaseMonth); } if (releaseDay != null) { - bundle.putInt(keyForField(FIELD_RELEASE_DAY), releaseDay); + bundle.putInt(FIELD_RELEASE_DAY, releaseDay); } if (discNumber != null) { - bundle.putInt(keyForField(FIELD_DISC_NUMBER), discNumber); + bundle.putInt(FIELD_DISC_NUMBER, discNumber); } if (totalDiscCount != null) { - bundle.putInt(keyForField(FIELD_TOTAL_DISC_COUNT), totalDiscCount); + bundle.putInt(FIELD_TOTAL_DISC_COUNT, totalDiscCount); } if (artworkDataType != null) { - bundle.putInt(keyForField(FIELD_ARTWORK_DATA_TYPE), artworkDataType); + bundle.putInt(FIELD_ARTWORK_DATA_TYPE, artworkDataType); } if (mediaType != null) { - bundle.putInt(keyForField(FIELD_MEDIA_TYPE), mediaType); + bundle.putInt(FIELD_MEDIA_TYPE, mediaType); } if (extras != null) { - bundle.putBundle(keyForField(FIELD_EXTRAS), extras); + bundle.putBundle(FIELD_EXTRAS, extras); } return bundle; } @@ -1291,89 +1250,85 @@ public Bundle toBundle() { private static MediaMetadata fromBundle(Bundle bundle) { Builder builder = new Builder(); builder - .setTitle(bundle.getCharSequence(keyForField(FIELD_TITLE))) - .setArtist(bundle.getCharSequence(keyForField(FIELD_ARTIST))) - .setAlbumTitle(bundle.getCharSequence(keyForField(FIELD_ALBUM_TITLE))) - .setAlbumArtist(bundle.getCharSequence(keyForField(FIELD_ALBUM_ARTIST))) - .setDisplayTitle(bundle.getCharSequence(keyForField(FIELD_DISPLAY_TITLE))) - .setSubtitle(bundle.getCharSequence(keyForField(FIELD_SUBTITLE))) - .setDescription(bundle.getCharSequence(keyForField(FIELD_DESCRIPTION))) + .setTitle(bundle.getCharSequence(FIELD_TITLE)) + .setArtist(bundle.getCharSequence(FIELD_ARTIST)) + .setAlbumTitle(bundle.getCharSequence(FIELD_ALBUM_TITLE)) + .setAlbumArtist(bundle.getCharSequence(FIELD_ALBUM_ARTIST)) + .setDisplayTitle(bundle.getCharSequence(FIELD_DISPLAY_TITLE)) + .setSubtitle(bundle.getCharSequence(FIELD_SUBTITLE)) + .setDescription(bundle.getCharSequence(FIELD_DESCRIPTION)) .setArtworkData( - bundle.getByteArray(keyForField(FIELD_ARTWORK_DATA)), - bundle.containsKey(keyForField(FIELD_ARTWORK_DATA_TYPE)) - ? bundle.getInt(keyForField(FIELD_ARTWORK_DATA_TYPE)) + bundle.getByteArray(FIELD_ARTWORK_DATA), + bundle.containsKey(FIELD_ARTWORK_DATA_TYPE) + ? bundle.getInt(FIELD_ARTWORK_DATA_TYPE) : null) - .setArtworkUri(bundle.getParcelable(keyForField(FIELD_ARTWORK_URI))) - .setWriter(bundle.getCharSequence(keyForField(FIELD_WRITER))) - .setComposer(bundle.getCharSequence(keyForField(FIELD_COMPOSER))) - .setConductor(bundle.getCharSequence(keyForField(FIELD_CONDUCTOR))) - .setGenre(bundle.getCharSequence(keyForField(FIELD_GENRE))) - .setCompilation(bundle.getCharSequence(keyForField(FIELD_COMPILATION))) - .setStation(bundle.getCharSequence(keyForField(FIELD_STATION))) - .setExtras(bundle.getBundle(keyForField(FIELD_EXTRAS))); - - if (bundle.containsKey(keyForField(FIELD_USER_RATING))) { - @Nullable Bundle fieldBundle = bundle.getBundle(keyForField(FIELD_USER_RATING)); + .setArtworkUri(bundle.getParcelable(FIELD_ARTWORK_URI)) + .setWriter(bundle.getCharSequence(FIELD_WRITER)) + .setComposer(bundle.getCharSequence(FIELD_COMPOSER)) + .setConductor(bundle.getCharSequence(FIELD_CONDUCTOR)) + .setGenre(bundle.getCharSequence(FIELD_GENRE)) + .setCompilation(bundle.getCharSequence(FIELD_COMPILATION)) + .setStation(bundle.getCharSequence(FIELD_STATION)) + .setExtras(bundle.getBundle(FIELD_EXTRAS)); + + if (bundle.containsKey(FIELD_USER_RATING)) { + @Nullable Bundle fieldBundle = bundle.getBundle(FIELD_USER_RATING); if (fieldBundle != null) { builder.setUserRating(Rating.CREATOR.fromBundle(fieldBundle)); } } - if (bundle.containsKey(keyForField(FIELD_OVERALL_RATING))) { - @Nullable Bundle fieldBundle = bundle.getBundle(keyForField(FIELD_OVERALL_RATING)); + if (bundle.containsKey(FIELD_OVERALL_RATING)) { + @Nullable Bundle fieldBundle = bundle.getBundle(FIELD_OVERALL_RATING); if (fieldBundle != null) { builder.setOverallRating(Rating.CREATOR.fromBundle(fieldBundle)); } } - if (bundle.containsKey(keyForField(FIELD_TRACK_NUMBER))) { - builder.setTrackNumber(bundle.getInt(keyForField(FIELD_TRACK_NUMBER))); + if (bundle.containsKey(FIELD_TRACK_NUMBER)) { + builder.setTrackNumber(bundle.getInt(FIELD_TRACK_NUMBER)); } - if (bundle.containsKey(keyForField(FIELD_TOTAL_TRACK_COUNT))) { - builder.setTotalTrackCount(bundle.getInt(keyForField(FIELD_TOTAL_TRACK_COUNT))); + if (bundle.containsKey(FIELD_TOTAL_TRACK_COUNT)) { + builder.setTotalTrackCount(bundle.getInt(FIELD_TOTAL_TRACK_COUNT)); } - if (bundle.containsKey(keyForField(FIELD_FOLDER_TYPE))) { - builder.setFolderType(bundle.getInt(keyForField(FIELD_FOLDER_TYPE))); + if (bundle.containsKey(FIELD_FOLDER_TYPE)) { + builder.setFolderType(bundle.getInt(FIELD_FOLDER_TYPE)); } - if (bundle.containsKey(keyForField(FIELD_IS_BROWSABLE))) { - builder.setIsBrowsable(bundle.getBoolean(keyForField(FIELD_IS_BROWSABLE))); + if (bundle.containsKey(FIELD_IS_BROWSABLE)) { + builder.setIsBrowsable(bundle.getBoolean(FIELD_IS_BROWSABLE)); } - if (bundle.containsKey(keyForField(FIELD_IS_PLAYABLE))) { - builder.setIsPlayable(bundle.getBoolean(keyForField(FIELD_IS_PLAYABLE))); + if (bundle.containsKey(FIELD_IS_PLAYABLE)) { + builder.setIsPlayable(bundle.getBoolean(FIELD_IS_PLAYABLE)); } - if (bundle.containsKey(keyForField(FIELD_RECORDING_YEAR))) { - builder.setRecordingYear(bundle.getInt(keyForField(FIELD_RECORDING_YEAR))); + if (bundle.containsKey(FIELD_RECORDING_YEAR)) { + builder.setRecordingYear(bundle.getInt(FIELD_RECORDING_YEAR)); } - if (bundle.containsKey(keyForField(FIELD_RECORDING_MONTH))) { - builder.setRecordingMonth(bundle.getInt(keyForField(FIELD_RECORDING_MONTH))); + if (bundle.containsKey(FIELD_RECORDING_MONTH)) { + builder.setRecordingMonth(bundle.getInt(FIELD_RECORDING_MONTH)); } - if (bundle.containsKey(keyForField(FIELD_RECORDING_DAY))) { - builder.setRecordingDay(bundle.getInt(keyForField(FIELD_RECORDING_DAY))); + if (bundle.containsKey(FIELD_RECORDING_DAY)) { + builder.setRecordingDay(bundle.getInt(FIELD_RECORDING_DAY)); } - if (bundle.containsKey(keyForField(FIELD_RELEASE_YEAR))) { - builder.setReleaseYear(bundle.getInt(keyForField(FIELD_RELEASE_YEAR))); + if (bundle.containsKey(FIELD_RELEASE_YEAR)) { + builder.setReleaseYear(bundle.getInt(FIELD_RELEASE_YEAR)); } - if (bundle.containsKey(keyForField(FIELD_RELEASE_MONTH))) { - builder.setReleaseMonth(bundle.getInt(keyForField(FIELD_RELEASE_MONTH))); + if (bundle.containsKey(FIELD_RELEASE_MONTH)) { + builder.setReleaseMonth(bundle.getInt(FIELD_RELEASE_MONTH)); } - if (bundle.containsKey(keyForField(FIELD_RELEASE_DAY))) { - builder.setReleaseDay(bundle.getInt(keyForField(FIELD_RELEASE_DAY))); + if (bundle.containsKey(FIELD_RELEASE_DAY)) { + builder.setReleaseDay(bundle.getInt(FIELD_RELEASE_DAY)); } - if (bundle.containsKey(keyForField(FIELD_DISC_NUMBER))) { - builder.setDiscNumber(bundle.getInt(keyForField(FIELD_DISC_NUMBER))); + if (bundle.containsKey(FIELD_DISC_NUMBER)) { + builder.setDiscNumber(bundle.getInt(FIELD_DISC_NUMBER)); } - if (bundle.containsKey(keyForField(FIELD_TOTAL_DISC_COUNT))) { - builder.setTotalDiscCount(bundle.getInt(keyForField(FIELD_TOTAL_DISC_COUNT))); + if (bundle.containsKey(FIELD_TOTAL_DISC_COUNT)) { + builder.setTotalDiscCount(bundle.getInt(FIELD_TOTAL_DISC_COUNT)); } - if (bundle.containsKey(keyForField(FIELD_MEDIA_TYPE))) { - builder.setMediaType(bundle.getInt(keyForField(FIELD_MEDIA_TYPE))); + if (bundle.containsKey(FIELD_MEDIA_TYPE)) { + builder.setMediaType(bundle.getInt(FIELD_MEDIA_TYPE)); } return builder.build(); } - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } - private static @FolderType int getFolderTypeFromMediaType(@MediaType int mediaType) { switch (mediaType) { case MEDIA_TYPE_ALBUM: diff --git a/libraries/common/src/main/java/androidx/media3/common/PercentageRating.java b/libraries/common/src/main/java/androidx/media3/common/PercentageRating.java index afc20a16877..8504f3a6ce0 100644 --- a/libraries/common/src/main/java/androidx/media3/common/PercentageRating.java +++ b/libraries/common/src/main/java/androidx/media3/common/PercentageRating.java @@ -16,18 +16,13 @@ package androidx.media3.common; import static androidx.media3.common.util.Assertions.checkArgument; -import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; import androidx.annotation.FloatRange; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.base.Objects; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** A rating expressed as a percentage. */ public final class PercentageRating extends Rating { @@ -79,20 +74,14 @@ public boolean equals(@Nullable Object obj) { private static final @RatingType int TYPE = RATING_TYPE_PERCENTAGE; - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_RATING_TYPE, FIELD_PERCENT}) - private @interface FieldNumber {} - - private static final int FIELD_PERCENT = 1; + private static final String FIELD_PERCENT = Util.intToStringMaxRadix(1); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE); - bundle.putFloat(keyForField(FIELD_PERCENT), percent); + bundle.putInt(FIELD_RATING_TYPE, TYPE); + bundle.putFloat(FIELD_PERCENT, percent); return bundle; } @@ -100,14 +89,8 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = PercentageRating::fromBundle; private static PercentageRating fromBundle(Bundle bundle) { - checkArgument( - bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET) - == TYPE); - float percent = bundle.getFloat(keyForField(FIELD_PERCENT), /* defaultValue= */ RATING_UNSET); + checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE); + float percent = bundle.getFloat(FIELD_PERCENT, /* defaultValue= */ RATING_UNSET); return percent == RATING_UNSET ? new PercentageRating() : new PercentageRating(percent); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java index 2bcc6746313..f9aa5978562 100644 --- a/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java +++ b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java @@ -346,13 +346,12 @@ public PlaybackException( @UnstableApi protected PlaybackException(Bundle bundle) { this( - /* message= */ bundle.getString(keyForField(FIELD_STRING_MESSAGE)), + /* message= */ bundle.getString(FIELD_STRING_MESSAGE), /* cause= */ getCauseFromBundle(bundle), /* errorCode= */ bundle.getInt( - keyForField(FIELD_INT_ERROR_CODE), /* defaultValue= */ ERROR_CODE_UNSPECIFIED), + FIELD_INT_ERROR_CODE, /* defaultValue= */ ERROR_CODE_UNSPECIFIED), /* timestampMs= */ bundle.getLong( - keyForField(FIELD_LONG_TIMESTAMP_MS), - /* defaultValue= */ SystemClock.elapsedRealtime())); + FIELD_LONG_TIMESTAMP_MS, /* defaultValue= */ SystemClock.elapsedRealtime())); } /** Creates a new instance using the given values. */ @@ -401,18 +400,18 @@ public boolean errorInfoEquals(@Nullable PlaybackException other) { // Bundleable implementation. - private static final int FIELD_INT_ERROR_CODE = 0; - private static final int FIELD_LONG_TIMESTAMP_MS = 1; - private static final int FIELD_STRING_MESSAGE = 2; - private static final int FIELD_STRING_CAUSE_CLASS_NAME = 3; - private static final int FIELD_STRING_CAUSE_MESSAGE = 4; + private static final String FIELD_INT_ERROR_CODE = Util.intToStringMaxRadix(0); + private static final String FIELD_LONG_TIMESTAMP_MS = Util.intToStringMaxRadix(1); + private static final String FIELD_STRING_MESSAGE = Util.intToStringMaxRadix(2); + private static final String FIELD_STRING_CAUSE_CLASS_NAME = Util.intToStringMaxRadix(3); + private static final String FIELD_STRING_CAUSE_MESSAGE = Util.intToStringMaxRadix(4); /** * Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()} * and {@link Bundleable.Creator}. * *

    Subclasses should obtain their {@link Bundle Bundle's} field keys by applying a non-negative - * offset on this constant and passing the result to {@link #keyForField(int)}. + * offset on this constant and passing the result to {@link Util#intToStringMaxRadix(int)}. */ @UnstableApi protected static final int FIELD_CUSTOM_ID_BASE = 1000; @@ -424,29 +423,17 @@ public boolean errorInfoEquals(@Nullable PlaybackException other) { @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_INT_ERROR_CODE), errorCode); - bundle.putLong(keyForField(FIELD_LONG_TIMESTAMP_MS), timestampMs); - bundle.putString(keyForField(FIELD_STRING_MESSAGE), getMessage()); + bundle.putInt(FIELD_INT_ERROR_CODE, errorCode); + bundle.putLong(FIELD_LONG_TIMESTAMP_MS, timestampMs); + bundle.putString(FIELD_STRING_MESSAGE, getMessage()); @Nullable Throwable cause = getCause(); if (cause != null) { - bundle.putString(keyForField(FIELD_STRING_CAUSE_CLASS_NAME), cause.getClass().getName()); - bundle.putString(keyForField(FIELD_STRING_CAUSE_MESSAGE), cause.getMessage()); + bundle.putString(FIELD_STRING_CAUSE_CLASS_NAME, cause.getClass().getName()); + bundle.putString(FIELD_STRING_CAUSE_MESSAGE, cause.getMessage()); } return bundle; } - /** - * Converts the given field number to a string which can be used as a field key when implementing - * {@link #toBundle()} and {@link Bundleable.Creator}. - * - *

    Subclasses should use {@code field} values greater than or equal to {@link - * #FIELD_CUSTOM_ID_BASE}. - */ - @UnstableApi - protected static String keyForField(int field) { - return Integer.toString(field, Character.MAX_RADIX); - } - // Creates a new {@link Throwable} with possibly {@code null} message. @SuppressWarnings("nullness:argument") private static Throwable createThrowable(Class clazz, @Nullable String message) @@ -462,8 +449,8 @@ private static RemoteException createRemoteException(@Nullable String message) { @Nullable private static Throwable getCauseFromBundle(Bundle bundle) { - @Nullable String causeClassName = bundle.getString(keyForField(FIELD_STRING_CAUSE_CLASS_NAME)); - @Nullable String causeMessage = bundle.getString(keyForField(FIELD_STRING_CAUSE_MESSAGE)); + @Nullable String causeClassName = bundle.getString(FIELD_STRING_CAUSE_CLASS_NAME); + @Nullable String causeMessage = bundle.getString(FIELD_STRING_CAUSE_MESSAGE); @Nullable Throwable cause = null; if (!TextUtils.isEmpty(causeClassName)) { try { diff --git a/libraries/common/src/main/java/androidx/media3/common/PlaybackParameters.java b/libraries/common/src/main/java/androidx/media3/common/PlaybackParameters.java index 84881a55ce6..df63b7d1117 100644 --- a/libraries/common/src/main/java/androidx/media3/common/PlaybackParameters.java +++ b/libraries/common/src/main/java/androidx/media3/common/PlaybackParameters.java @@ -15,20 +15,13 @@ */ package androidx.media3.common; -import static java.lang.annotation.ElementType.TYPE_USE; - import android.os.Bundle; import androidx.annotation.CheckResult; import androidx.annotation.FloatRange; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** Parameters that apply to playback, including speed setting. */ public final class PlaybackParameters implements Bundleable { @@ -122,21 +115,15 @@ public String toString() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_SPEED, FIELD_PITCH}) - private @interface FieldNumber {} - - private static final int FIELD_SPEED = 0; - private static final int FIELD_PITCH = 1; + private static final String FIELD_SPEED = Util.intToStringMaxRadix(0); + private static final String FIELD_PITCH = Util.intToStringMaxRadix(1); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putFloat(keyForField(FIELD_SPEED), speed); - bundle.putFloat(keyForField(FIELD_PITCH), pitch); + bundle.putFloat(FIELD_SPEED, speed); + bundle.putFloat(FIELD_PITCH, pitch); return bundle; } @@ -144,12 +131,8 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = bundle -> { - float speed = bundle.getFloat(keyForField(FIELD_SPEED), /* defaultValue= */ 1f); - float pitch = bundle.getFloat(keyForField(FIELD_PITCH), /* defaultValue= */ 1f); + float speed = bundle.getFloat(FIELD_SPEED, /* defaultValue= */ 1f); + float pitch = bundle.getFloat(FIELD_PITCH, /* defaultValue= */ 1f); return new PlaybackParameters(speed, pitch); }; - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index b3f024192a8..c9e7d4d360e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -268,27 +268,14 @@ public int hashCode() { } // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_MEDIA_ITEM_INDEX, - FIELD_MEDIA_ITEM, - FIELD_PERIOD_INDEX, - FIELD_POSITION_MS, - FIELD_CONTENT_POSITION_MS, - FIELD_AD_GROUP_INDEX, - FIELD_AD_INDEX_IN_AD_GROUP - }) - private @interface FieldNumber {} - - private static final int FIELD_MEDIA_ITEM_INDEX = 0; - private static final int FIELD_MEDIA_ITEM = 1; - private static final int FIELD_PERIOD_INDEX = 2; - private static final int FIELD_POSITION_MS = 3; - private static final int FIELD_CONTENT_POSITION_MS = 4; - private static final int FIELD_AD_GROUP_INDEX = 5; - private static final int FIELD_AD_INDEX_IN_AD_GROUP = 6; + + private static final String FIELD_MEDIA_ITEM_INDEX = Util.intToStringMaxRadix(0); + private static final String FIELD_MEDIA_ITEM = Util.intToStringMaxRadix(1); + private static final String FIELD_PERIOD_INDEX = Util.intToStringMaxRadix(2); + private static final String FIELD_POSITION_MS = Util.intToStringMaxRadix(3); + private static final String FIELD_CONTENT_POSITION_MS = Util.intToStringMaxRadix(4); + private static final String FIELD_AD_GROUP_INDEX = Util.intToStringMaxRadix(5); + private static final String FIELD_AD_INDEX_IN_AD_GROUP = Util.intToStringMaxRadix(6); /** * {@inheritDoc} @@ -300,15 +287,15 @@ public int hashCode() { @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_MEDIA_ITEM_INDEX), mediaItemIndex); + bundle.putInt(FIELD_MEDIA_ITEM_INDEX, mediaItemIndex); if (mediaItem != null) { - bundle.putBundle(keyForField(FIELD_MEDIA_ITEM), mediaItem.toBundle()); + bundle.putBundle(FIELD_MEDIA_ITEM, mediaItem.toBundle()); } - bundle.putInt(keyForField(FIELD_PERIOD_INDEX), periodIndex); - bundle.putLong(keyForField(FIELD_POSITION_MS), positionMs); - bundle.putLong(keyForField(FIELD_CONTENT_POSITION_MS), contentPositionMs); - bundle.putInt(keyForField(FIELD_AD_GROUP_INDEX), adGroupIndex); - bundle.putInt(keyForField(FIELD_AD_INDEX_IN_AD_GROUP), adIndexInAdGroup); + bundle.putInt(FIELD_PERIOD_INDEX, periodIndex); + bundle.putLong(FIELD_POSITION_MS, positionMs); + bundle.putLong(FIELD_CONTENT_POSITION_MS, contentPositionMs); + bundle.putInt(FIELD_AD_GROUP_INDEX, adGroupIndex); + bundle.putInt(FIELD_AD_INDEX_IN_AD_GROUP, adIndexInAdGroup); return bundle; } @@ -316,22 +303,18 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = PositionInfo::fromBundle; private static PositionInfo fromBundle(Bundle bundle) { - int mediaItemIndex = - bundle.getInt(keyForField(FIELD_MEDIA_ITEM_INDEX), /* defaultValue= */ C.INDEX_UNSET); - @Nullable Bundle mediaItemBundle = bundle.getBundle(keyForField(FIELD_MEDIA_ITEM)); + int mediaItemIndex = bundle.getInt(FIELD_MEDIA_ITEM_INDEX, /* defaultValue= */ C.INDEX_UNSET); + @Nullable Bundle mediaItemBundle = bundle.getBundle(FIELD_MEDIA_ITEM); @Nullable MediaItem mediaItem = mediaItemBundle == null ? null : MediaItem.CREATOR.fromBundle(mediaItemBundle); - int periodIndex = - bundle.getInt(keyForField(FIELD_PERIOD_INDEX), /* defaultValue= */ C.INDEX_UNSET); - long positionMs = - bundle.getLong(keyForField(FIELD_POSITION_MS), /* defaultValue= */ C.TIME_UNSET); + int periodIndex = bundle.getInt(FIELD_PERIOD_INDEX, /* defaultValue= */ C.INDEX_UNSET); + long positionMs = bundle.getLong(FIELD_POSITION_MS, /* defaultValue= */ C.TIME_UNSET); long contentPositionMs = - bundle.getLong(keyForField(FIELD_CONTENT_POSITION_MS), /* defaultValue= */ C.TIME_UNSET); - int adGroupIndex = - bundle.getInt(keyForField(FIELD_AD_GROUP_INDEX), /* defaultValue= */ C.INDEX_UNSET); + bundle.getLong(FIELD_CONTENT_POSITION_MS, /* defaultValue= */ C.TIME_UNSET); + int adGroupIndex = bundle.getInt(FIELD_AD_GROUP_INDEX, /* defaultValue= */ C.INDEX_UNSET); int adIndexInAdGroup = - bundle.getInt(keyForField(FIELD_AD_INDEX_IN_AD_GROUP), /* defaultValue= */ C.INDEX_UNSET); + bundle.getInt(FIELD_AD_INDEX_IN_AD_GROUP, /* defaultValue= */ C.INDEX_UNSET); return new PositionInfo( /* windowUid= */ null, mediaItemIndex, @@ -343,10 +326,6 @@ private static PositionInfo fromBundle(Bundle bundle) { adGroupIndex, adIndexInAdGroup); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } /** @@ -581,13 +560,7 @@ public int hashCode() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_COMMANDS}) - private @interface FieldNumber {} - - private static final int FIELD_COMMANDS = 0; + private static final String FIELD_COMMANDS = Util.intToStringMaxRadix(0); @UnstableApi @Override @@ -597,7 +570,7 @@ public Bundle toBundle() { for (int i = 0; i < flags.size(); i++) { commandsBundle.add(flags.get(i)); } - bundle.putIntegerArrayList(keyForField(FIELD_COMMANDS), commandsBundle); + bundle.putIntegerArrayList(FIELD_COMMANDS, commandsBundle); return bundle; } @@ -605,8 +578,7 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = Commands::fromBundle; private static Commands fromBundle(Bundle bundle) { - @Nullable - ArrayList commands = bundle.getIntegerArrayList(keyForField(FIELD_COMMANDS)); + @Nullable ArrayList commands = bundle.getIntegerArrayList(FIELD_COMMANDS); if (commands == null) { return Commands.EMPTY; } @@ -616,10 +588,6 @@ private static Commands fromBundle(Bundle bundle) { } return builder.build(); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } /** diff --git a/libraries/common/src/main/java/androidx/media3/common/Rating.java b/libraries/common/src/main/java/androidx/media3/common/Rating.java index f0df87c434d..0d0cd185335 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Rating.java +++ b/libraries/common/src/main/java/androidx/media3/common/Rating.java @@ -20,6 +20,7 @@ import android.os.Bundle; import androidx.annotation.IntDef; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -60,21 +61,14 @@ public abstract class Rating implements Bundleable { /* package */ static final int RATING_TYPE_STAR = 2; /* package */ static final int RATING_TYPE_THUMB = 3; - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_RATING_TYPE}) - private @interface FieldNumber {} - - /* package */ static final int FIELD_RATING_TYPE = 0; + /* package */ static final String FIELD_RATING_TYPE = Util.intToStringMaxRadix(0); /** Object that can restore a {@link Rating} from a {@link Bundle}. */ @UnstableApi public static final Creator CREATOR = Rating::fromBundle; private static Rating fromBundle(Bundle bundle) { @RatingType - int ratingType = - bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET); + int ratingType = bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET); switch (ratingType) { case RATING_TYPE_HEART: return HeartRating.CREATOR.fromBundle(bundle); @@ -89,8 +83,4 @@ private static Rating fromBundle(Bundle bundle) { throw new IllegalArgumentException("Unknown RatingType: " + ratingType); } } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/StarRating.java b/libraries/common/src/main/java/androidx/media3/common/StarRating.java index 2c38f7cb758..70aba78bfd5 100644 --- a/libraries/common/src/main/java/androidx/media3/common/StarRating.java +++ b/libraries/common/src/main/java/androidx/media3/common/StarRating.java @@ -16,19 +16,14 @@ package androidx.media3.common; import static androidx.media3.common.util.Assertions.checkArgument; -import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; import androidx.annotation.FloatRange; -import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.base.Objects; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** A rating expressed as a fractional number of stars. */ public final class StarRating extends Rating { @@ -106,22 +101,16 @@ public boolean equals(@Nullable Object obj) { private static final @RatingType int TYPE = RATING_TYPE_STAR; private static final int MAX_STARS_DEFAULT = 5; - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_RATING_TYPE, FIELD_MAX_STARS, FIELD_STAR_RATING}) - private @interface FieldNumber {} - - private static final int FIELD_MAX_STARS = 1; - private static final int FIELD_STAR_RATING = 2; + private static final String FIELD_MAX_STARS = Util.intToStringMaxRadix(1); + private static final String FIELD_STAR_RATING = Util.intToStringMaxRadix(2); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE); - bundle.putInt(keyForField(FIELD_MAX_STARS), maxStars); - bundle.putFloat(keyForField(FIELD_STAR_RATING), starRating); + bundle.putInt(FIELD_RATING_TYPE, TYPE); + bundle.putInt(FIELD_MAX_STARS, maxStars); + bundle.putFloat(FIELD_STAR_RATING, starRating); return bundle; } @@ -129,19 +118,11 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = StarRating::fromBundle; private static StarRating fromBundle(Bundle bundle) { - checkArgument( - bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET) - == TYPE); - int maxStars = - bundle.getInt(keyForField(FIELD_MAX_STARS), /* defaultValue= */ MAX_STARS_DEFAULT); - float starRating = - bundle.getFloat(keyForField(FIELD_STAR_RATING), /* defaultValue= */ RATING_UNSET); + checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE); + int maxStars = bundle.getInt(FIELD_MAX_STARS, /* defaultValue= */ MAX_STARS_DEFAULT); + float starRating = bundle.getFloat(FIELD_STAR_RATING, /* defaultValue= */ RATING_UNSET); return starRating == RATING_UNSET ? new StarRating(maxStars) : new StarRating(maxStars, starRating); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/ThumbRating.java b/libraries/common/src/main/java/androidx/media3/common/ThumbRating.java index cd4ad734739..b6e0cb06872 100644 --- a/libraries/common/src/main/java/androidx/media3/common/ThumbRating.java +++ b/libraries/common/src/main/java/androidx/media3/common/ThumbRating.java @@ -16,17 +16,12 @@ package androidx.media3.common; import static androidx.media3.common.util.Assertions.checkArgument; -import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.base.Objects; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** A rating expressed as "thumbs up" or "thumbs down". */ public final class ThumbRating extends Rating { @@ -78,22 +73,16 @@ public boolean equals(@Nullable Object obj) { private static final @RatingType int TYPE = RATING_TYPE_THUMB; - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_RATING_TYPE, FIELD_RATED, FIELD_IS_THUMBS_UP}) - private @interface FieldNumber {} - - private static final int FIELD_RATED = 1; - private static final int FIELD_IS_THUMBS_UP = 2; + private static final String FIELD_RATED = Util.intToStringMaxRadix(1); + private static final String FIELD_IS_THUMBS_UP = Util.intToStringMaxRadix(2); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE); - bundle.putBoolean(keyForField(FIELD_RATED), rated); - bundle.putBoolean(keyForField(FIELD_IS_THUMBS_UP), isThumbsUp); + bundle.putInt(FIELD_RATING_TYPE, TYPE); + bundle.putBoolean(FIELD_RATED, rated); + bundle.putBoolean(FIELD_IS_THUMBS_UP, isThumbsUp); return bundle; } @@ -101,17 +90,10 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = ThumbRating::fromBundle; private static ThumbRating fromBundle(Bundle bundle) { - checkArgument( - bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET) - == TYPE); - boolean rated = bundle.getBoolean(keyForField(FIELD_RATED), /* defaultValue= */ false); + checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE); + boolean rated = bundle.getBoolean(FIELD_RATED, /* defaultValue= */ false); return rated - ? new ThumbRating( - bundle.getBoolean(keyForField(FIELD_IS_THUMBS_UP), /* defaultValue= */ false)) + ? new ThumbRating(bundle.getBoolean(FIELD_IS_THUMBS_UP, /* defaultValue= */ false)) : new ThumbRating(); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java index d95b27f2bce..8e37968a0c3 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java +++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java @@ -20,14 +20,12 @@ import static androidx.media3.common.util.Assertions.checkState; import static java.lang.Math.max; import static java.lang.Math.min; -import static java.lang.annotation.ElementType.TYPE_USE; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.SystemClock; import android.util.Pair; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.BundleUtil; @@ -36,10 +34,6 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.InlineMe; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.ArrayList; import java.util.List; @@ -420,39 +414,20 @@ public int hashCode() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_MEDIA_ITEM, - FIELD_PRESENTATION_START_TIME_MS, - FIELD_WINDOW_START_TIME_MS, - FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS, - FIELD_IS_SEEKABLE, - FIELD_IS_DYNAMIC, - FIELD_LIVE_CONFIGURATION, - FIELD_IS_PLACEHOLDER, - FIELD_DEFAULT_POSITION_US, - FIELD_DURATION_US, - FIELD_FIRST_PERIOD_INDEX, - FIELD_LAST_PERIOD_INDEX, - FIELD_POSITION_IN_FIRST_PERIOD_US, - }) - private @interface FieldNumber {} - - private static final int FIELD_MEDIA_ITEM = 1; - private static final int FIELD_PRESENTATION_START_TIME_MS = 2; - private static final int FIELD_WINDOW_START_TIME_MS = 3; - private static final int FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS = 4; - private static final int FIELD_IS_SEEKABLE = 5; - private static final int FIELD_IS_DYNAMIC = 6; - private static final int FIELD_LIVE_CONFIGURATION = 7; - private static final int FIELD_IS_PLACEHOLDER = 8; - private static final int FIELD_DEFAULT_POSITION_US = 9; - private static final int FIELD_DURATION_US = 10; - private static final int FIELD_FIRST_PERIOD_INDEX = 11; - private static final int FIELD_LAST_PERIOD_INDEX = 12; - private static final int FIELD_POSITION_IN_FIRST_PERIOD_US = 13; + private static final String FIELD_MEDIA_ITEM = Util.intToStringMaxRadix(1); + private static final String FIELD_PRESENTATION_START_TIME_MS = Util.intToStringMaxRadix(2); + private static final String FIELD_WINDOW_START_TIME_MS = Util.intToStringMaxRadix(3); + private static final String FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS = + Util.intToStringMaxRadix(4); + private static final String FIELD_IS_SEEKABLE = Util.intToStringMaxRadix(5); + private static final String FIELD_IS_DYNAMIC = Util.intToStringMaxRadix(6); + private static final String FIELD_LIVE_CONFIGURATION = Util.intToStringMaxRadix(7); + private static final String FIELD_IS_PLACEHOLDER = Util.intToStringMaxRadix(8); + private static final String FIELD_DEFAULT_POSITION_US = Util.intToStringMaxRadix(9); + private static final String FIELD_DURATION_US = Util.intToStringMaxRadix(10); + private static final String FIELD_FIRST_PERIOD_INDEX = Util.intToStringMaxRadix(11); + private static final String FIELD_LAST_PERIOD_INDEX = Util.intToStringMaxRadix(12); + private static final String FIELD_POSITION_IN_FIRST_PERIOD_US = Util.intToStringMaxRadix(13); /** * Returns a {@link Bundle} representing the information stored in this object. @@ -467,46 +442,45 @@ public int hashCode() { public Bundle toBundle(boolean excludeMediaItem) { Bundle bundle = new Bundle(); if (!excludeMediaItem) { - bundle.putBundle(keyForField(FIELD_MEDIA_ITEM), mediaItem.toBundle()); + bundle.putBundle(FIELD_MEDIA_ITEM, mediaItem.toBundle()); } if (presentationStartTimeMs != C.TIME_UNSET) { - bundle.putLong(keyForField(FIELD_PRESENTATION_START_TIME_MS), presentationStartTimeMs); + bundle.putLong(FIELD_PRESENTATION_START_TIME_MS, presentationStartTimeMs); } if (windowStartTimeMs != C.TIME_UNSET) { - bundle.putLong(keyForField(FIELD_WINDOW_START_TIME_MS), windowStartTimeMs); + bundle.putLong(FIELD_WINDOW_START_TIME_MS, windowStartTimeMs); } if (elapsedRealtimeEpochOffsetMs != C.TIME_UNSET) { - bundle.putLong( - keyForField(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS), elapsedRealtimeEpochOffsetMs); + bundle.putLong(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS, elapsedRealtimeEpochOffsetMs); } if (isSeekable) { - bundle.putBoolean(keyForField(FIELD_IS_SEEKABLE), isSeekable); + bundle.putBoolean(FIELD_IS_SEEKABLE, isSeekable); } if (isDynamic) { - bundle.putBoolean(keyForField(FIELD_IS_DYNAMIC), isDynamic); + bundle.putBoolean(FIELD_IS_DYNAMIC, isDynamic); } @Nullable MediaItem.LiveConfiguration liveConfiguration = this.liveConfiguration; if (liveConfiguration != null) { - bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle()); + bundle.putBundle(FIELD_LIVE_CONFIGURATION, liveConfiguration.toBundle()); } if (isPlaceholder) { - bundle.putBoolean(keyForField(FIELD_IS_PLACEHOLDER), isPlaceholder); + bundle.putBoolean(FIELD_IS_PLACEHOLDER, isPlaceholder); } if (defaultPositionUs != 0) { - bundle.putLong(keyForField(FIELD_DEFAULT_POSITION_US), defaultPositionUs); + bundle.putLong(FIELD_DEFAULT_POSITION_US, defaultPositionUs); } if (durationUs != C.TIME_UNSET) { - bundle.putLong(keyForField(FIELD_DURATION_US), durationUs); + bundle.putLong(FIELD_DURATION_US, durationUs); } if (firstPeriodIndex != 0) { - bundle.putInt(keyForField(FIELD_FIRST_PERIOD_INDEX), firstPeriodIndex); + bundle.putInt(FIELD_FIRST_PERIOD_INDEX, firstPeriodIndex); } if (lastPeriodIndex != 0) { - bundle.putInt(keyForField(FIELD_LAST_PERIOD_INDEX), lastPeriodIndex); + bundle.putInt(FIELD_LAST_PERIOD_INDEX, lastPeriodIndex); } if (positionInFirstPeriodUs != 0) { - bundle.putLong(keyForField(FIELD_POSITION_IN_FIRST_PERIOD_US), positionInFirstPeriodUs); + bundle.putLong(FIELD_POSITION_IN_FIRST_PERIOD_US, positionInFirstPeriodUs); } return bundle; } @@ -534,42 +508,31 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = Window::fromBundle; private static Window fromBundle(Bundle bundle) { - @Nullable Bundle mediaItemBundle = bundle.getBundle(keyForField(FIELD_MEDIA_ITEM)); + @Nullable Bundle mediaItemBundle = bundle.getBundle(FIELD_MEDIA_ITEM); @Nullable MediaItem mediaItem = mediaItemBundle != null ? MediaItem.CREATOR.fromBundle(mediaItemBundle) : MediaItem.EMPTY; long presentationStartTimeMs = - bundle.getLong( - keyForField(FIELD_PRESENTATION_START_TIME_MS), /* defaultValue= */ C.TIME_UNSET); + bundle.getLong(FIELD_PRESENTATION_START_TIME_MS, /* defaultValue= */ C.TIME_UNSET); long windowStartTimeMs = - bundle.getLong(keyForField(FIELD_WINDOW_START_TIME_MS), /* defaultValue= */ C.TIME_UNSET); + bundle.getLong(FIELD_WINDOW_START_TIME_MS, /* defaultValue= */ C.TIME_UNSET); long elapsedRealtimeEpochOffsetMs = - bundle.getLong( - keyForField(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS), - /* defaultValue= */ C.TIME_UNSET); - boolean isSeekable = - bundle.getBoolean(keyForField(FIELD_IS_SEEKABLE), /* defaultValue= */ false); - boolean isDynamic = - bundle.getBoolean(keyForField(FIELD_IS_DYNAMIC), /* defaultValue= */ false); - @Nullable - Bundle liveConfigurationBundle = bundle.getBundle(keyForField(FIELD_LIVE_CONFIGURATION)); + bundle.getLong(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS, /* defaultValue= */ C.TIME_UNSET); + boolean isSeekable = bundle.getBoolean(FIELD_IS_SEEKABLE, /* defaultValue= */ false); + boolean isDynamic = bundle.getBoolean(FIELD_IS_DYNAMIC, /* defaultValue= */ false); + @Nullable Bundle liveConfigurationBundle = bundle.getBundle(FIELD_LIVE_CONFIGURATION); @Nullable MediaItem.LiveConfiguration liveConfiguration = liveConfigurationBundle != null ? MediaItem.LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle) : null; - boolean isPlaceHolder = - bundle.getBoolean(keyForField(FIELD_IS_PLACEHOLDER), /* defaultValue= */ false); - long defaultPositionUs = - bundle.getLong(keyForField(FIELD_DEFAULT_POSITION_US), /* defaultValue= */ 0); - long durationUs = - bundle.getLong(keyForField(FIELD_DURATION_US), /* defaultValue= */ C.TIME_UNSET); - int firstPeriodIndex = - bundle.getInt(keyForField(FIELD_FIRST_PERIOD_INDEX), /* defaultValue= */ 0); - int lastPeriodIndex = - bundle.getInt(keyForField(FIELD_LAST_PERIOD_INDEX), /* defaultValue= */ 0); + boolean isPlaceHolder = bundle.getBoolean(FIELD_IS_PLACEHOLDER, /* defaultValue= */ false); + long defaultPositionUs = bundle.getLong(FIELD_DEFAULT_POSITION_US, /* defaultValue= */ 0); + long durationUs = bundle.getLong(FIELD_DURATION_US, /* defaultValue= */ C.TIME_UNSET); + int firstPeriodIndex = bundle.getInt(FIELD_FIRST_PERIOD_INDEX, /* defaultValue= */ 0); + int lastPeriodIndex = bundle.getInt(FIELD_LAST_PERIOD_INDEX, /* defaultValue= */ 0); long positionInFirstPeriodUs = - bundle.getLong(keyForField(FIELD_POSITION_IN_FIRST_PERIOD_US), /* defaultValue= */ 0); + bundle.getLong(FIELD_POSITION_IN_FIRST_PERIOD_US, /* defaultValue= */ 0); Window window = new Window(); window.set( @@ -590,10 +553,6 @@ private static Window fromBundle(Bundle bundle) { window.isPlaceholder = isPlaceHolder; return window; } - - private static String keyForField(@Window.FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } /** @@ -945,23 +904,11 @@ public int hashCode() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_WINDOW_INDEX, - FIELD_DURATION_US, - FIELD_POSITION_IN_WINDOW_US, - FIELD_PLACEHOLDER, - FIELD_AD_PLAYBACK_STATE - }) - private @interface FieldNumber {} - - private static final int FIELD_WINDOW_INDEX = 0; - private static final int FIELD_DURATION_US = 1; - private static final int FIELD_POSITION_IN_WINDOW_US = 2; - private static final int FIELD_PLACEHOLDER = 3; - private static final int FIELD_AD_PLAYBACK_STATE = 4; + private static final String FIELD_WINDOW_INDEX = Util.intToStringMaxRadix(0); + private static final String FIELD_DURATION_US = Util.intToStringMaxRadix(1); + private static final String FIELD_POSITION_IN_WINDOW_US = Util.intToStringMaxRadix(2); + private static final String FIELD_PLACEHOLDER = Util.intToStringMaxRadix(3); + private static final String FIELD_AD_PLAYBACK_STATE = Util.intToStringMaxRadix(4); /** * {@inheritDoc} @@ -974,19 +921,19 @@ public int hashCode() { public Bundle toBundle() { Bundle bundle = new Bundle(); if (windowIndex != 0) { - bundle.putInt(keyForField(FIELD_WINDOW_INDEX), windowIndex); + bundle.putInt(FIELD_WINDOW_INDEX, windowIndex); } if (durationUs != C.TIME_UNSET) { - bundle.putLong(keyForField(FIELD_DURATION_US), durationUs); + bundle.putLong(FIELD_DURATION_US, durationUs); } if (positionInWindowUs != 0) { - bundle.putLong(keyForField(FIELD_POSITION_IN_WINDOW_US), positionInWindowUs); + bundle.putLong(FIELD_POSITION_IN_WINDOW_US, positionInWindowUs); } if (isPlaceholder) { - bundle.putBoolean(keyForField(FIELD_PLACEHOLDER), isPlaceholder); + bundle.putBoolean(FIELD_PLACEHOLDER, isPlaceholder); } if (!adPlaybackState.equals(AdPlaybackState.NONE)) { - bundle.putBundle(keyForField(FIELD_AD_PLAYBACK_STATE), adPlaybackState.toBundle()); + bundle.putBundle(FIELD_AD_PLAYBACK_STATE, adPlaybackState.toBundle()); } return bundle; } @@ -999,15 +946,11 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = Period::fromBundle; private static Period fromBundle(Bundle bundle) { - int windowIndex = bundle.getInt(keyForField(FIELD_WINDOW_INDEX), /* defaultValue= */ 0); - long durationUs = - bundle.getLong(keyForField(FIELD_DURATION_US), /* defaultValue= */ C.TIME_UNSET); - long positionInWindowUs = - bundle.getLong(keyForField(FIELD_POSITION_IN_WINDOW_US), /* defaultValue= */ 0); - boolean isPlaceholder = - bundle.getBoolean(keyForField(FIELD_PLACEHOLDER), /* defaultValue= */ false); - @Nullable - Bundle adPlaybackStateBundle = bundle.getBundle(keyForField(FIELD_AD_PLAYBACK_STATE)); + int windowIndex = bundle.getInt(FIELD_WINDOW_INDEX, /* defaultValue= */ 0); + long durationUs = bundle.getLong(FIELD_DURATION_US, /* defaultValue= */ C.TIME_UNSET); + long positionInWindowUs = bundle.getLong(FIELD_POSITION_IN_WINDOW_US, /* defaultValue= */ 0); + boolean isPlaceholder = bundle.getBoolean(FIELD_PLACEHOLDER, /* defaultValue= */ false); + @Nullable Bundle adPlaybackStateBundle = bundle.getBundle(FIELD_AD_PLAYBACK_STATE); AdPlaybackState adPlaybackState = adPlaybackStateBundle != null ? AdPlaybackState.CREATOR.fromBundle(adPlaybackStateBundle) @@ -1024,10 +967,6 @@ private static Period fromBundle(Bundle bundle) { isPlaceholder); return period; } - - private static String keyForField(@Period.FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } /** An empty timeline. */ @@ -1447,19 +1386,9 @@ public int hashCode() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_WINDOWS, - FIELD_PERIODS, - FIELD_SHUFFLED_WINDOW_INDICES, - }) - private @interface FieldNumber {} - - private static final int FIELD_WINDOWS = 0; - private static final int FIELD_PERIODS = 1; - private static final int FIELD_SHUFFLED_WINDOW_INDICES = 2; + private static final String FIELD_WINDOWS = Util.intToStringMaxRadix(0); + private static final String FIELD_PERIODS = Util.intToStringMaxRadix(1); + private static final String FIELD_SHUFFLED_WINDOW_INDICES = Util.intToStringMaxRadix(2); /** * {@inheritDoc} @@ -1499,11 +1428,9 @@ public final Bundle toBundle(boolean excludeMediaItems) { } Bundle bundle = new Bundle(); - BundleUtil.putBinder( - bundle, keyForField(FIELD_WINDOWS), new BundleListRetriever(windowBundles)); - BundleUtil.putBinder( - bundle, keyForField(FIELD_PERIODS), new BundleListRetriever(periodBundles)); - bundle.putIntArray(keyForField(FIELD_SHUFFLED_WINDOW_INDICES), shuffledWindowIndices); + BundleUtil.putBinder(bundle, FIELD_WINDOWS, new BundleListRetriever(windowBundles)); + BundleUtil.putBinder(bundle, FIELD_PERIODS, new BundleListRetriever(periodBundles)); + bundle.putIntArray(FIELD_SHUFFLED_WINDOW_INDICES, shuffledWindowIndices); return bundle; } @@ -1531,13 +1458,10 @@ public final Bundle toBundle() { private static Timeline fromBundle(Bundle bundle) { ImmutableList windows = - fromBundleListRetriever( - Window.CREATOR, BundleUtil.getBinder(bundle, keyForField(FIELD_WINDOWS))); + fromBundleListRetriever(Window.CREATOR, BundleUtil.getBinder(bundle, FIELD_WINDOWS)); ImmutableList periods = - fromBundleListRetriever( - Period.CREATOR, BundleUtil.getBinder(bundle, keyForField(FIELD_PERIODS))); - @Nullable - int[] shuffledWindowIndices = bundle.getIntArray(keyForField(FIELD_SHUFFLED_WINDOW_INDICES)); + fromBundleListRetriever(Period.CREATOR, BundleUtil.getBinder(bundle, FIELD_PERIODS)); + @Nullable int[] shuffledWindowIndices = bundle.getIntArray(FIELD_SHUFFLED_WINDOW_INDICES); return new RemotableTimeline( windows, periods, @@ -1559,10 +1483,6 @@ private static ImmutableList fromBundleListRetriever( return builder.build(); } - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } - private static int[] generateUnshuffledIndices(int n) { int[] indices = new int[n]; for (int i = 0; i < n; i++) { diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java b/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java index ce934111d56..f13d3dca4a7 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java @@ -16,20 +16,15 @@ package androidx.media3.common; import static androidx.media3.common.util.Assertions.checkArgument; -import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; import androidx.annotation.CheckResult; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -165,15 +160,8 @@ public boolean equals(@Nullable Object obj) { } // Bundleable implementation. - - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_FORMATS, FIELD_ID}) - private @interface FieldNumber {} - - private static final int FIELD_FORMATS = 0; - private static final int FIELD_ID = 1; + private static final String FIELD_FORMATS = Util.intToStringMaxRadix(0); + private static final String FIELD_ID = Util.intToStringMaxRadix(1); @UnstableApi @Override @@ -183,8 +171,8 @@ public Bundle toBundle() { for (Format format : formats) { arrayList.add(format.toBundle(/* excludeMetadata= */ true)); } - bundle.putParcelableArrayList(keyForField(FIELD_FORMATS), arrayList); - bundle.putString(keyForField(FIELD_ID), id); + bundle.putParcelableArrayList(FIELD_FORMATS, arrayList); + bundle.putString(FIELD_ID, id); return bundle; } @@ -192,20 +180,15 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = bundle -> { - @Nullable - List formatBundles = bundle.getParcelableArrayList(keyForField(FIELD_FORMATS)); + @Nullable List formatBundles = bundle.getParcelableArrayList(FIELD_FORMATS); List formats = formatBundles == null ? ImmutableList.of() : BundleableUtil.fromBundleList(Format.CREATOR, formatBundles); - String id = bundle.getString(keyForField(FIELD_ID), /* defaultValue= */ ""); + String id = bundle.getString(FIELD_ID, /* defaultValue= */ ""); return new TrackGroup(id, formats.toArray(new Format[0])); }; - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } - private void verifyCorrectness() { // TrackGroups should only contain tracks with exactly the same content (but in different // qualities). We only log an error instead of throwing to not break backwards-compatibility for diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionOverride.java b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionOverride.java index a673e95bd80..c40e88b654e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionOverride.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionOverride.java @@ -20,14 +20,11 @@ import static java.util.Collections.min; import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Ints; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.List; /** @@ -54,16 +51,8 @@ public final class TrackSelectionOverride implements Bundleable { /** The indices of tracks in a {@link TrackGroup} to be selected. */ public final ImmutableList trackIndices; - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - FIELD_TRACK_GROUP, - FIELD_TRACKS, - }) - private @interface FieldNumber {} - - private static final int FIELD_TRACK_GROUP = 0; - private static final int FIELD_TRACKS = 1; + private static final String FIELD_TRACK_GROUP = Util.intToStringMaxRadix(0); + private static final String FIELD_TRACKS = Util.intToStringMaxRadix(1); /** * Constructs an instance to force {@code trackIndex} in {@code trackGroup} to be selected. @@ -119,8 +108,8 @@ public int hashCode() { @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putBundle(keyForField(FIELD_TRACK_GROUP), mediaTrackGroup.toBundle()); - bundle.putIntArray(keyForField(FIELD_TRACKS), Ints.toArray(trackIndices)); + bundle.putBundle(FIELD_TRACK_GROUP, mediaTrackGroup.toBundle()); + bundle.putIntArray(FIELD_TRACKS, Ints.toArray(trackIndices)); return bundle; } @@ -128,13 +117,9 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = bundle -> { - Bundle trackGroupBundle = checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP))); + Bundle trackGroupBundle = checkNotNull(bundle.getBundle(FIELD_TRACK_GROUP)); TrackGroup mediaTrackGroup = TrackGroup.CREATOR.fromBundle(trackGroupBundle); - int[] tracks = checkNotNull(bundle.getIntArray(keyForField(FIELD_TRACKS))); + int[] tracks = checkNotNull(bundle.getIntArray(FIELD_TRACKS)); return new TrackSelectionOverride(mediaTrackGroup, Ints.asList(tracks)); }; - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java index 1c2f7a633a7..b65bc9400a1 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java @@ -158,95 +158,71 @@ protected Builder(TrackSelectionParameters initialValues) { @UnstableApi protected Builder(Bundle bundle) { // Video - maxVideoWidth = - bundle.getInt(keyForField(FIELD_MAX_VIDEO_WIDTH), DEFAULT_WITHOUT_CONTEXT.maxVideoWidth); + maxVideoWidth = bundle.getInt(FIELD_MAX_VIDEO_WIDTH, DEFAULT_WITHOUT_CONTEXT.maxVideoWidth); maxVideoHeight = - bundle.getInt( - keyForField(FIELD_MAX_VIDEO_HEIGHT), DEFAULT_WITHOUT_CONTEXT.maxVideoHeight); + bundle.getInt(FIELD_MAX_VIDEO_HEIGHT, DEFAULT_WITHOUT_CONTEXT.maxVideoHeight); maxVideoFrameRate = - bundle.getInt( - keyForField(FIELD_MAX_VIDEO_FRAMERATE), DEFAULT_WITHOUT_CONTEXT.maxVideoFrameRate); + bundle.getInt(FIELD_MAX_VIDEO_FRAMERATE, DEFAULT_WITHOUT_CONTEXT.maxVideoFrameRate); maxVideoBitrate = - bundle.getInt( - keyForField(FIELD_MAX_VIDEO_BITRATE), DEFAULT_WITHOUT_CONTEXT.maxVideoBitrate); - minVideoWidth = - bundle.getInt(keyForField(FIELD_MIN_VIDEO_WIDTH), DEFAULT_WITHOUT_CONTEXT.minVideoWidth); + bundle.getInt(FIELD_MAX_VIDEO_BITRATE, DEFAULT_WITHOUT_CONTEXT.maxVideoBitrate); + minVideoWidth = bundle.getInt(FIELD_MIN_VIDEO_WIDTH, DEFAULT_WITHOUT_CONTEXT.minVideoWidth); minVideoHeight = - bundle.getInt( - keyForField(FIELD_MIN_VIDEO_HEIGHT), DEFAULT_WITHOUT_CONTEXT.minVideoHeight); + bundle.getInt(FIELD_MIN_VIDEO_HEIGHT, DEFAULT_WITHOUT_CONTEXT.minVideoHeight); minVideoFrameRate = - bundle.getInt( - keyForField(FIELD_MIN_VIDEO_FRAMERATE), DEFAULT_WITHOUT_CONTEXT.minVideoFrameRate); + bundle.getInt(FIELD_MIN_VIDEO_FRAMERATE, DEFAULT_WITHOUT_CONTEXT.minVideoFrameRate); minVideoBitrate = - bundle.getInt( - keyForField(FIELD_MIN_VIDEO_BITRATE), DEFAULT_WITHOUT_CONTEXT.minVideoBitrate); - viewportWidth = - bundle.getInt(keyForField(FIELD_VIEWPORT_WIDTH), DEFAULT_WITHOUT_CONTEXT.viewportWidth); - viewportHeight = - bundle.getInt(keyForField(FIELD_VIEWPORT_HEIGHT), DEFAULT_WITHOUT_CONTEXT.viewportHeight); + bundle.getInt(FIELD_MIN_VIDEO_BITRATE, DEFAULT_WITHOUT_CONTEXT.minVideoBitrate); + viewportWidth = bundle.getInt(FIELD_VIEWPORT_WIDTH, DEFAULT_WITHOUT_CONTEXT.viewportWidth); + viewportHeight = bundle.getInt(FIELD_VIEWPORT_HEIGHT, DEFAULT_WITHOUT_CONTEXT.viewportHeight); viewportOrientationMayChange = bundle.getBoolean( - keyForField(FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE), + FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE, DEFAULT_WITHOUT_CONTEXT.viewportOrientationMayChange); preferredVideoMimeTypes = ImmutableList.copyOf( - firstNonNull( - bundle.getStringArray(keyForField(FIELD_PREFERRED_VIDEO_MIMETYPES)), - new String[0])); + firstNonNull(bundle.getStringArray(FIELD_PREFERRED_VIDEO_MIMETYPES), new String[0])); preferredVideoRoleFlags = bundle.getInt( - keyForField(FIELD_PREFERRED_VIDEO_ROLE_FLAGS), - DEFAULT_WITHOUT_CONTEXT.preferredVideoRoleFlags); + FIELD_PREFERRED_VIDEO_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredVideoRoleFlags); // Audio String[] preferredAudioLanguages1 = - firstNonNull( - bundle.getStringArray(keyForField(FIELD_PREFERRED_AUDIO_LANGUAGES)), new String[0]); + firstNonNull(bundle.getStringArray(FIELD_PREFERRED_AUDIO_LANGUAGES), new String[0]); preferredAudioLanguages = normalizeLanguageCodes(preferredAudioLanguages1); preferredAudioRoleFlags = bundle.getInt( - keyForField(FIELD_PREFERRED_AUDIO_ROLE_FLAGS), - DEFAULT_WITHOUT_CONTEXT.preferredAudioRoleFlags); + FIELD_PREFERRED_AUDIO_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredAudioRoleFlags); maxAudioChannelCount = bundle.getInt( - keyForField(FIELD_MAX_AUDIO_CHANNEL_COUNT), - DEFAULT_WITHOUT_CONTEXT.maxAudioChannelCount); + FIELD_MAX_AUDIO_CHANNEL_COUNT, DEFAULT_WITHOUT_CONTEXT.maxAudioChannelCount); maxAudioBitrate = - bundle.getInt( - keyForField(FIELD_MAX_AUDIO_BITRATE), DEFAULT_WITHOUT_CONTEXT.maxAudioBitrate); + bundle.getInt(FIELD_MAX_AUDIO_BITRATE, DEFAULT_WITHOUT_CONTEXT.maxAudioBitrate); preferredAudioMimeTypes = ImmutableList.copyOf( - firstNonNull( - bundle.getStringArray(keyForField(FIELD_PREFERRED_AUDIO_MIME_TYPES)), - new String[0])); + firstNonNull(bundle.getStringArray(FIELD_PREFERRED_AUDIO_MIME_TYPES), new String[0])); // Text preferredTextLanguages = normalizeLanguageCodes( - firstNonNull( - bundle.getStringArray(keyForField(FIELD_PREFERRED_TEXT_LANGUAGES)), - new String[0])); + firstNonNull(bundle.getStringArray(FIELD_PREFERRED_TEXT_LANGUAGES), new String[0])); preferredTextRoleFlags = bundle.getInt( - keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS), - DEFAULT_WITHOUT_CONTEXT.preferredTextRoleFlags); + FIELD_PREFERRED_TEXT_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredTextRoleFlags); ignoredTextSelectionFlags = bundle.getInt( - keyForField(FIELD_IGNORED_TEXT_SELECTION_FLAGS), + FIELD_IGNORED_TEXT_SELECTION_FLAGS, DEFAULT_WITHOUT_CONTEXT.ignoredTextSelectionFlags); selectUndeterminedTextLanguage = bundle.getBoolean( - keyForField(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE), + FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE, DEFAULT_WITHOUT_CONTEXT.selectUndeterminedTextLanguage); // General forceLowestBitrate = - bundle.getBoolean( - keyForField(FIELD_FORCE_LOWEST_BITRATE), DEFAULT_WITHOUT_CONTEXT.forceLowestBitrate); + bundle.getBoolean(FIELD_FORCE_LOWEST_BITRATE, DEFAULT_WITHOUT_CONTEXT.forceLowestBitrate); forceHighestSupportedBitrate = bundle.getBoolean( - keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE), + FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE, DEFAULT_WITHOUT_CONTEXT.forceHighestSupportedBitrate); @Nullable - List overrideBundleList = - bundle.getParcelableArrayList(keyForField(FIELD_SELECTION_OVERRIDES)); + List overrideBundleList = bundle.getParcelableArrayList(FIELD_SELECTION_OVERRIDES); List overrideList = overrideBundleList == null ? ImmutableList.of() @@ -257,7 +233,7 @@ protected Builder(Bundle bundle) { overrides.put(override.mediaTrackGroup, override); } int[] disabledTrackTypeArray = - firstNonNull(bundle.getIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE)), new int[0]); + firstNonNull(bundle.getIntArray(FIELD_DISABLED_TRACK_TYPE), new int[0]); disabledTrackTypes = new HashSet<>(); for (@C.TrackType int disabledTrackType : disabledTrackTypeArray) { disabledTrackTypes.add(disabledTrackType); @@ -1103,39 +1079,40 @@ public int hashCode() { // Bundleable implementation - private static final int FIELD_PREFERRED_AUDIO_LANGUAGES = 1; - private static final int FIELD_PREFERRED_AUDIO_ROLE_FLAGS = 2; - private static final int FIELD_PREFERRED_TEXT_LANGUAGES = 3; - private static final int FIELD_PREFERRED_TEXT_ROLE_FLAGS = 4; - private static final int FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE = 5; - private static final int FIELD_MAX_VIDEO_WIDTH = 6; - private static final int FIELD_MAX_VIDEO_HEIGHT = 7; - private static final int FIELD_MAX_VIDEO_FRAMERATE = 8; - private static final int FIELD_MAX_VIDEO_BITRATE = 9; - private static final int FIELD_MIN_VIDEO_WIDTH = 10; - private static final int FIELD_MIN_VIDEO_HEIGHT = 11; - private static final int FIELD_MIN_VIDEO_FRAMERATE = 12; - private static final int FIELD_MIN_VIDEO_BITRATE = 13; - private static final int FIELD_VIEWPORT_WIDTH = 14; - private static final int FIELD_VIEWPORT_HEIGHT = 15; - private static final int FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE = 16; - private static final int FIELD_PREFERRED_VIDEO_MIMETYPES = 17; - private static final int FIELD_MAX_AUDIO_CHANNEL_COUNT = 18; - private static final int FIELD_MAX_AUDIO_BITRATE = 19; - private static final int FIELD_PREFERRED_AUDIO_MIME_TYPES = 20; - private static final int FIELD_FORCE_LOWEST_BITRATE = 21; - private static final int FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE = 22; - private static final int FIELD_SELECTION_OVERRIDES = 23; - private static final int FIELD_DISABLED_TRACK_TYPE = 24; - private static final int FIELD_PREFERRED_VIDEO_ROLE_FLAGS = 25; - private static final int FIELD_IGNORED_TEXT_SELECTION_FLAGS = 26; + private static final String FIELD_PREFERRED_AUDIO_LANGUAGES = Util.intToStringMaxRadix(1); + private static final String FIELD_PREFERRED_AUDIO_ROLE_FLAGS = Util.intToStringMaxRadix(2); + private static final String FIELD_PREFERRED_TEXT_LANGUAGES = Util.intToStringMaxRadix(3); + private static final String FIELD_PREFERRED_TEXT_ROLE_FLAGS = Util.intToStringMaxRadix(4); + private static final String FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE = Util.intToStringMaxRadix(5); + private static final String FIELD_MAX_VIDEO_WIDTH = Util.intToStringMaxRadix(6); + private static final String FIELD_MAX_VIDEO_HEIGHT = Util.intToStringMaxRadix(7); + private static final String FIELD_MAX_VIDEO_FRAMERATE = Util.intToStringMaxRadix(8); + private static final String FIELD_MAX_VIDEO_BITRATE = Util.intToStringMaxRadix(9); + private static final String FIELD_MIN_VIDEO_WIDTH = Util.intToStringMaxRadix(10); + private static final String FIELD_MIN_VIDEO_HEIGHT = Util.intToStringMaxRadix(11); + private static final String FIELD_MIN_VIDEO_FRAMERATE = Util.intToStringMaxRadix(12); + private static final String FIELD_MIN_VIDEO_BITRATE = Util.intToStringMaxRadix(13); + private static final String FIELD_VIEWPORT_WIDTH = Util.intToStringMaxRadix(14); + private static final String FIELD_VIEWPORT_HEIGHT = Util.intToStringMaxRadix(15); + private static final String FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE = Util.intToStringMaxRadix(16); + private static final String FIELD_PREFERRED_VIDEO_MIMETYPES = Util.intToStringMaxRadix(17); + private static final String FIELD_MAX_AUDIO_CHANNEL_COUNT = Util.intToStringMaxRadix(18); + private static final String FIELD_MAX_AUDIO_BITRATE = Util.intToStringMaxRadix(19); + private static final String FIELD_PREFERRED_AUDIO_MIME_TYPES = Util.intToStringMaxRadix(20); + private static final String FIELD_FORCE_LOWEST_BITRATE = Util.intToStringMaxRadix(21); + private static final String FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE = Util.intToStringMaxRadix(22); + private static final String FIELD_SELECTION_OVERRIDES = Util.intToStringMaxRadix(23); + private static final String FIELD_DISABLED_TRACK_TYPE = Util.intToStringMaxRadix(24); + private static final String FIELD_PREFERRED_VIDEO_ROLE_FLAGS = Util.intToStringMaxRadix(25); + private static final String FIELD_IGNORED_TEXT_SELECTION_FLAGS = Util.intToStringMaxRadix(26); /** * Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()} * and {@link Bundleable.Creator}. * *

    Subclasses should obtain keys for their {@link Bundle} representation by applying a - * non-negative offset on this constant and passing the result to {@link #keyForField(int)}. + * non-negative offset on this constant and passing the result to {@link + * Util#intToStringMaxRadix(int)}. */ @UnstableApi protected static final int FIELD_CUSTOM_ID_BASE = 1000; @@ -1144,46 +1121,39 @@ public Bundle toBundle() { Bundle bundle = new Bundle(); // Video - bundle.putInt(keyForField(FIELD_MAX_VIDEO_WIDTH), maxVideoWidth); - bundle.putInt(keyForField(FIELD_MAX_VIDEO_HEIGHT), maxVideoHeight); - bundle.putInt(keyForField(FIELD_MAX_VIDEO_FRAMERATE), maxVideoFrameRate); - bundle.putInt(keyForField(FIELD_MAX_VIDEO_BITRATE), maxVideoBitrate); - bundle.putInt(keyForField(FIELD_MIN_VIDEO_WIDTH), minVideoWidth); - bundle.putInt(keyForField(FIELD_MIN_VIDEO_HEIGHT), minVideoHeight); - bundle.putInt(keyForField(FIELD_MIN_VIDEO_FRAMERATE), minVideoFrameRate); - bundle.putInt(keyForField(FIELD_MIN_VIDEO_BITRATE), minVideoBitrate); - bundle.putInt(keyForField(FIELD_VIEWPORT_WIDTH), viewportWidth); - bundle.putInt(keyForField(FIELD_VIEWPORT_HEIGHT), viewportHeight); - bundle.putBoolean( - keyForField(FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE), viewportOrientationMayChange); + bundle.putInt(FIELD_MAX_VIDEO_WIDTH, maxVideoWidth); + bundle.putInt(FIELD_MAX_VIDEO_HEIGHT, maxVideoHeight); + bundle.putInt(FIELD_MAX_VIDEO_FRAMERATE, maxVideoFrameRate); + bundle.putInt(FIELD_MAX_VIDEO_BITRATE, maxVideoBitrate); + bundle.putInt(FIELD_MIN_VIDEO_WIDTH, minVideoWidth); + bundle.putInt(FIELD_MIN_VIDEO_HEIGHT, minVideoHeight); + bundle.putInt(FIELD_MIN_VIDEO_FRAMERATE, minVideoFrameRate); + bundle.putInt(FIELD_MIN_VIDEO_BITRATE, minVideoBitrate); + bundle.putInt(FIELD_VIEWPORT_WIDTH, viewportWidth); + bundle.putInt(FIELD_VIEWPORT_HEIGHT, viewportHeight); + bundle.putBoolean(FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE, viewportOrientationMayChange); bundle.putStringArray( - keyForField(FIELD_PREFERRED_VIDEO_MIMETYPES), - preferredVideoMimeTypes.toArray(new String[0])); - bundle.putInt(keyForField(FIELD_PREFERRED_VIDEO_ROLE_FLAGS), preferredVideoRoleFlags); + FIELD_PREFERRED_VIDEO_MIMETYPES, preferredVideoMimeTypes.toArray(new String[0])); + bundle.putInt(FIELD_PREFERRED_VIDEO_ROLE_FLAGS, preferredVideoRoleFlags); // Audio bundle.putStringArray( - keyForField(FIELD_PREFERRED_AUDIO_LANGUAGES), - preferredAudioLanguages.toArray(new String[0])); - bundle.putInt(keyForField(FIELD_PREFERRED_AUDIO_ROLE_FLAGS), preferredAudioRoleFlags); - bundle.putInt(keyForField(FIELD_MAX_AUDIO_CHANNEL_COUNT), maxAudioChannelCount); - bundle.putInt(keyForField(FIELD_MAX_AUDIO_BITRATE), maxAudioBitrate); + FIELD_PREFERRED_AUDIO_LANGUAGES, preferredAudioLanguages.toArray(new String[0])); + bundle.putInt(FIELD_PREFERRED_AUDIO_ROLE_FLAGS, preferredAudioRoleFlags); + bundle.putInt(FIELD_MAX_AUDIO_CHANNEL_COUNT, maxAudioChannelCount); + bundle.putInt(FIELD_MAX_AUDIO_BITRATE, maxAudioBitrate); bundle.putStringArray( - keyForField(FIELD_PREFERRED_AUDIO_MIME_TYPES), - preferredAudioMimeTypes.toArray(new String[0])); + FIELD_PREFERRED_AUDIO_MIME_TYPES, preferredAudioMimeTypes.toArray(new String[0])); // Text bundle.putStringArray( - keyForField(FIELD_PREFERRED_TEXT_LANGUAGES), preferredTextLanguages.toArray(new String[0])); - bundle.putInt(keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS), preferredTextRoleFlags); - bundle.putInt(keyForField(FIELD_IGNORED_TEXT_SELECTION_FLAGS), ignoredTextSelectionFlags); - bundle.putBoolean( - keyForField(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE), selectUndeterminedTextLanguage); + FIELD_PREFERRED_TEXT_LANGUAGES, preferredTextLanguages.toArray(new String[0])); + bundle.putInt(FIELD_PREFERRED_TEXT_ROLE_FLAGS, preferredTextRoleFlags); + bundle.putInt(FIELD_IGNORED_TEXT_SELECTION_FLAGS, ignoredTextSelectionFlags); + bundle.putBoolean(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE, selectUndeterminedTextLanguage); // General - bundle.putBoolean(keyForField(FIELD_FORCE_LOWEST_BITRATE), forceLowestBitrate); - bundle.putBoolean( - keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE), forceHighestSupportedBitrate); - bundle.putParcelableArrayList( - keyForField(FIELD_SELECTION_OVERRIDES), toBundleArrayList(overrides.values())); - bundle.putIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE), Ints.toArray(disabledTrackTypes)); + bundle.putBoolean(FIELD_FORCE_LOWEST_BITRATE, forceLowestBitrate); + bundle.putBoolean(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE, forceHighestSupportedBitrate); + bundle.putParcelableArrayList(FIELD_SELECTION_OVERRIDES, toBundleArrayList(overrides.values())); + bundle.putIntArray(FIELD_DISABLED_TRACK_TYPE, Ints.toArray(disabledTrackTypes)); return bundle; } @@ -1199,16 +1169,4 @@ public static TrackSelectionParameters fromBundle(Bundle bundle) { @UnstableApi @Deprecated public static final Creator CREATOR = TrackSelectionParameters::fromBundle; - - /** - * Converts the given field number to a string which can be used as a field key when implementing - * {@link #toBundle()} and {@link Bundleable.Creator}. - * - *

    Subclasses should use {@code field} values greater than or equal to {@link - * #FIELD_CUSTOM_ID_BASE}. - */ - @UnstableApi - protected static String keyForField(int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/Tracks.java b/libraries/common/src/main/java/androidx/media3/common/Tracks.java index 6da0a9204c3..28d2e89a974 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Tracks.java +++ b/libraries/common/src/main/java/androidx/media3/common/Tracks.java @@ -18,20 +18,15 @@ import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.BundleableUtil.toBundleArrayList; -import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Booleans; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.Arrays; import java.util.List; @@ -221,29 +216,19 @@ public int hashCode() { } // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_TRACK_GROUP, - FIELD_TRACK_SUPPORT, - FIELD_TRACK_SELECTED, - FIELD_ADAPTIVE_SUPPORTED, - }) - private @interface FieldNumber {} - - private static final int FIELD_TRACK_GROUP = 0; - private static final int FIELD_TRACK_SUPPORT = 1; - private static final int FIELD_TRACK_SELECTED = 3; - private static final int FIELD_ADAPTIVE_SUPPORTED = 4; + + private static final String FIELD_TRACK_GROUP = Util.intToStringMaxRadix(0); + private static final String FIELD_TRACK_SUPPORT = Util.intToStringMaxRadix(1); + private static final String FIELD_TRACK_SELECTED = Util.intToStringMaxRadix(3); + private static final String FIELD_ADAPTIVE_SUPPORTED = Util.intToStringMaxRadix(4); @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putBundle(keyForField(FIELD_TRACK_GROUP), mediaTrackGroup.toBundle()); - bundle.putIntArray(keyForField(FIELD_TRACK_SUPPORT), trackSupport); - bundle.putBooleanArray(keyForField(FIELD_TRACK_SELECTED), trackSelected); - bundle.putBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), adaptiveSupported); + bundle.putBundle(FIELD_TRACK_GROUP, mediaTrackGroup.toBundle()); + bundle.putIntArray(FIELD_TRACK_SUPPORT, trackSupport); + bundle.putBooleanArray(FIELD_TRACK_SELECTED, trackSelected); + bundle.putBoolean(FIELD_ADAPTIVE_SUPPORTED, adaptiveSupported); return bundle; } @@ -253,23 +238,16 @@ public Bundle toBundle() { bundle -> { // Can't create a Tracks.Group without a TrackGroup TrackGroup trackGroup = - TrackGroup.CREATOR.fromBundle( - checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP)))); + TrackGroup.CREATOR.fromBundle(checkNotNull(bundle.getBundle(FIELD_TRACK_GROUP))); final @C.FormatSupport int[] trackSupport = MoreObjects.firstNonNull( - bundle.getIntArray(keyForField(FIELD_TRACK_SUPPORT)), new int[trackGroup.length]); + bundle.getIntArray(FIELD_TRACK_SUPPORT), new int[trackGroup.length]); boolean[] selected = MoreObjects.firstNonNull( - bundle.getBooleanArray(keyForField(FIELD_TRACK_SELECTED)), - new boolean[trackGroup.length]); - boolean adaptiveSupported = - bundle.getBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), false); + bundle.getBooleanArray(FIELD_TRACK_SELECTED), new boolean[trackGroup.length]); + boolean adaptiveSupported = bundle.getBoolean(FIELD_ADAPTIVE_SUPPORTED, false); return new Group(trackGroup, adaptiveSupported, trackSupport, selected); }; - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } /** Empty tracks. */ @@ -385,21 +363,13 @@ public int hashCode() { } // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_TRACK_GROUPS, - }) - private @interface FieldNumber {} - - private static final int FIELD_TRACK_GROUPS = 0; + private static final String FIELD_TRACK_GROUPS = Util.intToStringMaxRadix(0); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putParcelableArrayList(keyForField(FIELD_TRACK_GROUPS), toBundleArrayList(groups)); + bundle.putParcelableArrayList(FIELD_TRACK_GROUPS, toBundleArrayList(groups)); return bundle; } @@ -407,16 +377,11 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = bundle -> { - @Nullable - List groupBundles = bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUPS)); + @Nullable List groupBundles = bundle.getParcelableArrayList(FIELD_TRACK_GROUPS); List groups = groupBundles == null ? ImmutableList.of() : BundleableUtil.fromBundleList(Group.CREATOR, groupBundles); return new Tracks(groups); }; - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/VideoSize.java b/libraries/common/src/main/java/androidx/media3/common/VideoSize.java index 9c25c257ae0..fee94edbcd3 100644 --- a/libraries/common/src/main/java/androidx/media3/common/VideoSize.java +++ b/libraries/common/src/main/java/androidx/media3/common/VideoSize.java @@ -15,18 +15,12 @@ */ package androidx.media3.common; -import static java.lang.annotation.ElementType.TYPE_USE; - import android.os.Bundle; import androidx.annotation.FloatRange; -import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import androidx.media3.common.util.Util; /** Represents the video size. */ public final class VideoSize implements Bundleable { @@ -132,48 +126,32 @@ public int hashCode() { } // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_WIDTH, - FIELD_HEIGHT, - FIELD_UNAPPLIED_ROTATION_DEGREES, - FIELD_PIXEL_WIDTH_HEIGHT_RATIO, - }) - private @interface FieldNumber {} - - private static final int FIELD_WIDTH = 0; - private static final int FIELD_HEIGHT = 1; - private static final int FIELD_UNAPPLIED_ROTATION_DEGREES = 2; - private static final int FIELD_PIXEL_WIDTH_HEIGHT_RATIO = 3; + + private static final String FIELD_WIDTH = Util.intToStringMaxRadix(0); + private static final String FIELD_HEIGHT = Util.intToStringMaxRadix(1); + private static final String FIELD_UNAPPLIED_ROTATION_DEGREES = Util.intToStringMaxRadix(2); + private static final String FIELD_PIXEL_WIDTH_HEIGHT_RATIO = Util.intToStringMaxRadix(3); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_WIDTH), width); - bundle.putInt(keyForField(FIELD_HEIGHT), height); - bundle.putInt(keyForField(FIELD_UNAPPLIED_ROTATION_DEGREES), unappliedRotationDegrees); - bundle.putFloat(keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), pixelWidthHeightRatio); + bundle.putInt(FIELD_WIDTH, width); + bundle.putInt(FIELD_HEIGHT, height); + bundle.putInt(FIELD_UNAPPLIED_ROTATION_DEGREES, unappliedRotationDegrees); + bundle.putFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, pixelWidthHeightRatio); return bundle; } @UnstableApi public static final Creator CREATOR = bundle -> { - int width = bundle.getInt(keyForField(FIELD_WIDTH), DEFAULT_WIDTH); - int height = bundle.getInt(keyForField(FIELD_HEIGHT), DEFAULT_HEIGHT); + int width = bundle.getInt(FIELD_WIDTH, DEFAULT_WIDTH); + int height = bundle.getInt(FIELD_HEIGHT, DEFAULT_HEIGHT); int unappliedRotationDegrees = - bundle.getInt( - keyForField(FIELD_UNAPPLIED_ROTATION_DEGREES), DEFAULT_UNAPPLIED_ROTATION_DEGREES); + bundle.getInt(FIELD_UNAPPLIED_ROTATION_DEGREES, DEFAULT_UNAPPLIED_ROTATION_DEGREES); float pixelWidthHeightRatio = - bundle.getFloat( - keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO); + bundle.getFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO); return new VideoSize(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); }; - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/text/Cue.java b/libraries/common/src/main/java/androidx/media3/common/text/Cue.java index 475a29f9d3e..cb50d1005f3 100644 --- a/libraries/common/src/main/java/androidx/media3/common/text/Cue.java +++ b/libraries/common/src/main/java/androidx/media3/common/text/Cue.java @@ -35,6 +35,7 @@ import androidx.media3.common.Bundleable; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.base.Objects; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.lang.annotation.Documented; @@ -977,69 +978,45 @@ public Cue build() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_TEXT, - FIELD_TEXT_ALIGNMENT, - FIELD_MULTI_ROW_ALIGNMENT, - FIELD_BITMAP, - FIELD_LINE, - FIELD_LINE_TYPE, - FIELD_LINE_ANCHOR, - FIELD_POSITION, - FIELD_POSITION_ANCHOR, - FIELD_TEXT_SIZE_TYPE, - FIELD_TEXT_SIZE, - FIELD_SIZE, - FIELD_BITMAP_HEIGHT, - FIELD_WINDOW_COLOR, - FIELD_WINDOW_COLOR_SET, - FIELD_VERTICAL_TYPE, - FIELD_SHEAR_DEGREES - }) - private @interface FieldNumber {} - - private static final int FIELD_TEXT = 0; - private static final int FIELD_TEXT_ALIGNMENT = 1; - private static final int FIELD_MULTI_ROW_ALIGNMENT = 2; - private static final int FIELD_BITMAP = 3; - private static final int FIELD_LINE = 4; - private static final int FIELD_LINE_TYPE = 5; - private static final int FIELD_LINE_ANCHOR = 6; - private static final int FIELD_POSITION = 7; - private static final int FIELD_POSITION_ANCHOR = 8; - private static final int FIELD_TEXT_SIZE_TYPE = 9; - private static final int FIELD_TEXT_SIZE = 10; - private static final int FIELD_SIZE = 11; - private static final int FIELD_BITMAP_HEIGHT = 12; - private static final int FIELD_WINDOW_COLOR = 13; - private static final int FIELD_WINDOW_COLOR_SET = 14; - private static final int FIELD_VERTICAL_TYPE = 15; - private static final int FIELD_SHEAR_DEGREES = 16; + private static final String FIELD_TEXT = Util.intToStringMaxRadix(0); + private static final String FIELD_TEXT_ALIGNMENT = Util.intToStringMaxRadix(1); + private static final String FIELD_MULTI_ROW_ALIGNMENT = Util.intToStringMaxRadix(2); + private static final String FIELD_BITMAP = Util.intToStringMaxRadix(3); + private static final String FIELD_LINE = Util.intToStringMaxRadix(4); + private static final String FIELD_LINE_TYPE = Util.intToStringMaxRadix(5); + private static final String FIELD_LINE_ANCHOR = Util.intToStringMaxRadix(6); + private static final String FIELD_POSITION = Util.intToStringMaxRadix(7); + private static final String FIELD_POSITION_ANCHOR = Util.intToStringMaxRadix(8); + private static final String FIELD_TEXT_SIZE_TYPE = Util.intToStringMaxRadix(9); + private static final String FIELD_TEXT_SIZE = Util.intToStringMaxRadix(10); + private static final String FIELD_SIZE = Util.intToStringMaxRadix(11); + private static final String FIELD_BITMAP_HEIGHT = Util.intToStringMaxRadix(12); + private static final String FIELD_WINDOW_COLOR = Util.intToStringMaxRadix(13); + private static final String FIELD_WINDOW_COLOR_SET = Util.intToStringMaxRadix(14); + private static final String FIELD_VERTICAL_TYPE = Util.intToStringMaxRadix(15); + private static final String FIELD_SHEAR_DEGREES = Util.intToStringMaxRadix(16); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putCharSequence(keyForField(FIELD_TEXT), text); - bundle.putSerializable(keyForField(FIELD_TEXT_ALIGNMENT), textAlignment); - bundle.putSerializable(keyForField(FIELD_MULTI_ROW_ALIGNMENT), multiRowAlignment); - bundle.putParcelable(keyForField(FIELD_BITMAP), bitmap); - bundle.putFloat(keyForField(FIELD_LINE), line); - bundle.putInt(keyForField(FIELD_LINE_TYPE), lineType); - bundle.putInt(keyForField(FIELD_LINE_ANCHOR), lineAnchor); - bundle.putFloat(keyForField(FIELD_POSITION), position); - bundle.putInt(keyForField(FIELD_POSITION_ANCHOR), positionAnchor); - bundle.putInt(keyForField(FIELD_TEXT_SIZE_TYPE), textSizeType); - bundle.putFloat(keyForField(FIELD_TEXT_SIZE), textSize); - bundle.putFloat(keyForField(FIELD_SIZE), size); - bundle.putFloat(keyForField(FIELD_BITMAP_HEIGHT), bitmapHeight); - bundle.putBoolean(keyForField(FIELD_WINDOW_COLOR_SET), windowColorSet); - bundle.putInt(keyForField(FIELD_WINDOW_COLOR), windowColor); - bundle.putInt(keyForField(FIELD_VERTICAL_TYPE), verticalType); - bundle.putFloat(keyForField(FIELD_SHEAR_DEGREES), shearDegrees); + bundle.putCharSequence(FIELD_TEXT, text); + bundle.putSerializable(FIELD_TEXT_ALIGNMENT, textAlignment); + bundle.putSerializable(FIELD_MULTI_ROW_ALIGNMENT, multiRowAlignment); + bundle.putParcelable(FIELD_BITMAP, bitmap); + bundle.putFloat(FIELD_LINE, line); + bundle.putInt(FIELD_LINE_TYPE, lineType); + bundle.putInt(FIELD_LINE_ANCHOR, lineAnchor); + bundle.putFloat(FIELD_POSITION, position); + bundle.putInt(FIELD_POSITION_ANCHOR, positionAnchor); + bundle.putInt(FIELD_TEXT_SIZE_TYPE, textSizeType); + bundle.putFloat(FIELD_TEXT_SIZE, textSize); + bundle.putFloat(FIELD_SIZE, size); + bundle.putFloat(FIELD_BITMAP_HEIGHT, bitmapHeight); + bundle.putBoolean(FIELD_WINDOW_COLOR_SET, windowColorSet); + bundle.putInt(FIELD_WINDOW_COLOR, windowColor); + bundle.putInt(FIELD_VERTICAL_TYPE, verticalType); + bundle.putFloat(FIELD_SHEAR_DEGREES, shearDegrees); return bundle; } @@ -1047,67 +1024,56 @@ public Bundle toBundle() { private static final Cue fromBundle(Bundle bundle) { Builder builder = new Builder(); - @Nullable CharSequence text = bundle.getCharSequence(keyForField(FIELD_TEXT)); + @Nullable CharSequence text = bundle.getCharSequence(FIELD_TEXT); if (text != null) { builder.setText(text); } - @Nullable - Alignment textAlignment = (Alignment) bundle.getSerializable(keyForField(FIELD_TEXT_ALIGNMENT)); + @Nullable Alignment textAlignment = (Alignment) bundle.getSerializable(FIELD_TEXT_ALIGNMENT); if (textAlignment != null) { builder.setTextAlignment(textAlignment); } @Nullable - Alignment multiRowAlignment = - (Alignment) bundle.getSerializable(keyForField(FIELD_MULTI_ROW_ALIGNMENT)); + Alignment multiRowAlignment = (Alignment) bundle.getSerializable(FIELD_MULTI_ROW_ALIGNMENT); if (multiRowAlignment != null) { builder.setMultiRowAlignment(multiRowAlignment); } - @Nullable Bitmap bitmap = bundle.getParcelable(keyForField(FIELD_BITMAP)); + @Nullable Bitmap bitmap = bundle.getParcelable(FIELD_BITMAP); if (bitmap != null) { builder.setBitmap(bitmap); } - if (bundle.containsKey(keyForField(FIELD_LINE)) - && bundle.containsKey(keyForField(FIELD_LINE_TYPE))) { - builder.setLine( - bundle.getFloat(keyForField(FIELD_LINE)), bundle.getInt(keyForField(FIELD_LINE_TYPE))); + if (bundle.containsKey(FIELD_LINE) && bundle.containsKey(FIELD_LINE_TYPE)) { + builder.setLine(bundle.getFloat(FIELD_LINE), bundle.getInt(FIELD_LINE_TYPE)); } - if (bundle.containsKey(keyForField(FIELD_LINE_ANCHOR))) { - builder.setLineAnchor(bundle.getInt(keyForField(FIELD_LINE_ANCHOR))); + if (bundle.containsKey(FIELD_LINE_ANCHOR)) { + builder.setLineAnchor(bundle.getInt(FIELD_LINE_ANCHOR)); } - if (bundle.containsKey(keyForField(FIELD_POSITION))) { - builder.setPosition(bundle.getFloat(keyForField(FIELD_POSITION))); + if (bundle.containsKey(FIELD_POSITION)) { + builder.setPosition(bundle.getFloat(FIELD_POSITION)); } - if (bundle.containsKey(keyForField(FIELD_POSITION_ANCHOR))) { - builder.setPositionAnchor(bundle.getInt(keyForField(FIELD_POSITION_ANCHOR))); + if (bundle.containsKey(FIELD_POSITION_ANCHOR)) { + builder.setPositionAnchor(bundle.getInt(FIELD_POSITION_ANCHOR)); } - if (bundle.containsKey(keyForField(FIELD_TEXT_SIZE)) - && bundle.containsKey(keyForField(FIELD_TEXT_SIZE_TYPE))) { - builder.setTextSize( - bundle.getFloat(keyForField(FIELD_TEXT_SIZE)), - bundle.getInt(keyForField(FIELD_TEXT_SIZE_TYPE))); + if (bundle.containsKey(FIELD_TEXT_SIZE) && bundle.containsKey(FIELD_TEXT_SIZE_TYPE)) { + builder.setTextSize(bundle.getFloat(FIELD_TEXT_SIZE), bundle.getInt(FIELD_TEXT_SIZE_TYPE)); } - if (bundle.containsKey(keyForField(FIELD_SIZE))) { - builder.setSize(bundle.getFloat(keyForField(FIELD_SIZE))); + if (bundle.containsKey(FIELD_SIZE)) { + builder.setSize(bundle.getFloat(FIELD_SIZE)); } - if (bundle.containsKey(keyForField(FIELD_BITMAP_HEIGHT))) { - builder.setBitmapHeight(bundle.getFloat(keyForField(FIELD_BITMAP_HEIGHT))); + if (bundle.containsKey(FIELD_BITMAP_HEIGHT)) { + builder.setBitmapHeight(bundle.getFloat(FIELD_BITMAP_HEIGHT)); } - if (bundle.containsKey(keyForField(FIELD_WINDOW_COLOR))) { - builder.setWindowColor(bundle.getInt(keyForField(FIELD_WINDOW_COLOR))); + if (bundle.containsKey(FIELD_WINDOW_COLOR)) { + builder.setWindowColor(bundle.getInt(FIELD_WINDOW_COLOR)); } - if (!bundle.getBoolean(keyForField(FIELD_WINDOW_COLOR_SET), /* defaultValue= */ false)) { + if (!bundle.getBoolean(FIELD_WINDOW_COLOR_SET, /* defaultValue= */ false)) { builder.clearWindowColor(); } - if (bundle.containsKey(keyForField(FIELD_VERTICAL_TYPE))) { - builder.setVerticalType(bundle.getInt(keyForField(FIELD_VERTICAL_TYPE))); + if (bundle.containsKey(FIELD_VERTICAL_TYPE)) { + builder.setVerticalType(bundle.getInt(FIELD_VERTICAL_TYPE)); } - if (bundle.containsKey(keyForField(FIELD_SHEAR_DEGREES))) { - builder.setShearDegrees(bundle.getFloat(keyForField(FIELD_SHEAR_DEGREES))); + if (bundle.containsKey(FIELD_SHEAR_DEGREES)) { + builder.setShearDegrees(bundle.getFloat(FIELD_SHEAR_DEGREES)); } return builder.build(); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/text/CueGroup.java b/libraries/common/src/main/java/androidx/media3/common/text/CueGroup.java index df11b6fda8a..a77a75c66ca 100644 --- a/libraries/common/src/main/java/androidx/media3/common/text/CueGroup.java +++ b/libraries/common/src/main/java/androidx/media3/common/text/CueGroup.java @@ -15,21 +15,15 @@ */ package androidx.media3.common.text; -import static java.lang.annotation.ElementType.TYPE_USE; - import android.graphics.Bitmap; import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.Bundleable; import androidx.media3.common.Timeline; import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.ArrayList; import java.util.List; @@ -66,41 +60,31 @@ public CueGroup(List cues, long presentationTimeUs) { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_CUES, FIELD_PRESENTATION_TIME_US}) - private @interface FieldNumber {} - - private static final int FIELD_CUES = 0; - private static final int FIELD_PRESENTATION_TIME_US = 1; + private static final String FIELD_CUES = Util.intToStringMaxRadix(0); + private static final String FIELD_PRESENTATION_TIME_US = Util.intToStringMaxRadix(1); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); bundle.putParcelableArrayList( - keyForField(FIELD_CUES), BundleableUtil.toBundleArrayList(filterOutBitmapCues(cues))); - bundle.putLong(keyForField(FIELD_PRESENTATION_TIME_US), presentationTimeUs); + FIELD_CUES, BundleableUtil.toBundleArrayList(filterOutBitmapCues(cues))); + bundle.putLong(FIELD_PRESENTATION_TIME_US, presentationTimeUs); return bundle; } @UnstableApi public static final Creator CREATOR = CueGroup::fromBundle; private static final CueGroup fromBundle(Bundle bundle) { - @Nullable ArrayList cueBundles = bundle.getParcelableArrayList(keyForField(FIELD_CUES)); + @Nullable ArrayList cueBundles = bundle.getParcelableArrayList(FIELD_CUES); List cues = cueBundles == null ? ImmutableList.of() : BundleableUtil.fromBundleList(Cue.CREATOR, cueBundles); - long presentationTimeUs = bundle.getLong(keyForField(FIELD_PRESENTATION_TIME_US)); + long presentationTimeUs = bundle.getLong(FIELD_PRESENTATION_TIME_US); return new CueGroup(cues, presentationTimeUs); } - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } - /** * Filters out {@link Cue} objects containing {@link Bitmap}. It is used when transferring cues * between processes to prevent transferring too much data. diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index b1669556d2c..464db2648d9 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -2884,6 +2884,16 @@ public static Drawable getDrawable( : resources.getDrawable(drawableRes); } + /** + * Returns a string representation of the integer using radix value {@link Character#MAX_RADIX}. + * + * @param i An integer to be converted to String. + */ + @UnstableApi + public static String intToStringMaxRadix(int i) { + return Integer.toString(i, Character.MAX_RADIX); + } + @Nullable private static String getSystemProperty(String name) { try { diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java index 3f557ca8d2a..4df2c9d0b75 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java @@ -859,6 +859,8 @@ public void createDefaultMediaItemInstance_toBundleSkipsDefaultValues_fromBundle @Test public void createMediaItemInstance_roundTripViaBundle_yieldsEqualInstance() { + Bundle extras = new Bundle(); + extras.putString("key", "value"); // Creates instance by setting some non-default values MediaItem mediaItem = new MediaItem.Builder() @@ -874,11 +876,14 @@ public void createMediaItemInstance_roundTripViaBundle_yieldsEqualInstance() { new RequestMetadata.Builder() .setMediaUri(Uri.parse("http://test.test")) .setSearchQuery("search") + .setExtras(extras) .build()) .build(); MediaItem mediaItemFromBundle = MediaItem.CREATOR.fromBundle(mediaItem.toBundle()); assertThat(mediaItemFromBundle).isEqualTo(mediaItem); + assertThat(mediaItemFromBundle.requestMetadata.extras) + .isEqualTo(mediaItem.requestMetadata.extras); } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlaybackException.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlaybackException.java index 19d69529ca2..82a958ce16a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlaybackException.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlaybackException.java @@ -250,17 +250,15 @@ private ExoPlaybackException( private ExoPlaybackException(Bundle bundle) { super(bundle); - type = bundle.getInt(keyForField(FIELD_TYPE), /* defaultValue= */ TYPE_UNEXPECTED); - rendererName = bundle.getString(keyForField(FIELD_RENDERER_NAME)); - rendererIndex = - bundle.getInt(keyForField(FIELD_RENDERER_INDEX), /* defaultValue= */ C.INDEX_UNSET); - @Nullable Bundle rendererFormatBundle = bundle.getBundle(keyForField(FIELD_RENDERER_FORMAT)); + type = bundle.getInt(FIELD_TYPE, /* defaultValue= */ TYPE_UNEXPECTED); + rendererName = bundle.getString(FIELD_RENDERER_NAME); + rendererIndex = bundle.getInt(FIELD_RENDERER_INDEX, /* defaultValue= */ C.INDEX_UNSET); + @Nullable Bundle rendererFormatBundle = bundle.getBundle(FIELD_RENDERER_FORMAT); rendererFormat = rendererFormatBundle == null ? null : Format.CREATOR.fromBundle(rendererFormatBundle); rendererFormatSupport = - bundle.getInt( - keyForField(FIELD_RENDERER_FORMAT_SUPPORT), /* defaultValue= */ C.FORMAT_HANDLED); - isRecoverable = bundle.getBoolean(keyForField(FIELD_IS_RECOVERABLE), /* defaultValue= */ false); + bundle.getInt(FIELD_RENDERER_FORMAT_SUPPORT, /* defaultValue= */ C.FORMAT_HANDLED); + isRecoverable = bundle.getBoolean(FIELD_IS_RECOVERABLE, /* defaultValue= */ false); mediaPeriodId = null; } @@ -403,12 +401,17 @@ private static String deriveMessage( @UnstableApi public static final Creator CREATOR = ExoPlaybackException::new; - private static final int FIELD_TYPE = FIELD_CUSTOM_ID_BASE + 1; - private static final int FIELD_RENDERER_NAME = FIELD_CUSTOM_ID_BASE + 2; - private static final int FIELD_RENDERER_INDEX = FIELD_CUSTOM_ID_BASE + 3; - private static final int FIELD_RENDERER_FORMAT = FIELD_CUSTOM_ID_BASE + 4; - private static final int FIELD_RENDERER_FORMAT_SUPPORT = FIELD_CUSTOM_ID_BASE + 5; - private static final int FIELD_IS_RECOVERABLE = FIELD_CUSTOM_ID_BASE + 6; + private static final String FIELD_TYPE = Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 1); + private static final String FIELD_RENDERER_NAME = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 2); + private static final String FIELD_RENDERER_INDEX = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 3); + private static final String FIELD_RENDERER_FORMAT = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 4); + private static final String FIELD_RENDERER_FORMAT_SUPPORT = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 5); + private static final String FIELD_IS_RECOVERABLE = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 6); /** * {@inheritDoc} @@ -420,14 +423,14 @@ private static String deriveMessage( @Override public Bundle toBundle() { Bundle bundle = super.toBundle(); - bundle.putInt(keyForField(FIELD_TYPE), type); - bundle.putString(keyForField(FIELD_RENDERER_NAME), rendererName); - bundle.putInt(keyForField(FIELD_RENDERER_INDEX), rendererIndex); + bundle.putInt(FIELD_TYPE, type); + bundle.putString(FIELD_RENDERER_NAME, rendererName); + bundle.putInt(FIELD_RENDERER_INDEX, rendererIndex); if (rendererFormat != null) { - bundle.putBundle(keyForField(FIELD_RENDERER_FORMAT), rendererFormat.toBundle()); + bundle.putBundle(FIELD_RENDERER_FORMAT, rendererFormat.toBundle()); } - bundle.putInt(keyForField(FIELD_RENDERER_FORMAT_SUPPORT), rendererFormatSupport); - bundle.putBoolean(keyForField(FIELD_IS_RECOVERABLE), isRecoverable); + bundle.putInt(FIELD_RENDERER_FORMAT_SUPPORT, rendererFormatSupport); + bundle.putBoolean(FIELD_IS_RECOVERABLE, isRecoverable); return bundle; } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/TrackGroupArray.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/TrackGroupArray.java index 0bc5a014f67..befc158d25a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/TrackGroupArray.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/TrackGroupArray.java @@ -15,10 +15,7 @@ */ package androidx.media3.exoplayer.source; -import static java.lang.annotation.ElementType.TYPE_USE; - import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.Bundleable; import androidx.media3.common.C; @@ -26,11 +23,8 @@ import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.List; /** @@ -118,21 +112,13 @@ public boolean equals(@Nullable Object obj) { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_TRACK_GROUPS, - }) - private @interface FieldNumber {} - - private static final int FIELD_TRACK_GROUPS = 0; + private static final String FIELD_TRACK_GROUPS = Util.intToStringMaxRadix(0); @Override public Bundle toBundle() { Bundle bundle = new Bundle(); bundle.putParcelableArrayList( - keyForField(FIELD_TRACK_GROUPS), BundleableUtil.toBundleArrayList(trackGroups)); + FIELD_TRACK_GROUPS, BundleableUtil.toBundleArrayList(trackGroups)); return bundle; } @@ -140,8 +126,7 @@ public Bundle toBundle() { public static final Creator CREATOR = bundle -> { @Nullable - List trackGroupBundles = - bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUPS)); + List trackGroupBundles = bundle.getParcelableArrayList(FIELD_TRACK_GROUPS); if (trackGroupBundles == null) { return new TrackGroupArray(); } @@ -163,8 +148,4 @@ private void verifyCorrectness() { } } } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index c3c8992476d..50a0ab216d4 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -827,69 +827,62 @@ private Builder(Bundle bundle) { // Video setExceedVideoConstraintsIfNecessary( bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY), + Parameters.FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY, defaultValue.exceedVideoConstraintsIfNecessary)); setAllowVideoMixedMimeTypeAdaptiveness( bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS), + Parameters.FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS, defaultValue.allowVideoMixedMimeTypeAdaptiveness)); setAllowVideoNonSeamlessAdaptiveness( bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS), + Parameters.FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS, defaultValue.allowVideoNonSeamlessAdaptiveness)); setAllowVideoMixedDecoderSupportAdaptiveness( bundle.getBoolean( - Parameters.keyForField( - Parameters.FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), + Parameters.FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS, defaultValue.allowVideoMixedDecoderSupportAdaptiveness)); // Audio setExceedAudioConstraintsIfNecessary( bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY), + Parameters.FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY, defaultValue.exceedAudioConstraintsIfNecessary)); setAllowAudioMixedMimeTypeAdaptiveness( bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS), + Parameters.FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS, defaultValue.allowAudioMixedMimeTypeAdaptiveness)); setAllowAudioMixedSampleRateAdaptiveness( bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS), + Parameters.FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS, defaultValue.allowAudioMixedSampleRateAdaptiveness)); setAllowAudioMixedChannelCountAdaptiveness( bundle.getBoolean( - Parameters.keyForField( - Parameters.FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS), + Parameters.FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS, defaultValue.allowAudioMixedChannelCountAdaptiveness)); setAllowAudioMixedDecoderSupportAdaptiveness( bundle.getBoolean( - Parameters.keyForField( - Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), + Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS, defaultValue.allowAudioMixedDecoderSupportAdaptiveness)); setConstrainAudioChannelCountToDeviceCapabilities( bundle.getBoolean( - Parameters.keyForField( - Parameters.FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES), + Parameters.FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES, defaultValue.constrainAudioChannelCountToDeviceCapabilities)); // General setExceedRendererCapabilitiesIfNecessary( bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY), + Parameters.FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY, defaultValue.exceedRendererCapabilitiesIfNecessary)); setTunnelingEnabled( - bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_TUNNELING_ENABLED), - defaultValue.tunnelingEnabled)); + bundle.getBoolean(Parameters.FIELD_TUNNELING_ENABLED, defaultValue.tunnelingEnabled)); setAllowMultipleAdaptiveSelections( bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS), + Parameters.FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS, defaultValue.allowMultipleAdaptiveSelections)); // Overrides selectionOverrides = new SparseArray<>(); setSelectionOverridesFromBundle(bundle); rendererDisabledFlags = makeSparseBooleanArrayFromTrueKeys( - bundle.getIntArray( - Parameters.keyForField(Parameters.FIELD_RENDERER_DISABLED_INDICES))); + bundle.getIntArray(Parameters.FIELD_RENDERER_DISABLED_INDICES)); } @CanIgnoreReturnValue @@ -1571,20 +1564,17 @@ private void init(Builder this) { private void setSelectionOverridesFromBundle(Bundle bundle) { @Nullable int[] rendererIndices = - bundle.getIntArray( - Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_RENDERER_INDICES)); + bundle.getIntArray(Parameters.FIELD_SELECTION_OVERRIDES_RENDERER_INDICES); @Nullable ArrayList trackGroupArrayBundles = - bundle.getParcelableArrayList( - Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS)); + bundle.getParcelableArrayList(Parameters.FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS); List trackGroupArrays = trackGroupArrayBundles == null ? ImmutableList.of() : BundleableUtil.fromBundleList(TrackGroupArray.CREATOR, trackGroupArrayBundles); @Nullable SparseArray selectionOverrideBundles = - bundle.getSparseParcelableArray( - Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES)); + bundle.getSparseParcelableArray(Parameters.FIELD_SELECTION_OVERRIDES); SparseArray selectionOverrides = selectionOverrideBundles == null ? new SparseArray<>() @@ -1874,32 +1864,40 @@ public int hashCode() { // Bundleable implementation. - private static final int FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY = FIELD_CUSTOM_ID_BASE; - private static final int FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS = - FIELD_CUSTOM_ID_BASE + 1; - private static final int FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS = FIELD_CUSTOM_ID_BASE + 2; - private static final int FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY = FIELD_CUSTOM_ID_BASE + 3; - private static final int FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS = - FIELD_CUSTOM_ID_BASE + 4; - private static final int FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS = - FIELD_CUSTOM_ID_BASE + 5; - private static final int FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS = - FIELD_CUSTOM_ID_BASE + 6; - private static final int FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY = - FIELD_CUSTOM_ID_BASE + 7; - private static final int FIELD_TUNNELING_ENABLED = FIELD_CUSTOM_ID_BASE + 8; - private static final int FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = FIELD_CUSTOM_ID_BASE + 9; - private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = FIELD_CUSTOM_ID_BASE + 10; - private static final int FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS = - FIELD_CUSTOM_ID_BASE + 11; - private static final int FIELD_SELECTION_OVERRIDES = FIELD_CUSTOM_ID_BASE + 12; - private static final int FIELD_RENDERER_DISABLED_INDICES = FIELD_CUSTOM_ID_BASE + 13; - private static final int FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = - FIELD_CUSTOM_ID_BASE + 14; - private static final int FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = - FIELD_CUSTOM_ID_BASE + 15; - private static final int FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES = - FIELD_CUSTOM_ID_BASE + 16; + private static final String FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE); + private static final String FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 1); + private static final String FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 2); + private static final String FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 3); + private static final String FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 4); + private static final String FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 5); + private static final String FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 6); + private static final String FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 7); + private static final String FIELD_TUNNELING_ENABLED = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 8); + private static final String FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 9); + private static final String FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 10); + private static final String FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 11); + private static final String FIELD_SELECTION_OVERRIDES = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 12); + private static final String FIELD_RENDERER_DISABLED_INDICES = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 13); + private static final String FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 14); + private static final String FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 15); + private static final String FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 16); @Override public Bundle toBundle() { @@ -1907,49 +1905,40 @@ public Bundle toBundle() { // Video bundle.putBoolean( - keyForField(FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY), - exceedVideoConstraintsIfNecessary); + FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY, exceedVideoConstraintsIfNecessary); bundle.putBoolean( - keyForField(FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS), - allowVideoMixedMimeTypeAdaptiveness); + FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS, allowVideoMixedMimeTypeAdaptiveness); bundle.putBoolean( - keyForField(FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS), - allowVideoNonSeamlessAdaptiveness); + FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS, allowVideoNonSeamlessAdaptiveness); bundle.putBoolean( - keyForField(FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), + FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS, allowVideoMixedDecoderSupportAdaptiveness); // Audio bundle.putBoolean( - keyForField(FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY), - exceedAudioConstraintsIfNecessary); + FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY, exceedAudioConstraintsIfNecessary); bundle.putBoolean( - keyForField(FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS), - allowAudioMixedMimeTypeAdaptiveness); + FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS, allowAudioMixedMimeTypeAdaptiveness); bundle.putBoolean( - keyForField(FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS), - allowAudioMixedSampleRateAdaptiveness); + FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS, allowAudioMixedSampleRateAdaptiveness); bundle.putBoolean( - keyForField(FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS), + FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS, allowAudioMixedChannelCountAdaptiveness); bundle.putBoolean( - keyForField(FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), + FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS, allowAudioMixedDecoderSupportAdaptiveness); bundle.putBoolean( - keyForField(FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES), + FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES, constrainAudioChannelCountToDeviceCapabilities); // General bundle.putBoolean( - keyForField(FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY), - exceedRendererCapabilitiesIfNecessary); - bundle.putBoolean(keyForField(FIELD_TUNNELING_ENABLED), tunnelingEnabled); - bundle.putBoolean( - keyForField(FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS), allowMultipleAdaptiveSelections); + FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY, exceedRendererCapabilitiesIfNecessary); + bundle.putBoolean(FIELD_TUNNELING_ENABLED, tunnelingEnabled); + bundle.putBoolean(FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS, allowMultipleAdaptiveSelections); putSelectionOverridesToBundle(bundle, selectionOverrides); // Only true values are put into rendererDisabledFlags. bundle.putIntArray( - keyForField(FIELD_RENDERER_DISABLED_INDICES), - getKeysFromSparseBooleanArray(rendererDisabledFlags)); + FIELD_RENDERER_DISABLED_INDICES, getKeysFromSparseBooleanArray(rendererDisabledFlags)); return bundle; } @@ -1982,12 +1971,12 @@ private static void putSelectionOverridesToBundle( rendererIndices.add(rendererIndex); } bundle.putIntArray( - keyForField(FIELD_SELECTION_OVERRIDES_RENDERER_INDICES), Ints.toArray(rendererIndices)); + FIELD_SELECTION_OVERRIDES_RENDERER_INDICES, Ints.toArray(rendererIndices)); bundle.putParcelableArrayList( - keyForField(FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS), + FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS, BundleableUtil.toBundleArrayList(trackGroupArrays)); bundle.putSparseParcelableArray( - keyForField(FIELD_SELECTION_OVERRIDES), BundleableUtil.toBundleSparseArray(selections)); + FIELD_SELECTION_OVERRIDES, BundleableUtil.toBundleSparseArray(selections)); } } @@ -2116,27 +2105,17 @@ public boolean equals(@Nullable Object obj) { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_GROUP_INDEX, - FIELD_TRACKS, - FIELD_TRACK_TYPE, - }) - private @interface FieldNumber {} - - private static final int FIELD_GROUP_INDEX = 0; - private static final int FIELD_TRACKS = 1; - private static final int FIELD_TRACK_TYPE = 2; + private static final String FIELD_GROUP_INDEX = Util.intToStringMaxRadix(0); + private static final String FIELD_TRACKS = Util.intToStringMaxRadix(1); + private static final String FIELD_TRACK_TYPE = Util.intToStringMaxRadix(2); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_GROUP_INDEX), groupIndex); - bundle.putIntArray(keyForField(FIELD_TRACKS), tracks); - bundle.putInt(keyForField(FIELD_TRACK_TYPE), type); + bundle.putInt(FIELD_GROUP_INDEX, groupIndex); + bundle.putIntArray(FIELD_TRACKS, tracks); + bundle.putInt(FIELD_TRACK_TYPE, type); return bundle; } @@ -2144,17 +2123,13 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = bundle -> { - int groupIndex = bundle.getInt(keyForField(FIELD_GROUP_INDEX), -1); - @Nullable int[] tracks = bundle.getIntArray(keyForField(FIELD_TRACKS)); - int trackType = bundle.getInt(keyForField(FIELD_TRACK_TYPE), -1); + int groupIndex = bundle.getInt(FIELD_GROUP_INDEX, -1); + @Nullable int[] tracks = bundle.getIntArray(FIELD_TRACKS); + int trackType = bundle.getInt(FIELD_TRACK_TYPE, -1); Assertions.checkArgument(groupIndex >= 0 && trackType >= 0); Assertions.checkNotNull(tracks); return new SelectionOverride(groupIndex, tracks, trackType); }; - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } /** diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index 2f328e1c157..70fccc76558 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -30,7 +30,6 @@ import static androidx.media3.exoplayer.ima.ImaUtil.splitAdPlaybackStateForPeriods; import static androidx.media3.exoplayer.ima.ImaUtil.updateAdDurationInAdGroup; import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState; -import static java.lang.annotation.ElementType.TYPE_USE; import android.content.Context; import android.net.Uri; @@ -39,7 +38,6 @@ import android.os.Looper; import android.util.Pair; import android.view.ViewGroup; -import androidx.annotation.IntDef; import androidx.annotation.MainThread; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -98,10 +96,6 @@ import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -328,13 +322,7 @@ public int hashCode() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_AD_PLAYBACK_STATES}) - private @interface FieldNumber {} - - private static final int FIELD_AD_PLAYBACK_STATES = 1; + private static final String FIELD_AD_PLAYBACK_STATES = Util.intToStringMaxRadix(1); @Override public Bundle toBundle() { @@ -343,7 +331,7 @@ public Bundle toBundle() { for (Map.Entry entry : adPlaybackStates.entrySet()) { adPlaybackStatesBundle.putBundle(entry.getKey(), entry.getValue().toBundle()); } - bundle.putBundle(keyForField(FIELD_AD_PLAYBACK_STATES), adPlaybackStatesBundle); + bundle.putBundle(FIELD_AD_PLAYBACK_STATES, adPlaybackStatesBundle); return bundle; } @@ -354,8 +342,7 @@ private static State fromBundle(Bundle bundle) { @Nullable ImmutableMap.Builder adPlaybackStateMap = new ImmutableMap.Builder<>(); - Bundle adPlaybackStateBundle = - checkNotNull(bundle.getBundle(keyForField(FIELD_AD_PLAYBACK_STATES))); + Bundle adPlaybackStateBundle = checkNotNull(bundle.getBundle(FIELD_AD_PLAYBACK_STATES)); for (String key : adPlaybackStateBundle.keySet()) { AdPlaybackState adPlaybackState = AdPlaybackState.CREATOR.fromBundle( @@ -365,10 +352,6 @@ private static State fromBundle(Bundle bundle) { } return new State(adPlaybackStateMap.buildOrThrow()); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } private final ImaUtil.ServerSideAdInsertionConfiguration configuration; diff --git a/libraries/session/src/main/java/androidx/media3/session/CommandButton.java b/libraries/session/src/main/java/androidx/media3/session/CommandButton.java index b8632b76175..989cd4a57c2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/CommandButton.java +++ b/libraries/session/src/main/java/androidx/media3/session/CommandButton.java @@ -17,20 +17,15 @@ import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; -import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; import androidx.annotation.DrawableRes; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.Bundleable; import androidx.media3.common.Player; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.List; /** @@ -201,38 +196,25 @@ private CommandButton( // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_SESSION_COMMAND, - FIELD_PLAYER_COMMAND, - FIELD_ICON_RES_ID, - FIELD_DISPLAY_NAME, - FIELD_EXTRAS, - FIELD_ENABLED - }) - private @interface FieldNumber {} - - private static final int FIELD_SESSION_COMMAND = 0; - private static final int FIELD_PLAYER_COMMAND = 1; - private static final int FIELD_ICON_RES_ID = 2; - private static final int FIELD_DISPLAY_NAME = 3; - private static final int FIELD_EXTRAS = 4; - private static final int FIELD_ENABLED = 5; + private static final String FIELD_SESSION_COMMAND = Util.intToStringMaxRadix(0); + private static final String FIELD_PLAYER_COMMAND = Util.intToStringMaxRadix(1); + private static final String FIELD_ICON_RES_ID = Util.intToStringMaxRadix(2); + private static final String FIELD_DISPLAY_NAME = Util.intToStringMaxRadix(3); + private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(4); + private static final String FIELD_ENABLED = Util.intToStringMaxRadix(5); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); if (sessionCommand != null) { - bundle.putBundle(keyForField(FIELD_SESSION_COMMAND), sessionCommand.toBundle()); + bundle.putBundle(FIELD_SESSION_COMMAND, sessionCommand.toBundle()); } - bundle.putInt(keyForField(FIELD_PLAYER_COMMAND), playerCommand); - bundle.putInt(keyForField(FIELD_ICON_RES_ID), iconResId); - bundle.putCharSequence(keyForField(FIELD_DISPLAY_NAME), displayName); - bundle.putBundle(keyForField(FIELD_EXTRAS), extras); - bundle.putBoolean(keyForField(FIELD_ENABLED), isEnabled); + bundle.putInt(FIELD_PLAYER_COMMAND, playerCommand); + bundle.putInt(FIELD_ICON_RES_ID, iconResId); + bundle.putCharSequence(FIELD_DISPLAY_NAME, displayName); + bundle.putBundle(FIELD_EXTRAS, extras); + bundle.putBoolean(FIELD_ENABLED, isEnabled); return bundle; } @@ -240,7 +222,7 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = CommandButton::fromBundle; private static CommandButton fromBundle(Bundle bundle) { - @Nullable Bundle sessionCommandBundle = bundle.getBundle(keyForField(FIELD_SESSION_COMMAND)); + @Nullable Bundle sessionCommandBundle = bundle.getBundle(FIELD_SESSION_COMMAND); @Nullable SessionCommand sessionCommand = sessionCommandBundle == null @@ -248,13 +230,11 @@ private static CommandButton fromBundle(Bundle bundle) { : SessionCommand.CREATOR.fromBundle(sessionCommandBundle); @Player.Command int playerCommand = - bundle.getInt( - keyForField(FIELD_PLAYER_COMMAND), /* defaultValue= */ Player.COMMAND_INVALID); - int iconResId = bundle.getInt(keyForField(FIELD_ICON_RES_ID), /* defaultValue= */ 0); - CharSequence displayName = - bundle.getCharSequence(keyForField(FIELD_DISPLAY_NAME), /* defaultValue= */ ""); - @Nullable Bundle extras = bundle.getBundle(keyForField(FIELD_EXTRAS)); - boolean enabled = bundle.getBoolean(keyForField(FIELD_ENABLED), /* defaultValue= */ false); + bundle.getInt(FIELD_PLAYER_COMMAND, /* defaultValue= */ Player.COMMAND_INVALID); + int iconResId = bundle.getInt(FIELD_ICON_RES_ID, /* defaultValue= */ 0); + CharSequence displayName = bundle.getCharSequence(FIELD_DISPLAY_NAME, /* defaultValue= */ ""); + @Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS); + boolean enabled = bundle.getBoolean(FIELD_ENABLED, /* defaultValue= */ false); Builder builder = new Builder(); if (sessionCommand != null) { builder.setSessionCommand(sessionCommand); @@ -269,8 +249,4 @@ private static CommandButton fromBundle(Bundle bundle) { .setEnabled(enabled) .build(); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectionRequest.java b/libraries/session/src/main/java/androidx/media3/session/ConnectionRequest.java index 6b09d056a59..037baff628d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectionRequest.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectionRequest.java @@ -17,17 +17,12 @@ import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; -import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.Bundleable; import androidx.media3.common.MediaLibraryInfo; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import androidx.media3.common.util.Util; /** * Created by {@link MediaController} to send its state to the {@link MediaSession} to request to @@ -69,47 +64,34 @@ private ConnectionRequest( // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_LIBRARY_VERSION, - FIELD_PACKAGE_NAME, - FIELD_PID, - FIELD_CONNECTION_HINTS, - FIELD_CONTROLLER_INTERFACE_VERSION - }) - private @interface FieldNumber {} - - private static final int FIELD_LIBRARY_VERSION = 0; - private static final int FIELD_PACKAGE_NAME = 1; - private static final int FIELD_PID = 2; - private static final int FIELD_CONNECTION_HINTS = 3; - private static final int FIELD_CONTROLLER_INTERFACE_VERSION = 4; + private static final String FIELD_LIBRARY_VERSION = Util.intToStringMaxRadix(0); + private static final String FIELD_PACKAGE_NAME = Util.intToStringMaxRadix(1); + private static final String FIELD_PID = Util.intToStringMaxRadix(2); + private static final String FIELD_CONNECTION_HINTS = Util.intToStringMaxRadix(3); + private static final String FIELD_CONTROLLER_INTERFACE_VERSION = Util.intToStringMaxRadix(4); // Next id: 5 @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_LIBRARY_VERSION), libraryVersion); - bundle.putString(keyForField(FIELD_PACKAGE_NAME), packageName); - bundle.putInt(keyForField(FIELD_PID), pid); - bundle.putBundle(keyForField(FIELD_CONNECTION_HINTS), connectionHints); - bundle.putInt(keyForField(FIELD_CONTROLLER_INTERFACE_VERSION), controllerInterfaceVersion); + bundle.putInt(FIELD_LIBRARY_VERSION, libraryVersion); + bundle.putString(FIELD_PACKAGE_NAME, packageName); + bundle.putInt(FIELD_PID, pid); + bundle.putBundle(FIELD_CONNECTION_HINTS, connectionHints); + bundle.putInt(FIELD_CONTROLLER_INTERFACE_VERSION, controllerInterfaceVersion); return bundle; } /** Object that can restore {@link ConnectionRequest} from a {@link Bundle}. */ public static final Creator CREATOR = bundle -> { - int libraryVersion = - bundle.getInt(keyForField(FIELD_LIBRARY_VERSION), /* defaultValue= */ 0); + int libraryVersion = bundle.getInt(FIELD_LIBRARY_VERSION, /* defaultValue= */ 0); int controllerInterfaceVersion = - bundle.getInt(keyForField(FIELD_CONTROLLER_INTERFACE_VERSION), /* defaultValue= */ 0); - String packageName = checkNotNull(bundle.getString(keyForField(FIELD_PACKAGE_NAME))); - int pid = bundle.getInt(keyForField(FIELD_PID), /* defaultValue= */ 0); + bundle.getInt(FIELD_CONTROLLER_INTERFACE_VERSION, /* defaultValue= */ 0); + String packageName = checkNotNull(bundle.getString(FIELD_PACKAGE_NAME)); + int pid = bundle.getInt(FIELD_PID, /* defaultValue= */ 0); checkArgument(pid != 0); - @Nullable Bundle connectionHints = bundle.getBundle(keyForField(FIELD_CONNECTION_HINTS)); + @Nullable Bundle connectionHints = bundle.getBundle(FIELD_CONNECTION_HINTS); return new ConnectionRequest( libraryVersion, controllerInterfaceVersion, @@ -117,8 +99,4 @@ public Bundle toBundle() { pid, connectionHints == null ? Bundle.EMPTY : connectionHints); }; - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java index afb57b623f1..113848eda06 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java @@ -16,20 +16,15 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkNotNull; -import static java.lang.annotation.ElementType.TYPE_USE; import android.app.PendingIntent; import android.os.Bundle; import android.os.IBinder; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.core.app.BundleCompat; import androidx.media3.common.Bundleable; import androidx.media3.common.Player; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import androidx.media3.common.util.Util; /** * Created by {@link MediaSession} to send its state to the {@link MediaController} when the @@ -78,47 +73,29 @@ public ConnectionState( // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_LIBRARY_VERSION, - FIELD_SESSION_BINDER, - FIELD_SESSION_ACTIVITY, - FIELD_SESSION_COMMANDS, - FIELD_PLAYER_COMMANDS_FROM_SESSION, - FIELD_PLAYER_COMMANDS_FROM_PLAYER, - FIELD_TOKEN_EXTRAS, - FIELD_PLAYER_INFO, - FIELD_SESSION_INTERFACE_VERSION, - }) - private @interface FieldNumber {} - - private static final int FIELD_LIBRARY_VERSION = 0; - private static final int FIELD_SESSION_BINDER = 1; - private static final int FIELD_SESSION_ACTIVITY = 2; - private static final int FIELD_SESSION_COMMANDS = 3; - private static final int FIELD_PLAYER_COMMANDS_FROM_SESSION = 4; - private static final int FIELD_PLAYER_COMMANDS_FROM_PLAYER = 5; - private static final int FIELD_TOKEN_EXTRAS = 6; - private static final int FIELD_PLAYER_INFO = 7; - private static final int FIELD_SESSION_INTERFACE_VERSION = 8; + private static final String FIELD_LIBRARY_VERSION = Util.intToStringMaxRadix(0); + private static final String FIELD_SESSION_BINDER = Util.intToStringMaxRadix(1); + private static final String FIELD_SESSION_ACTIVITY = Util.intToStringMaxRadix(2); + private static final String FIELD_SESSION_COMMANDS = Util.intToStringMaxRadix(3); + private static final String FIELD_PLAYER_COMMANDS_FROM_SESSION = Util.intToStringMaxRadix(4); + private static final String FIELD_PLAYER_COMMANDS_FROM_PLAYER = Util.intToStringMaxRadix(5); + private static final String FIELD_TOKEN_EXTRAS = Util.intToStringMaxRadix(6); + private static final String FIELD_PLAYER_INFO = Util.intToStringMaxRadix(7); + private static final String FIELD_SESSION_INTERFACE_VERSION = Util.intToStringMaxRadix(8); // Next field key = 9 @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_LIBRARY_VERSION), libraryVersion); - BundleCompat.putBinder(bundle, keyForField(FIELD_SESSION_BINDER), sessionBinder.asBinder()); - bundle.putParcelable(keyForField(FIELD_SESSION_ACTIVITY), sessionActivity); - bundle.putBundle(keyForField(FIELD_SESSION_COMMANDS), sessionCommands.toBundle()); + bundle.putInt(FIELD_LIBRARY_VERSION, libraryVersion); + BundleCompat.putBinder(bundle, FIELD_SESSION_BINDER, sessionBinder.asBinder()); + bundle.putParcelable(FIELD_SESSION_ACTIVITY, sessionActivity); + bundle.putBundle(FIELD_SESSION_COMMANDS, sessionCommands.toBundle()); + bundle.putBundle(FIELD_PLAYER_COMMANDS_FROM_SESSION, playerCommandsFromSession.toBundle()); + bundle.putBundle(FIELD_PLAYER_COMMANDS_FROM_PLAYER, playerCommandsFromPlayer.toBundle()); + bundle.putBundle(FIELD_TOKEN_EXTRAS, tokenExtras); bundle.putBundle( - keyForField(FIELD_PLAYER_COMMANDS_FROM_SESSION), playerCommandsFromSession.toBundle()); - bundle.putBundle( - keyForField(FIELD_PLAYER_COMMANDS_FROM_PLAYER), playerCommandsFromPlayer.toBundle()); - bundle.putBundle(keyForField(FIELD_TOKEN_EXTRAS), tokenExtras); - bundle.putBundle( - keyForField(FIELD_PLAYER_INFO), + FIELD_PLAYER_INFO, playerInfo.toBundle( /* excludeMediaItems= */ !playerCommandsFromPlayer.contains(Player.COMMAND_GET_TIMELINE) || !playerCommandsFromSession.contains(Player.COMMAND_GET_TIMELINE), @@ -130,7 +107,7 @@ public Bundle toBundle() { /* excludeTimeline= */ false, /* excludeTracks= */ !playerCommandsFromPlayer.contains(Player.COMMAND_GET_TRACKS) || !playerCommandsFromSession.contains(Player.COMMAND_GET_TRACKS))); - bundle.putInt(keyForField(FIELD_SESSION_INTERFACE_VERSION), sessionInterfaceVersion); + bundle.putInt(FIELD_SESSION_INTERFACE_VERSION, sessionInterfaceVersion); return bundle; } @@ -138,34 +115,30 @@ public Bundle toBundle() { public static final Creator CREATOR = ConnectionState::fromBundle; private static ConnectionState fromBundle(Bundle bundle) { - int libraryVersion = bundle.getInt(keyForField(FIELD_LIBRARY_VERSION), /* defaultValue= */ 0); + int libraryVersion = bundle.getInt(FIELD_LIBRARY_VERSION, /* defaultValue= */ 0); int sessionInterfaceVersion = - bundle.getInt(keyForField(FIELD_SESSION_INTERFACE_VERSION), /* defaultValue= */ 0); - IBinder sessionBinder = - checkNotNull(BundleCompat.getBinder(bundle, keyForField(FIELD_SESSION_BINDER))); - @Nullable - PendingIntent sessionActivity = bundle.getParcelable(keyForField(FIELD_SESSION_ACTIVITY)); - @Nullable Bundle sessionCommandsBundle = bundle.getBundle(keyForField(FIELD_SESSION_COMMANDS)); + bundle.getInt(FIELD_SESSION_INTERFACE_VERSION, /* defaultValue= */ 0); + IBinder sessionBinder = checkNotNull(BundleCompat.getBinder(bundle, FIELD_SESSION_BINDER)); + @Nullable PendingIntent sessionActivity = bundle.getParcelable(FIELD_SESSION_ACTIVITY); + @Nullable Bundle sessionCommandsBundle = bundle.getBundle(FIELD_SESSION_COMMANDS); SessionCommands sessionCommands = sessionCommandsBundle == null ? SessionCommands.EMPTY : SessionCommands.CREATOR.fromBundle(sessionCommandsBundle); @Nullable - Bundle playerCommandsFromPlayerBundle = - bundle.getBundle(keyForField(FIELD_PLAYER_COMMANDS_FROM_PLAYER)); + Bundle playerCommandsFromPlayerBundle = bundle.getBundle(FIELD_PLAYER_COMMANDS_FROM_PLAYER); Player.Commands playerCommandsFromPlayer = playerCommandsFromPlayerBundle == null ? Player.Commands.EMPTY : Player.Commands.CREATOR.fromBundle(playerCommandsFromPlayerBundle); @Nullable - Bundle playerCommandsFromSessionBundle = - bundle.getBundle(keyForField(FIELD_PLAYER_COMMANDS_FROM_SESSION)); + Bundle playerCommandsFromSessionBundle = bundle.getBundle(FIELD_PLAYER_COMMANDS_FROM_SESSION); Player.Commands playerCommandsFromSession = playerCommandsFromSessionBundle == null ? Player.Commands.EMPTY : Player.Commands.CREATOR.fromBundle(playerCommandsFromSessionBundle); - @Nullable Bundle tokenExtras = bundle.getBundle(keyForField(FIELD_TOKEN_EXTRAS)); - @Nullable Bundle playerInfoBundle = bundle.getBundle(keyForField(FIELD_PLAYER_INFO)); + @Nullable Bundle tokenExtras = bundle.getBundle(FIELD_TOKEN_EXTRAS); + @Nullable Bundle playerInfoBundle = bundle.getBundle(FIELD_PLAYER_INFO); PlayerInfo playerInfo = playerInfoBundle == null ? PlayerInfo.DEFAULT @@ -181,8 +154,4 @@ private static ConnectionState fromBundle(Bundle bundle) { tokenExtras == null ? Bundle.EMPTY : tokenExtras, playerInfo); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java index 471d0200eb0..1d123d11073 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java @@ -34,6 +34,7 @@ import androidx.media3.common.MediaMetadata; import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import androidx.media3.session.MediaLibraryService.LibraryParams; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; @@ -262,23 +263,11 @@ private static void verifyMediaItem(MediaItem item) { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_RESULT_CODE, - FIELD_COMPLETION_TIME_MS, - FIELD_PARAMS, - FIELD_VALUE, - FIELD_VALUE_TYPE - }) - private @interface FieldNumber {} - - private static final int FIELD_RESULT_CODE = 0; - private static final int FIELD_COMPLETION_TIME_MS = 1; - private static final int FIELD_PARAMS = 2; - private static final int FIELD_VALUE = 3; - private static final int FIELD_VALUE_TYPE = 4; + private static final String FIELD_RESULT_CODE = Util.intToStringMaxRadix(0); + private static final String FIELD_COMPLETION_TIME_MS = Util.intToStringMaxRadix(1); + private static final String FIELD_PARAMS = Util.intToStringMaxRadix(2); + private static final String FIELD_VALUE = Util.intToStringMaxRadix(3); + private static final String FIELD_VALUE_TYPE = Util.intToStringMaxRadix(4); // Casting V to ImmutableList is safe if valueType == VALUE_TYPE_ITEM_LIST. @SuppressWarnings("unchecked") @@ -286,24 +275,24 @@ private static void verifyMediaItem(MediaItem item) { @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_RESULT_CODE), resultCode); - bundle.putLong(keyForField(FIELD_COMPLETION_TIME_MS), completionTimeMs); + bundle.putInt(FIELD_RESULT_CODE, resultCode); + bundle.putLong(FIELD_COMPLETION_TIME_MS, completionTimeMs); if (params != null) { - bundle.putBundle(keyForField(FIELD_PARAMS), params.toBundle()); + bundle.putBundle(FIELD_PARAMS, params.toBundle()); } - bundle.putInt(keyForField(FIELD_VALUE_TYPE), valueType); + bundle.putInt(FIELD_VALUE_TYPE, valueType); if (value == null) { return bundle; } switch (valueType) { case VALUE_TYPE_ITEM: - bundle.putBundle(keyForField(FIELD_VALUE), ((MediaItem) value).toBundle()); + bundle.putBundle(FIELD_VALUE, ((MediaItem) value).toBundle()); break; case VALUE_TYPE_ITEM_LIST: BundleCompat.putBinder( bundle, - keyForField(FIELD_VALUE), + FIELD_VALUE, new BundleListRetriever(BundleableUtil.toBundleList((ImmutableList) value))); break; case VALUE_TYPE_VOID: @@ -367,27 +356,24 @@ private static LibraryResult fromUnknownBundle(Bundle bundle) { */ private static LibraryResult fromBundle( Bundle bundle, @Nullable @ValueType Integer expectedType) { - int resultCode = - bundle.getInt(keyForField(FIELD_RESULT_CODE), /* defaultValue= */ RESULT_SUCCESS); + int resultCode = bundle.getInt(FIELD_RESULT_CODE, /* defaultValue= */ RESULT_SUCCESS); long completionTimeMs = - bundle.getLong( - keyForField(FIELD_COMPLETION_TIME_MS), - /* defaultValue= */ SystemClock.elapsedRealtime()); - @Nullable Bundle paramsBundle = bundle.getBundle(keyForField(FIELD_PARAMS)); + bundle.getLong(FIELD_COMPLETION_TIME_MS, /* defaultValue= */ SystemClock.elapsedRealtime()); + @Nullable Bundle paramsBundle = bundle.getBundle(FIELD_PARAMS); @Nullable MediaLibraryService.LibraryParams params = paramsBundle == null ? null : LibraryParams.CREATOR.fromBundle(paramsBundle); - @ValueType int valueType = bundle.getInt(keyForField(FIELD_VALUE_TYPE)); + @ValueType int valueType = bundle.getInt(FIELD_VALUE_TYPE); @Nullable Object value; switch (valueType) { case VALUE_TYPE_ITEM: checkState(expectedType == null || expectedType == VALUE_TYPE_ITEM); - @Nullable Bundle valueBundle = bundle.getBundle(keyForField(FIELD_VALUE)); + @Nullable Bundle valueBundle = bundle.getBundle(FIELD_VALUE); value = valueBundle == null ? null : MediaItem.CREATOR.fromBundle(valueBundle); break; case VALUE_TYPE_ITEM_LIST: checkState(expectedType == null || expectedType == VALUE_TYPE_ITEM_LIST); - @Nullable IBinder valueRetriever = BundleCompat.getBinder(bundle, keyForField(FIELD_VALUE)); + @Nullable IBinder valueRetriever = BundleCompat.getBinder(bundle, FIELD_VALUE); value = valueRetriever == null ? null @@ -405,10 +391,6 @@ private static LibraryResult fromBundle( return new LibraryResult<>(resultCode, completionTimeMs, params, value, VALUE_TYPE_ITEM_LIST); } - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } - @Documented @Retention(RetentionPolicy.SOURCE) @Target(TYPE_USE) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java index 48442529ff7..6a96d47a7d3 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java @@ -19,7 +19,6 @@ import static androidx.media3.common.util.Assertions.checkNotEmpty; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED; -import static java.lang.annotation.ElementType.TYPE_USE; import android.app.PendingIntent; import android.content.Context; @@ -27,7 +26,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.IBinder; -import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.annotation.Nullable; import androidx.media.MediaSessionManager.RemoteUserInfo; @@ -36,15 +34,12 @@ import androidx.media3.common.MediaMetadata; import androidx.media3.common.Player; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import androidx.media3.session.MediaSession.ControllerInfo; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** * Superclass to be extended by services hosting {@link MediaLibrarySession media library sessions}. @@ -666,30 +661,19 @@ public LibraryParams build() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_EXTRAS, - FIELD_RECENT, - FIELD_OFFLINE, - FIELD_SUGGESTED, - }) - private @interface FieldNumber {} - - private static final int FIELD_EXTRAS = 0; - private static final int FIELD_RECENT = 1; - private static final int FIELD_OFFLINE = 2; - private static final int FIELD_SUGGESTED = 3; + private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(0); + private static final String FIELD_RECENT = Util.intToStringMaxRadix(1); + private static final String FIELD_OFFLINE = Util.intToStringMaxRadix(2); + private static final String FIELD_SUGGESTED = Util.intToStringMaxRadix(3); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putBundle(keyForField(FIELD_EXTRAS), extras); - bundle.putBoolean(keyForField(FIELD_RECENT), isRecent); - bundle.putBoolean(keyForField(FIELD_OFFLINE), isOffline); - bundle.putBoolean(keyForField(FIELD_SUGGESTED), isSuggested); + bundle.putBundle(FIELD_EXTRAS, extras); + bundle.putBoolean(FIELD_RECENT, isRecent); + bundle.putBoolean(FIELD_OFFLINE, isOffline); + bundle.putBoolean(FIELD_SUGGESTED, isSuggested); return bundle; } @@ -697,17 +681,12 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = LibraryParams::fromBundle; private static LibraryParams fromBundle(Bundle bundle) { - @Nullable Bundle extras = bundle.getBundle(keyForField(FIELD_EXTRAS)); - boolean recent = bundle.getBoolean(keyForField(FIELD_RECENT), /* defaultValue= */ false); - boolean offline = bundle.getBoolean(keyForField(FIELD_OFFLINE), /* defaultValue= */ false); - boolean suggested = - bundle.getBoolean(keyForField(FIELD_SUGGESTED), /* defaultValue= */ false); + @Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS); + boolean recent = bundle.getBoolean(FIELD_RECENT, /* defaultValue= */ false); + boolean offline = bundle.getBoolean(FIELD_OFFLINE, /* defaultValue= */ false); + boolean suggested = bundle.getBoolean(FIELD_SUGGESTED, /* defaultValue= */ false); return new LibraryParams(extras == null ? Bundle.EMPTY : extras, recent, offline, suggested); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java index 79c780c36e1..dfb94e8a3db 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java @@ -20,13 +20,11 @@ import static androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_NONE; import static androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST; import static androidx.media3.common.Player.STATE_IDLE; -import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; import android.os.SystemClock; import androidx.annotation.CheckResult; import androidx.annotation.FloatRange; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.AudioAttributes; import androidx.media3.common.Bundleable; @@ -47,12 +45,9 @@ import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.base.Objects; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** * Information about the player that {@link MediaSession} uses to send its state to {@link @@ -83,36 +78,24 @@ public BundlingExclusions(boolean isTimelineExcluded, boolean areCurrentTracksEx // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_IS_TIMELINE_EXCLUDED, FIELD_ARE_CURRENT_TRACKS_EXCLUDED}) - private @interface FieldNumber {} - - private static final int FIELD_IS_TIMELINE_EXCLUDED = 0; - private static final int FIELD_ARE_CURRENT_TRACKS_EXCLUDED = 1; + private static final String FIELD_IS_TIMELINE_EXCLUDED = Util.intToStringMaxRadix(0); + private static final String FIELD_ARE_CURRENT_TRACKS_EXCLUDED = Util.intToStringMaxRadix(1); // Next field key = 2 @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putBoolean(keyForField(FIELD_IS_TIMELINE_EXCLUDED), isTimelineExcluded); - bundle.putBoolean(keyForField(FIELD_ARE_CURRENT_TRACKS_EXCLUDED), areCurrentTracksExcluded); + bundle.putBoolean(FIELD_IS_TIMELINE_EXCLUDED, isTimelineExcluded); + bundle.putBoolean(FIELD_ARE_CURRENT_TRACKS_EXCLUDED, areCurrentTracksExcluded); return bundle; } public static final Creator CREATOR = bundle -> new BundlingExclusions( - bundle.getBoolean( - keyForField(FIELD_IS_TIMELINE_EXCLUDED), /* defaultValue= */ false), - bundle.getBoolean( - keyForField(FIELD_ARE_CURRENT_TRACKS_EXCLUDED), /* defaultValue= */ false)); - - private static String keyForField(@BundlingExclusions.FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } + bundle.getBoolean(FIELD_IS_TIMELINE_EXCLUDED, /* defaultValue= */ false), + bundle.getBoolean(FIELD_ARE_CURRENT_TRACKS_EXCLUDED, /* defaultValue= */ false)); @Override public boolean equals(@Nullable Object o) { @@ -783,73 +766,36 @@ private boolean isPlaying( // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_PLAYBACK_PARAMETERS, - FIELD_REPEAT_MODE, - FIELD_SHUFFLE_MODE_ENABLED, - FIELD_TIMELINE, - FIELD_VIDEO_SIZE, - FIELD_PLAYLIST_METADATA, - FIELD_VOLUME, - FIELD_AUDIO_ATTRIBUTES, - FIELD_DEVICE_INFO, - FIELD_DEVICE_VOLUME, - FIELD_DEVICE_MUTED, - FIELD_PLAY_WHEN_READY, - FIELD_PLAY_WHEN_READY_CHANGED_REASON, - FIELD_PLAYBACK_SUPPRESSION_REASON, - FIELD_PLAYBACK_STATE, - FIELD_IS_PLAYING, - FIELD_IS_LOADING, - FIELD_PLAYBACK_ERROR, - FIELD_SESSION_POSITION_INFO, - FIELD_MEDIA_ITEM_TRANSITION_REASON, - FIELD_OLD_POSITION_INFO, - FIELD_NEW_POSITION_INFO, - FIELD_DISCONTINUITY_REASON, - FIELD_CUE_GROUP, - FIELD_MEDIA_METADATA, - FIELD_SEEK_BACK_INCREMENT_MS, - FIELD_SEEK_FORWARD_INCREMENT_MS, - FIELD_MAX_SEEK_TO_PREVIOUS_POSITION_MS, - FIELD_TRACK_SELECTION_PARAMETERS, - FIELD_CURRENT_TRACKS, - }) - private @interface FieldNumber {} - - private static final int FIELD_PLAYBACK_PARAMETERS = 1; - private static final int FIELD_REPEAT_MODE = 2; - private static final int FIELD_SHUFFLE_MODE_ENABLED = 3; - private static final int FIELD_TIMELINE = 4; - private static final int FIELD_VIDEO_SIZE = 5; - private static final int FIELD_PLAYLIST_METADATA = 6; - private static final int FIELD_VOLUME = 7; - private static final int FIELD_AUDIO_ATTRIBUTES = 8; - private static final int FIELD_DEVICE_INFO = 9; - private static final int FIELD_DEVICE_VOLUME = 10; - private static final int FIELD_DEVICE_MUTED = 11; - private static final int FIELD_PLAY_WHEN_READY = 12; - private static final int FIELD_PLAY_WHEN_READY_CHANGED_REASON = 13; - private static final int FIELD_PLAYBACK_SUPPRESSION_REASON = 14; - private static final int FIELD_PLAYBACK_STATE = 15; - private static final int FIELD_IS_PLAYING = 16; - private static final int FIELD_IS_LOADING = 17; - private static final int FIELD_PLAYBACK_ERROR = 18; - private static final int FIELD_SESSION_POSITION_INFO = 19; - private static final int FIELD_MEDIA_ITEM_TRANSITION_REASON = 20; - private static final int FIELD_OLD_POSITION_INFO = 21; - private static final int FIELD_NEW_POSITION_INFO = 22; - private static final int FIELD_DISCONTINUITY_REASON = 23; - private static final int FIELD_CUE_GROUP = 24; - private static final int FIELD_MEDIA_METADATA = 25; - private static final int FIELD_SEEK_BACK_INCREMENT_MS = 26; - private static final int FIELD_SEEK_FORWARD_INCREMENT_MS = 27; - private static final int FIELD_MAX_SEEK_TO_PREVIOUS_POSITION_MS = 28; - private static final int FIELD_TRACK_SELECTION_PARAMETERS = 29; - private static final int FIELD_CURRENT_TRACKS = 30; + private static final String FIELD_PLAYBACK_PARAMETERS = Util.intToStringMaxRadix(1); + private static final String FIELD_REPEAT_MODE = Util.intToStringMaxRadix(2); + private static final String FIELD_SHUFFLE_MODE_ENABLED = Util.intToStringMaxRadix(3); + private static final String FIELD_TIMELINE = Util.intToStringMaxRadix(4); + private static final String FIELD_VIDEO_SIZE = Util.intToStringMaxRadix(5); + private static final String FIELD_PLAYLIST_METADATA = Util.intToStringMaxRadix(6); + private static final String FIELD_VOLUME = Util.intToStringMaxRadix(7); + private static final String FIELD_AUDIO_ATTRIBUTES = Util.intToStringMaxRadix(8); + private static final String FIELD_DEVICE_INFO = Util.intToStringMaxRadix(9); + private static final String FIELD_DEVICE_VOLUME = Util.intToStringMaxRadix(10); + private static final String FIELD_DEVICE_MUTED = Util.intToStringMaxRadix(11); + private static final String FIELD_PLAY_WHEN_READY = Util.intToStringMaxRadix(12); + private static final String FIELD_PLAY_WHEN_READY_CHANGED_REASON = Util.intToStringMaxRadix(13); + private static final String FIELD_PLAYBACK_SUPPRESSION_REASON = Util.intToStringMaxRadix(14); + private static final String FIELD_PLAYBACK_STATE = Util.intToStringMaxRadix(15); + private static final String FIELD_IS_PLAYING = Util.intToStringMaxRadix(16); + private static final String FIELD_IS_LOADING = Util.intToStringMaxRadix(17); + private static final String FIELD_PLAYBACK_ERROR = Util.intToStringMaxRadix(18); + private static final String FIELD_SESSION_POSITION_INFO = Util.intToStringMaxRadix(19); + private static final String FIELD_MEDIA_ITEM_TRANSITION_REASON = Util.intToStringMaxRadix(20); + private static final String FIELD_OLD_POSITION_INFO = Util.intToStringMaxRadix(21); + private static final String FIELD_NEW_POSITION_INFO = Util.intToStringMaxRadix(22); + private static final String FIELD_DISCONTINUITY_REASON = Util.intToStringMaxRadix(23); + private static final String FIELD_CUE_GROUP = Util.intToStringMaxRadix(24); + private static final String FIELD_MEDIA_METADATA = Util.intToStringMaxRadix(25); + private static final String FIELD_SEEK_BACK_INCREMENT_MS = Util.intToStringMaxRadix(26); + private static final String FIELD_SEEK_FORWARD_INCREMENT_MS = Util.intToStringMaxRadix(27); + private static final String FIELD_MAX_SEEK_TO_PREVIOUS_POSITION_MS = Util.intToStringMaxRadix(28); + private static final String FIELD_TRACK_SELECTION_PARAMETERS = Util.intToStringMaxRadix(29); + private static final String FIELD_CURRENT_TRACKS = Util.intToStringMaxRadix(30); // Next field key = 31 public Bundle toBundle( @@ -860,48 +806,46 @@ public Bundle toBundle( boolean excludeTracks) { Bundle bundle = new Bundle(); if (playerError != null) { - bundle.putBundle(keyForField(FIELD_PLAYBACK_ERROR), playerError.toBundle()); - } - bundle.putInt(keyForField(FIELD_MEDIA_ITEM_TRANSITION_REASON), mediaItemTransitionReason); - bundle.putBundle(keyForField(FIELD_SESSION_POSITION_INFO), sessionPositionInfo.toBundle()); - bundle.putBundle(keyForField(FIELD_OLD_POSITION_INFO), oldPositionInfo.toBundle()); - bundle.putBundle(keyForField(FIELD_NEW_POSITION_INFO), newPositionInfo.toBundle()); - bundle.putInt(keyForField(FIELD_DISCONTINUITY_REASON), discontinuityReason); - bundle.putBundle(keyForField(FIELD_PLAYBACK_PARAMETERS), playbackParameters.toBundle()); - bundle.putInt(keyForField(FIELD_REPEAT_MODE), repeatMode); - bundle.putBoolean(keyForField(FIELD_SHUFFLE_MODE_ENABLED), shuffleModeEnabled); + bundle.putBundle(FIELD_PLAYBACK_ERROR, playerError.toBundle()); + } + bundle.putInt(FIELD_MEDIA_ITEM_TRANSITION_REASON, mediaItemTransitionReason); + bundle.putBundle(FIELD_SESSION_POSITION_INFO, sessionPositionInfo.toBundle()); + bundle.putBundle(FIELD_OLD_POSITION_INFO, oldPositionInfo.toBundle()); + bundle.putBundle(FIELD_NEW_POSITION_INFO, newPositionInfo.toBundle()); + bundle.putInt(FIELD_DISCONTINUITY_REASON, discontinuityReason); + bundle.putBundle(FIELD_PLAYBACK_PARAMETERS, playbackParameters.toBundle()); + bundle.putInt(FIELD_REPEAT_MODE, repeatMode); + bundle.putBoolean(FIELD_SHUFFLE_MODE_ENABLED, shuffleModeEnabled); if (!excludeTimeline) { - bundle.putBundle(keyForField(FIELD_TIMELINE), timeline.toBundle(excludeMediaItems)); + bundle.putBundle(FIELD_TIMELINE, timeline.toBundle(excludeMediaItems)); } - bundle.putBundle(keyForField(FIELD_VIDEO_SIZE), videoSize.toBundle()); + bundle.putBundle(FIELD_VIDEO_SIZE, videoSize.toBundle()); if (!excludeMediaItemsMetadata) { - bundle.putBundle(keyForField(FIELD_PLAYLIST_METADATA), playlistMetadata.toBundle()); + bundle.putBundle(FIELD_PLAYLIST_METADATA, playlistMetadata.toBundle()); } - bundle.putFloat(keyForField(FIELD_VOLUME), volume); - bundle.putBundle(keyForField(FIELD_AUDIO_ATTRIBUTES), audioAttributes.toBundle()); + bundle.putFloat(FIELD_VOLUME, volume); + bundle.putBundle(FIELD_AUDIO_ATTRIBUTES, audioAttributes.toBundle()); if (!excludeCues) { - bundle.putBundle(keyForField(FIELD_CUE_GROUP), cueGroup.toBundle()); - } - bundle.putBundle(keyForField(FIELD_DEVICE_INFO), deviceInfo.toBundle()); - bundle.putInt(keyForField(FIELD_DEVICE_VOLUME), deviceVolume); - bundle.putBoolean(keyForField(FIELD_DEVICE_MUTED), deviceMuted); - bundle.putBoolean(keyForField(FIELD_PLAY_WHEN_READY), playWhenReady); - bundle.putInt(keyForField(FIELD_PLAYBACK_SUPPRESSION_REASON), playbackSuppressionReason); - bundle.putInt(keyForField(FIELD_PLAYBACK_STATE), playbackState); - bundle.putBoolean(keyForField(FIELD_IS_PLAYING), isPlaying); - bundle.putBoolean(keyForField(FIELD_IS_LOADING), isLoading); + bundle.putBundle(FIELD_CUE_GROUP, cueGroup.toBundle()); + } + bundle.putBundle(FIELD_DEVICE_INFO, deviceInfo.toBundle()); + bundle.putInt(FIELD_DEVICE_VOLUME, deviceVolume); + bundle.putBoolean(FIELD_DEVICE_MUTED, deviceMuted); + bundle.putBoolean(FIELD_PLAY_WHEN_READY, playWhenReady); + bundle.putInt(FIELD_PLAYBACK_SUPPRESSION_REASON, playbackSuppressionReason); + bundle.putInt(FIELD_PLAYBACK_STATE, playbackState); + bundle.putBoolean(FIELD_IS_PLAYING, isPlaying); + bundle.putBoolean(FIELD_IS_LOADING, isLoading); bundle.putBundle( - keyForField(FIELD_MEDIA_METADATA), + FIELD_MEDIA_METADATA, excludeMediaItems ? MediaMetadata.EMPTY.toBundle() : mediaMetadata.toBundle()); - bundle.putLong(keyForField(FIELD_SEEK_BACK_INCREMENT_MS), seekBackIncrementMs); - bundle.putLong(keyForField(FIELD_SEEK_FORWARD_INCREMENT_MS), seekForwardIncrementMs); - bundle.putLong( - keyForField(FIELD_MAX_SEEK_TO_PREVIOUS_POSITION_MS), maxSeekToPreviousPositionMs); + bundle.putLong(FIELD_SEEK_BACK_INCREMENT_MS, seekBackIncrementMs); + bundle.putLong(FIELD_SEEK_FORWARD_INCREMENT_MS, seekForwardIncrementMs); + bundle.putLong(FIELD_MAX_SEEK_TO_PREVIOUS_POSITION_MS, maxSeekToPreviousPositionMs); if (!excludeTracks) { - bundle.putBundle(keyForField(FIELD_CURRENT_TRACKS), currentTracks.toBundle()); + bundle.putBundle(FIELD_CURRENT_TRACKS, currentTracks.toBundle()); } - bundle.putBundle( - keyForField(FIELD_TRACK_SELECTION_PARAMETERS), trackSelectionParameters.toBundle()); + bundle.putBundle(FIELD_TRACK_SELECTION_PARAMETERS, trackSelectionParameters.toBundle()); return bundle; } @@ -920,107 +864,96 @@ public Bundle toBundle() { public static final Creator CREATOR = PlayerInfo::fromBundle; private static PlayerInfo fromBundle(Bundle bundle) { - @Nullable Bundle playerErrorBundle = bundle.getBundle(keyForField(FIELD_PLAYBACK_ERROR)); + @Nullable Bundle playerErrorBundle = bundle.getBundle(FIELD_PLAYBACK_ERROR); @Nullable PlaybackException playerError = playerErrorBundle == null ? null : PlaybackException.CREATOR.fromBundle(playerErrorBundle); int mediaItemTransitionReason = - bundle.getInt( - keyForField(FIELD_MEDIA_ITEM_TRANSITION_REASON), MEDIA_ITEM_TRANSITION_REASON_REPEAT); - @Nullable - Bundle sessionPositionInfoBundle = bundle.getBundle(keyForField(FIELD_SESSION_POSITION_INFO)); + bundle.getInt(FIELD_MEDIA_ITEM_TRANSITION_REASON, MEDIA_ITEM_TRANSITION_REASON_REPEAT); + @Nullable Bundle sessionPositionInfoBundle = bundle.getBundle(FIELD_SESSION_POSITION_INFO); SessionPositionInfo sessionPositionInfo = sessionPositionInfoBundle == null ? SessionPositionInfo.DEFAULT : SessionPositionInfo.CREATOR.fromBundle(sessionPositionInfoBundle); - @Nullable Bundle oldPositionInfoBundle = bundle.getBundle(keyForField(FIELD_OLD_POSITION_INFO)); + @Nullable Bundle oldPositionInfoBundle = bundle.getBundle(FIELD_OLD_POSITION_INFO); PositionInfo oldPositionInfo = oldPositionInfoBundle == null ? SessionPositionInfo.DEFAULT_POSITION_INFO : PositionInfo.CREATOR.fromBundle(oldPositionInfoBundle); - @Nullable Bundle newPositionInfoBundle = bundle.getBundle(keyForField(FIELD_NEW_POSITION_INFO)); + @Nullable Bundle newPositionInfoBundle = bundle.getBundle(FIELD_NEW_POSITION_INFO); PositionInfo newPositionInfo = newPositionInfoBundle == null ? SessionPositionInfo.DEFAULT_POSITION_INFO : PositionInfo.CREATOR.fromBundle(newPositionInfoBundle); int discontinuityReason = - bundle.getInt( - keyForField(FIELD_DISCONTINUITY_REASON), DISCONTINUITY_REASON_AUTO_TRANSITION); - @Nullable - Bundle playbackParametersBundle = bundle.getBundle(keyForField(FIELD_PLAYBACK_PARAMETERS)); + bundle.getInt(FIELD_DISCONTINUITY_REASON, DISCONTINUITY_REASON_AUTO_TRANSITION); + @Nullable Bundle playbackParametersBundle = bundle.getBundle(FIELD_PLAYBACK_PARAMETERS); PlaybackParameters playbackParameters = playbackParametersBundle == null ? PlaybackParameters.DEFAULT : PlaybackParameters.CREATOR.fromBundle(playbackParametersBundle); @Player.RepeatMode - int repeatMode = - bundle.getInt(keyForField(FIELD_REPEAT_MODE), /* defaultValue= */ Player.REPEAT_MODE_OFF); + int repeatMode = bundle.getInt(FIELD_REPEAT_MODE, /* defaultValue= */ Player.REPEAT_MODE_OFF); boolean shuffleModeEnabled = - bundle.getBoolean(keyForField(FIELD_SHUFFLE_MODE_ENABLED), /* defaultValue= */ false); - @Nullable Bundle timelineBundle = bundle.getBundle(keyForField(FIELD_TIMELINE)); + bundle.getBoolean(FIELD_SHUFFLE_MODE_ENABLED, /* defaultValue= */ false); + @Nullable Bundle timelineBundle = bundle.getBundle(FIELD_TIMELINE); Timeline timeline = timelineBundle == null ? Timeline.EMPTY : Timeline.CREATOR.fromBundle(timelineBundle); - @Nullable Bundle videoSizeBundle = bundle.getBundle(keyForField(FIELD_VIDEO_SIZE)); + @Nullable Bundle videoSizeBundle = bundle.getBundle(FIELD_VIDEO_SIZE); VideoSize videoSize = videoSizeBundle == null ? VideoSize.UNKNOWN : VideoSize.CREATOR.fromBundle(videoSizeBundle); - @Nullable - Bundle playlistMetadataBundle = bundle.getBundle(keyForField(FIELD_PLAYLIST_METADATA)); + @Nullable Bundle playlistMetadataBundle = bundle.getBundle(FIELD_PLAYLIST_METADATA); MediaMetadata playlistMetadata = playlistMetadataBundle == null ? MediaMetadata.EMPTY : MediaMetadata.CREATOR.fromBundle(playlistMetadataBundle); - float volume = bundle.getFloat(keyForField(FIELD_VOLUME), /* defaultValue= */ 1); - @Nullable Bundle audioAttributesBundle = bundle.getBundle(keyForField(FIELD_AUDIO_ATTRIBUTES)); + float volume = bundle.getFloat(FIELD_VOLUME, /* defaultValue= */ 1); + @Nullable Bundle audioAttributesBundle = bundle.getBundle(FIELD_AUDIO_ATTRIBUTES); AudioAttributes audioAttributes = audioAttributesBundle == null ? AudioAttributes.DEFAULT : AudioAttributes.CREATOR.fromBundle(audioAttributesBundle); - @Nullable Bundle cueGroupBundle = bundle.getBundle(keyForField(FIELD_CUE_GROUP)); + @Nullable Bundle cueGroupBundle = bundle.getBundle(FIELD_CUE_GROUP); CueGroup cueGroup = cueGroupBundle == null ? CueGroup.EMPTY_TIME_ZERO : CueGroup.CREATOR.fromBundle(cueGroupBundle); - @Nullable Bundle deviceInfoBundle = bundle.getBundle(keyForField(FIELD_DEVICE_INFO)); + @Nullable Bundle deviceInfoBundle = bundle.getBundle(FIELD_DEVICE_INFO); DeviceInfo deviceInfo = deviceInfoBundle == null ? DeviceInfo.UNKNOWN : DeviceInfo.CREATOR.fromBundle(deviceInfoBundle); - int deviceVolume = bundle.getInt(keyForField(FIELD_DEVICE_VOLUME), /* defaultValue= */ 0); - boolean deviceMuted = - bundle.getBoolean(keyForField(FIELD_DEVICE_MUTED), /* defaultValue= */ false); - boolean playWhenReady = - bundle.getBoolean(keyForField(FIELD_PLAY_WHEN_READY), /* defaultValue= */ false); + int deviceVolume = bundle.getInt(FIELD_DEVICE_VOLUME, /* defaultValue= */ 0); + boolean deviceMuted = bundle.getBoolean(FIELD_DEVICE_MUTED, /* defaultValue= */ false); + boolean playWhenReady = bundle.getBoolean(FIELD_PLAY_WHEN_READY, /* defaultValue= */ false); int playWhenReadyChangedReason = bundle.getInt( - keyForField(FIELD_PLAY_WHEN_READY_CHANGED_REASON), + FIELD_PLAY_WHEN_READY_CHANGED_REASON, /* defaultValue= */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); @Player.PlaybackSuppressionReason int playbackSuppressionReason = bundle.getInt( - keyForField(FIELD_PLAYBACK_SUPPRESSION_REASON), + FIELD_PLAYBACK_SUPPRESSION_REASON, /* defaultValue= */ PLAYBACK_SUPPRESSION_REASON_NONE); @Player.State - int playbackState = - bundle.getInt(keyForField(FIELD_PLAYBACK_STATE), /* defaultValue= */ STATE_IDLE); - boolean isPlaying = bundle.getBoolean(keyForField(FIELD_IS_PLAYING), /* defaultValue= */ false); - boolean isLoading = bundle.getBoolean(keyForField(FIELD_IS_LOADING), /* defaultValue= */ false); - @Nullable Bundle mediaMetadataBundle = bundle.getBundle(keyForField(FIELD_MEDIA_METADATA)); + int playbackState = bundle.getInt(FIELD_PLAYBACK_STATE, /* defaultValue= */ STATE_IDLE); + boolean isPlaying = bundle.getBoolean(FIELD_IS_PLAYING, /* defaultValue= */ false); + boolean isLoading = bundle.getBoolean(FIELD_IS_LOADING, /* defaultValue= */ false); + @Nullable Bundle mediaMetadataBundle = bundle.getBundle(FIELD_MEDIA_METADATA); MediaMetadata mediaMetadata = mediaMetadataBundle == null ? MediaMetadata.EMPTY : MediaMetadata.CREATOR.fromBundle(mediaMetadataBundle); - long seekBackIncrementMs = - bundle.getLong(keyForField(FIELD_SEEK_BACK_INCREMENT_MS), /* defaultValue= */ 0); + long seekBackIncrementMs = bundle.getLong(FIELD_SEEK_BACK_INCREMENT_MS, /* defaultValue= */ 0); long seekForwardIncrementMs = - bundle.getLong(keyForField(FIELD_SEEK_FORWARD_INCREMENT_MS), /* defaultValue= */ 0); + bundle.getLong(FIELD_SEEK_FORWARD_INCREMENT_MS, /* defaultValue= */ 0); long maxSeekToPreviousPosition = - bundle.getLong(keyForField(FIELD_MAX_SEEK_TO_PREVIOUS_POSITION_MS), /* defaultValue= */ 0); - Bundle currentTracksBundle = bundle.getBundle(keyForField(FIELD_CURRENT_TRACKS)); + bundle.getLong(FIELD_MAX_SEEK_TO_PREVIOUS_POSITION_MS, /* defaultValue= */ 0); + Bundle currentTracksBundle = bundle.getBundle(FIELD_CURRENT_TRACKS); Tracks currentTracks = currentTracksBundle == null ? Tracks.EMPTY : Tracks.CREATOR.fromBundle(currentTracksBundle); @Nullable - Bundle trackSelectionParametersBundle = - bundle.getBundle(keyForField(FIELD_TRACK_SELECTION_PARAMETERS)); + Bundle trackSelectionParametersBundle = bundle.getBundle(FIELD_TRACK_SELECTION_PARAMETERS); TrackSelectionParameters trackSelectionParameters = trackSelectionParametersBundle == null ? TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT @@ -1057,8 +990,4 @@ private static PlayerInfo fromBundle(Bundle bundle) { currentTracks, trackSelectionParameters); } - - private static String keyForField(@PlayerInfo.FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java b/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java index 35ef3918265..c514af10c8b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java @@ -26,6 +26,7 @@ import androidx.media3.common.Bundleable; import androidx.media3.common.Rating; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import androidx.media3.session.MediaLibraryService.LibraryParams; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; @@ -165,24 +166,17 @@ public int hashCode() { } // Bundleable implementation. - - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_COMMAND_CODE, FIELD_CUSTOM_ACTION, FIELD_CUSTOM_EXTRAS}) - private @interface FieldNumber {} - - private static final int FIELD_COMMAND_CODE = 0; - private static final int FIELD_CUSTOM_ACTION = 1; - private static final int FIELD_CUSTOM_EXTRAS = 2; + private static final String FIELD_COMMAND_CODE = Util.intToStringMaxRadix(0); + private static final String FIELD_CUSTOM_ACTION = Util.intToStringMaxRadix(1); + private static final String FIELD_CUSTOM_EXTRAS = Util.intToStringMaxRadix(2); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_COMMAND_CODE), commandCode); - bundle.putString(keyForField(FIELD_CUSTOM_ACTION), customAction); - bundle.putBundle(keyForField(FIELD_CUSTOM_EXTRAS), customExtras); + bundle.putInt(FIELD_COMMAND_CODE, commandCode); + bundle.putString(FIELD_CUSTOM_ACTION, customAction); + bundle.putBundle(FIELD_CUSTOM_EXTRAS, customExtras); return bundle; } @@ -191,18 +185,14 @@ public Bundle toBundle() { public static final Creator CREATOR = bundle -> { int commandCode = - bundle.getInt(keyForField(FIELD_COMMAND_CODE), /* defaultValue= */ COMMAND_CODE_CUSTOM); + bundle.getInt(FIELD_COMMAND_CODE, /* defaultValue= */ COMMAND_CODE_CUSTOM); if (commandCode != COMMAND_CODE_CUSTOM) { return new SessionCommand(commandCode); } else { - String customAction = checkNotNull(bundle.getString(keyForField(FIELD_CUSTOM_ACTION))); - @Nullable Bundle customExtras = bundle.getBundle(keyForField(FIELD_CUSTOM_EXTRAS)); + String customAction = checkNotNull(bundle.getString(FIELD_CUSTOM_ACTION)); + @Nullable Bundle customExtras = bundle.getBundle(FIELD_CUSTOM_EXTRAS); return new SessionCommand( customAction, customExtras == null ? Bundle.EMPTY : customExtras); } }; - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionCommands.java b/libraries/session/src/main/java/androidx/media3/session/SessionCommands.java index d255bfa9706..ff8592dc313 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionCommands.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionCommands.java @@ -18,22 +18,17 @@ import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.session.SessionCommand.COMMAND_CODE_CUSTOM; -import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; import androidx.media3.common.Bundleable; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import androidx.media3.session.SessionCommand.CommandCode; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -235,13 +230,7 @@ private static boolean containsCommandCode( // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_SESSION_COMMANDS}) - private @interface FieldNumber {} - - private static final int FIELD_SESSION_COMMANDS = 0; + private static final String FIELD_SESSION_COMMANDS = Util.intToStringMaxRadix(0); @UnstableApi @Override @@ -251,7 +240,7 @@ public Bundle toBundle() { for (SessionCommand command : commands) { sessionCommandBundleList.add(command.toBundle()); } - bundle.putParcelableArrayList(keyForField(FIELD_SESSION_COMMANDS), sessionCommandBundleList); + bundle.putParcelableArrayList(FIELD_SESSION_COMMANDS, sessionCommandBundleList); return bundle; } @@ -261,7 +250,7 @@ public Bundle toBundle() { bundle -> { @Nullable ArrayList sessionCommandBundleList = - bundle.getParcelableArrayList(keyForField(FIELD_SESSION_COMMANDS)); + bundle.getParcelableArrayList(FIELD_SESSION_COMMANDS); if (sessionCommandBundleList == null) { Log.w(TAG, "Missing commands. Creating an empty SessionCommands"); return SessionCommands.EMPTY; @@ -273,8 +262,4 @@ public Bundle toBundle() { } return builder.build(); }; - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java b/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java index f2464b5af20..f8960d2a876 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java @@ -16,19 +16,14 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkArgument; -import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.Bundleable; import androidx.media3.common.C; import androidx.media3.common.Player.PositionInfo; +import androidx.media3.common.util.Util; import com.google.common.base.Objects; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** * Position information to be shared between session and controller. @@ -162,47 +157,30 @@ public String toString() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_POSITION_INFO, - FIELD_IS_PLAYING_AD, - FIELD_EVENT_TIME_MS, - FIELD_DURATION_MS, - FIELD_BUFFERED_POSITION_MS, - FIELD_BUFFERED_PERCENTAGE, - FIELD_TOTAL_BUFFERED_DURATION_MS, - FIELD_CURRENT_LIVE_OFFSET_MS, - FIELD_CONTENT_DURATION_MS, - FIELD_CONTENT_BUFFERED_POSITION_MS - }) - private @interface FieldNumber {} - - private static final int FIELD_POSITION_INFO = 0; - private static final int FIELD_IS_PLAYING_AD = 1; - private static final int FIELD_EVENT_TIME_MS = 2; - private static final int FIELD_DURATION_MS = 3; - private static final int FIELD_BUFFERED_POSITION_MS = 4; - private static final int FIELD_BUFFERED_PERCENTAGE = 5; - private static final int FIELD_TOTAL_BUFFERED_DURATION_MS = 6; - private static final int FIELD_CURRENT_LIVE_OFFSET_MS = 7; - private static final int FIELD_CONTENT_DURATION_MS = 8; - private static final int FIELD_CONTENT_BUFFERED_POSITION_MS = 9; + private static final String FIELD_POSITION_INFO = Util.intToStringMaxRadix(0); + private static final String FIELD_IS_PLAYING_AD = Util.intToStringMaxRadix(1); + private static final String FIELD_EVENT_TIME_MS = Util.intToStringMaxRadix(2); + private static final String FIELD_DURATION_MS = Util.intToStringMaxRadix(3); + private static final String FIELD_BUFFERED_POSITION_MS = Util.intToStringMaxRadix(4); + private static final String FIELD_BUFFERED_PERCENTAGE = Util.intToStringMaxRadix(5); + private static final String FIELD_TOTAL_BUFFERED_DURATION_MS = Util.intToStringMaxRadix(6); + private static final String FIELD_CURRENT_LIVE_OFFSET_MS = Util.intToStringMaxRadix(7); + private static final String FIELD_CONTENT_DURATION_MS = Util.intToStringMaxRadix(8); + private static final String FIELD_CONTENT_BUFFERED_POSITION_MS = Util.intToStringMaxRadix(9); @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putBundle(keyForField(FIELD_POSITION_INFO), positionInfo.toBundle()); - bundle.putBoolean(keyForField(FIELD_IS_PLAYING_AD), isPlayingAd); - bundle.putLong(keyForField(FIELD_EVENT_TIME_MS), eventTimeMs); - bundle.putLong(keyForField(FIELD_DURATION_MS), durationMs); - bundle.putLong(keyForField(FIELD_BUFFERED_POSITION_MS), bufferedPositionMs); - bundle.putInt(keyForField(FIELD_BUFFERED_PERCENTAGE), bufferedPercentage); - bundle.putLong(keyForField(FIELD_TOTAL_BUFFERED_DURATION_MS), totalBufferedDurationMs); - bundle.putLong(keyForField(FIELD_CURRENT_LIVE_OFFSET_MS), currentLiveOffsetMs); - bundle.putLong(keyForField(FIELD_CONTENT_DURATION_MS), contentDurationMs); - bundle.putLong(keyForField(FIELD_CONTENT_BUFFERED_POSITION_MS), contentBufferedPositionMs); + bundle.putBundle(FIELD_POSITION_INFO, positionInfo.toBundle()); + bundle.putBoolean(FIELD_IS_PLAYING_AD, isPlayingAd); + bundle.putLong(FIELD_EVENT_TIME_MS, eventTimeMs); + bundle.putLong(FIELD_DURATION_MS, durationMs); + bundle.putLong(FIELD_BUFFERED_POSITION_MS, bufferedPositionMs); + bundle.putInt(FIELD_BUFFERED_PERCENTAGE, bufferedPercentage); + bundle.putLong(FIELD_TOTAL_BUFFERED_DURATION_MS, totalBufferedDurationMs); + bundle.putLong(FIELD_CURRENT_LIVE_OFFSET_MS, currentLiveOffsetMs); + bundle.putLong(FIELD_CONTENT_DURATION_MS, contentDurationMs); + bundle.putLong(FIELD_CONTENT_BUFFERED_POSITION_MS, contentBufferedPositionMs); return bundle; } @@ -210,30 +188,25 @@ public Bundle toBundle() { public static final Creator CREATOR = SessionPositionInfo::fromBundle; private static SessionPositionInfo fromBundle(Bundle bundle) { - @Nullable Bundle positionInfoBundle = bundle.getBundle(keyForField(FIELD_POSITION_INFO)); + @Nullable Bundle positionInfoBundle = bundle.getBundle(FIELD_POSITION_INFO); PositionInfo positionInfo = positionInfoBundle == null ? DEFAULT_POSITION_INFO : PositionInfo.CREATOR.fromBundle(positionInfoBundle); - boolean isPlayingAd = - bundle.getBoolean(keyForField(FIELD_IS_PLAYING_AD), /* defaultValue= */ false); - long eventTimeMs = - bundle.getLong(keyForField(FIELD_EVENT_TIME_MS), /* defaultValue= */ C.TIME_UNSET); - long durationMs = - bundle.getLong(keyForField(FIELD_DURATION_MS), /* defaultValue= */ C.TIME_UNSET); + boolean isPlayingAd = bundle.getBoolean(FIELD_IS_PLAYING_AD, /* defaultValue= */ false); + long eventTimeMs = bundle.getLong(FIELD_EVENT_TIME_MS, /* defaultValue= */ C.TIME_UNSET); + long durationMs = bundle.getLong(FIELD_DURATION_MS, /* defaultValue= */ C.TIME_UNSET); long bufferedPositionMs = - bundle.getLong(keyForField(FIELD_BUFFERED_POSITION_MS), /* defaultValue= */ C.TIME_UNSET); - int bufferedPercentage = - bundle.getInt(keyForField(FIELD_BUFFERED_PERCENTAGE), /* defaultValue= */ 0); + bundle.getLong(FIELD_BUFFERED_POSITION_MS, /* defaultValue= */ C.TIME_UNSET); + int bufferedPercentage = bundle.getInt(FIELD_BUFFERED_PERCENTAGE, /* defaultValue= */ 0); long totalBufferedDurationMs = - bundle.getLong(keyForField(FIELD_TOTAL_BUFFERED_DURATION_MS), /* defaultValue= */ 0); + bundle.getLong(FIELD_TOTAL_BUFFERED_DURATION_MS, /* defaultValue= */ 0); long currentLiveOffsetMs = - bundle.getLong(keyForField(FIELD_CURRENT_LIVE_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET); + bundle.getLong(FIELD_CURRENT_LIVE_OFFSET_MS, /* defaultValue= */ C.TIME_UNSET); long contentDurationMs = - bundle.getLong(keyForField(FIELD_CONTENT_DURATION_MS), /* defaultValue= */ C.TIME_UNSET); + bundle.getLong(FIELD_CONTENT_DURATION_MS, /* defaultValue= */ C.TIME_UNSET); long contentBufferedPositionMs = - bundle.getLong( - keyForField(FIELD_CONTENT_BUFFERED_POSITION_MS), /* defaultValue= */ C.TIME_UNSET); + bundle.getLong(FIELD_CONTENT_BUFFERED_POSITION_MS, /* defaultValue= */ C.TIME_UNSET); return new SessionPositionInfo( positionInfo, @@ -247,8 +220,4 @@ private static SessionPositionInfo fromBundle(Bundle bundle) { contentDurationMs, contentBufferedPositionMs); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionResult.java b/libraries/session/src/main/java/androidx/media3/session/SessionResult.java index f4ca56cf39c..ebd389ba5dc 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionResult.java @@ -25,6 +25,7 @@ import androidx.annotation.Nullable; import androidx.media3.common.Bundleable; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.util.concurrent.ListenableFuture; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -175,23 +176,17 @@ private SessionResult(@Code int resultCode, Bundle extras, long completionTimeMs // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_RESULT_CODE, FIELD_EXTRAS, FIELD_COMPLETION_TIME_MS}) - private @interface FieldNumber {} - - private static final int FIELD_RESULT_CODE = 0; - private static final int FIELD_EXTRAS = 1; - private static final int FIELD_COMPLETION_TIME_MS = 2; + private static final String FIELD_RESULT_CODE = Util.intToStringMaxRadix(0); + private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(1); + private static final String FIELD_COMPLETION_TIME_MS = Util.intToStringMaxRadix(2); @UnstableApi @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_RESULT_CODE), resultCode); - bundle.putBundle(keyForField(FIELD_EXTRAS), extras); - bundle.putLong(keyForField(FIELD_COMPLETION_TIME_MS), completionTimeMs); + bundle.putInt(FIELD_RESULT_CODE, resultCode); + bundle.putBundle(FIELD_EXTRAS, extras); + bundle.putLong(FIELD_COMPLETION_TIME_MS, completionTimeMs); return bundle; } @@ -199,17 +194,10 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = SessionResult::fromBundle; private static SessionResult fromBundle(Bundle bundle) { - int resultCode = - bundle.getInt(keyForField(FIELD_RESULT_CODE), /* defaultValue= */ RESULT_ERROR_UNKNOWN); - @Nullable Bundle extras = bundle.getBundle(keyForField(FIELD_EXTRAS)); + int resultCode = bundle.getInt(FIELD_RESULT_CODE, /* defaultValue= */ RESULT_ERROR_UNKNOWN); + @Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS); long completionTimeMs = - bundle.getLong( - keyForField(FIELD_COMPLETION_TIME_MS), - /* defaultValue= */ SystemClock.elapsedRealtime()); + bundle.getLong(FIELD_COMPLETION_TIME_MS, /* defaultValue= */ SystemClock.elapsedRealtime()); return new SessionResult(resultCode, extras == null ? Bundle.EMPTY : extras, completionTimeMs); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java index 7a25ccc1c9d..624e108d7be 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java @@ -41,6 +41,7 @@ import androidx.media3.common.C; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -153,9 +154,9 @@ private SessionToken(MediaSessionCompat.Token token, String packageName, int uid } private SessionToken(Bundle bundle) { - checkArgument(bundle.containsKey(keyForField(FIELD_IMPL_TYPE)), "Impl type needs to be set."); - @SessionTokenImplType int implType = bundle.getInt(keyForField(FIELD_IMPL_TYPE)); - Bundle implBundle = checkNotNull(bundle.getBundle(keyForField(FIELD_IMPL))); + checkArgument(bundle.containsKey(FIELD_IMPL_TYPE), "Impl type needs to be set."); + @SessionTokenImplType int implType = bundle.getInt(FIELD_IMPL_TYPE); + Bundle implBundle = checkNotNull(bundle.getBundle(FIELD_IMPL)); if (implType == IMPL_TYPE_BASE) { impl = SessionTokenImplBase.CREATOR.fromBundle(implBundle); } else { @@ -481,14 +482,8 @@ private static int getUid(PackageManager manager, String packageName) { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({FIELD_IMPL_TYPE, FIELD_IMPL}) - private @interface FieldNumber {} - - private static final int FIELD_IMPL_TYPE = 0; - private static final int FIELD_IMPL = 1; + private static final String FIELD_IMPL_TYPE = Util.intToStringMaxRadix(0); + private static final String FIELD_IMPL = Util.intToStringMaxRadix(1); /** Types of {@link SessionTokenImpl} */ @Documented @@ -505,11 +500,11 @@ private static int getUid(PackageManager manager, String packageName) { public Bundle toBundle() { Bundle bundle = new Bundle(); if (impl instanceof SessionTokenImplBase) { - bundle.putInt(keyForField(FIELD_IMPL_TYPE), IMPL_TYPE_BASE); + bundle.putInt(FIELD_IMPL_TYPE, IMPL_TYPE_BASE); } else { - bundle.putInt(keyForField(FIELD_IMPL_TYPE), IMPL_TYPE_LEGACY); + bundle.putInt(FIELD_IMPL_TYPE, IMPL_TYPE_LEGACY); } - bundle.putBundle(keyForField(FIELD_IMPL), impl.toBundle()); + bundle.putBundle(FIELD_IMPL, impl.toBundle()); return bundle; } @@ -519,8 +514,4 @@ public Bundle toBundle() { private static SessionToken fromBundle(Bundle bundle) { return new SessionToken(bundle); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplBase.java b/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplBase.java index 1c2e869b530..98250bab745 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplBase.java @@ -18,21 +18,15 @@ import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotEmpty; import static androidx.media3.common.util.Assertions.checkNotNull; -import static java.lang.annotation.ElementType.TYPE_USE; import android.content.ComponentName; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.core.app.BundleCompat; import androidx.media3.common.util.Util; import com.google.common.base.Objects; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /* package */ final class SessionTokenImplBase implements SessionToken.SessionTokenImpl { @@ -211,45 +205,29 @@ public Object getBinder() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_UID, - FIELD_TYPE, - FIELD_LIBRARY_VERSION, - FIELD_PACKAGE_NAME, - FIELD_SERVICE_NAME, - FIELD_ISESSION, - FIELD_COMPONENT_NAME, - FIELD_EXTRAS, - FIELD_INTERFACE_VERSION - }) - private @interface FieldNumber {} - - private static final int FIELD_UID = 0; - private static final int FIELD_TYPE = 1; - private static final int FIELD_LIBRARY_VERSION = 2; - private static final int FIELD_PACKAGE_NAME = 3; - private static final int FIELD_SERVICE_NAME = 4; - private static final int FIELD_COMPONENT_NAME = 5; - private static final int FIELD_ISESSION = 6; - private static final int FIELD_EXTRAS = 7; - private static final int FIELD_INTERFACE_VERSION = 8; + private static final String FIELD_UID = Util.intToStringMaxRadix(0); + private static final String FIELD_TYPE = Util.intToStringMaxRadix(1); + private static final String FIELD_LIBRARY_VERSION = Util.intToStringMaxRadix(2); + private static final String FIELD_PACKAGE_NAME = Util.intToStringMaxRadix(3); + private static final String FIELD_SERVICE_NAME = Util.intToStringMaxRadix(4); + private static final String FIELD_COMPONENT_NAME = Util.intToStringMaxRadix(5); + private static final String FIELD_ISESSION = Util.intToStringMaxRadix(6); + private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(7); + private static final String FIELD_INTERFACE_VERSION = Util.intToStringMaxRadix(8); // Next field key = 9 @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putInt(keyForField(FIELD_UID), uid); - bundle.putInt(keyForField(FIELD_TYPE), type); - bundle.putInt(keyForField(FIELD_LIBRARY_VERSION), libraryVersion); - bundle.putString(keyForField(FIELD_PACKAGE_NAME), packageName); - bundle.putString(keyForField(FIELD_SERVICE_NAME), serviceName); - BundleCompat.putBinder(bundle, keyForField(FIELD_ISESSION), iSession); - bundle.putParcelable(keyForField(FIELD_COMPONENT_NAME), componentName); - bundle.putBundle(keyForField(FIELD_EXTRAS), extras); - bundle.putInt(keyForField(FIELD_INTERFACE_VERSION), interfaceVersion); + bundle.putInt(FIELD_UID, uid); + bundle.putInt(FIELD_TYPE, type); + bundle.putInt(FIELD_LIBRARY_VERSION, libraryVersion); + bundle.putString(FIELD_PACKAGE_NAME, packageName); + bundle.putString(FIELD_SERVICE_NAME, serviceName); + BundleCompat.putBinder(bundle, FIELD_ISESSION, iSession); + bundle.putParcelable(FIELD_COMPONENT_NAME, componentName); + bundle.putBundle(FIELD_EXTRAS, extras); + bundle.putInt(FIELD_INTERFACE_VERSION, interfaceVersion); return bundle; } @@ -257,20 +235,18 @@ public Bundle toBundle() { public static final Creator CREATOR = SessionTokenImplBase::fromBundle; private static SessionTokenImplBase fromBundle(Bundle bundle) { - checkArgument(bundle.containsKey(keyForField(FIELD_UID)), "uid should be set."); - int uid = bundle.getInt(keyForField(FIELD_UID)); - checkArgument(bundle.containsKey(keyForField(FIELD_TYPE)), "type should be set."); - int type = bundle.getInt(keyForField(FIELD_TYPE)); - int libraryVersion = bundle.getInt(keyForField(FIELD_LIBRARY_VERSION), /* defaultValue= */ 0); - int interfaceVersion = - bundle.getInt(keyForField(FIELD_INTERFACE_VERSION), /* defaultValue= */ 0); + checkArgument(bundle.containsKey(FIELD_UID), "uid should be set."); + int uid = bundle.getInt(FIELD_UID); + checkArgument(bundle.containsKey(FIELD_TYPE), "type should be set."); + int type = bundle.getInt(FIELD_TYPE); + int libraryVersion = bundle.getInt(FIELD_LIBRARY_VERSION, /* defaultValue= */ 0); + int interfaceVersion = bundle.getInt(FIELD_INTERFACE_VERSION, /* defaultValue= */ 0); String packageName = - checkNotEmpty( - bundle.getString(keyForField(FIELD_PACKAGE_NAME)), "package name should be set."); - String serviceName = bundle.getString(keyForField(FIELD_SERVICE_NAME), /* defaultValue= */ ""); - @Nullable IBinder iSession = BundleCompat.getBinder(bundle, keyForField(FIELD_ISESSION)); - @Nullable ComponentName componentName = bundle.getParcelable(keyForField(FIELD_COMPONENT_NAME)); - @Nullable Bundle extras = bundle.getBundle(keyForField(FIELD_EXTRAS)); + checkNotEmpty(bundle.getString(FIELD_PACKAGE_NAME), "package name should be set."); + String serviceName = bundle.getString(FIELD_SERVICE_NAME, /* defaultValue= */ ""); + @Nullable IBinder iSession = BundleCompat.getBinder(bundle, FIELD_ISESSION); + @Nullable ComponentName componentName = bundle.getParcelable(FIELD_COMPONENT_NAME); + @Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS); return new SessionTokenImplBase( uid, type, @@ -282,8 +258,4 @@ private static SessionTokenImplBase fromBundle(Bundle bundle) { iSession, extras == null ? Bundle.EMPTY : extras); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java index da43c9fbd6b..a7edc7073cc 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java @@ -22,20 +22,14 @@ import static androidx.media3.session.SessionToken.TYPE_LIBRARY_SERVICE; import static androidx.media3.session.SessionToken.TYPE_SESSION; import static androidx.media3.session.SessionToken.TYPE_SESSION_LEGACY; -import static java.lang.annotation.ElementType.TYPE_USE; import android.content.ComponentName; import android.os.Bundle; import android.support.v4.media.session.MediaSessionCompat; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.util.Util; import androidx.media3.session.SessionToken.SessionTokenImpl; import com.google.common.base.Objects; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /* package */ final class SessionTokenImplLegacy implements SessionTokenImpl { @@ -176,36 +170,22 @@ public Object getBinder() { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - FIELD_LEGACY_TOKEN, - FIELD_UID, - FIELD_TYPE, - FIELD_COMPONENT_NAME, - FIELD_PACKAGE_NAME, - FIELD_EXTRAS - }) - private @interface FieldNumber {} - - private static final int FIELD_LEGACY_TOKEN = 0; - private static final int FIELD_UID = 1; - private static final int FIELD_TYPE = 2; - private static final int FIELD_COMPONENT_NAME = 3; - private static final int FIELD_PACKAGE_NAME = 4; - private static final int FIELD_EXTRAS = 5; + private static final String FIELD_LEGACY_TOKEN = Util.intToStringMaxRadix(0); + private static final String FIELD_UID = Util.intToStringMaxRadix(1); + private static final String FIELD_TYPE = Util.intToStringMaxRadix(2); + private static final String FIELD_COMPONENT_NAME = Util.intToStringMaxRadix(3); + private static final String FIELD_PACKAGE_NAME = Util.intToStringMaxRadix(4); + private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(5); @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putBundle( - keyForField(FIELD_LEGACY_TOKEN), legacyToken == null ? null : legacyToken.toBundle()); - bundle.putInt(keyForField(FIELD_UID), uid); - bundle.putInt(keyForField(FIELD_TYPE), type); - bundle.putParcelable(keyForField(FIELD_COMPONENT_NAME), componentName); - bundle.putString(keyForField(FIELD_PACKAGE_NAME), packageName); - bundle.putBundle(keyForField(FIELD_EXTRAS), extras); + bundle.putBundle(FIELD_LEGACY_TOKEN, legacyToken == null ? null : legacyToken.toBundle()); + bundle.putInt(FIELD_UID, uid); + bundle.putInt(FIELD_TYPE, type); + bundle.putParcelable(FIELD_COMPONENT_NAME, componentName); + bundle.putString(FIELD_PACKAGE_NAME, packageName); + bundle.putBundle(FIELD_EXTRAS, extras); return bundle; } @@ -213,24 +193,19 @@ public Bundle toBundle() { public static final Creator CREATOR = SessionTokenImplLegacy::fromBundle; private static SessionTokenImplLegacy fromBundle(Bundle bundle) { - @Nullable Bundle legacyTokenBundle = bundle.getBundle(keyForField(FIELD_LEGACY_TOKEN)); + @Nullable Bundle legacyTokenBundle = bundle.getBundle(FIELD_LEGACY_TOKEN); @Nullable MediaSessionCompat.Token legacyToken = legacyTokenBundle == null ? null : MediaSessionCompat.Token.fromBundle(legacyTokenBundle); - checkArgument(bundle.containsKey(keyForField(FIELD_UID)), "uid should be set."); - int uid = bundle.getInt(keyForField(FIELD_UID)); - checkArgument(bundle.containsKey(keyForField(FIELD_TYPE)), "type should be set."); - int type = bundle.getInt(keyForField(FIELD_TYPE)); - @Nullable ComponentName componentName = bundle.getParcelable(keyForField(FIELD_COMPONENT_NAME)); + checkArgument(bundle.containsKey(FIELD_UID), "uid should be set."); + int uid = bundle.getInt(FIELD_UID); + checkArgument(bundle.containsKey(FIELD_TYPE), "type should be set."); + int type = bundle.getInt(FIELD_TYPE); + @Nullable ComponentName componentName = bundle.getParcelable(FIELD_COMPONENT_NAME); String packageName = - checkNotEmpty( - bundle.getString(keyForField(FIELD_PACKAGE_NAME)), "package name should be set."); - @Nullable Bundle extras = bundle.getBundle(keyForField(FIELD_EXTRAS)); + checkNotEmpty(bundle.getString(FIELD_PACKAGE_NAME), "package name should be set."); + @Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS); return new SessionTokenImplLegacy( legacyToken, uid, type, componentName, packageName, extras == null ? Bundle.EMPTY : extras); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } From b6970c09b89e3690113bb00efd9cd61d51ac801e Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 10 Jan 2023 17:08:34 +0000 Subject: [PATCH 096/141] Update bandwidth meter estimates PiperOrigin-RevId: 501010994 (cherry picked from commit 2c7e9ca8237e39bde686dd635699778aa8c6b96e) --- .../upstream/DefaultBandwidthMeter.java | 460 +++++++++--------- 1 file changed, 238 insertions(+), 222 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeter.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeter.java index 04f73c76ee1..924b9d3ac0b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeter.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeter.java @@ -48,27 +48,27 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList /** Default initial Wifi bitrate estimate in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI = - ImmutableList.of(4_800_000L, 3_100_000L, 2_100_000L, 1_500_000L, 800_000L); + ImmutableList.of(4_400_000L, 3_200_000L, 2_300_000L, 1_600_000L, 810_000L); /** Default initial 2G bitrate estimates in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_2G = - ImmutableList.of(1_500_000L, 1_000_000L, 730_000L, 440_000L, 170_000L); + ImmutableList.of(1_400_000L, 990_000L, 730_000L, 510_000L, 230_000L); /** Default initial 3G bitrate estimates in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_3G = - ImmutableList.of(2_200_000L, 1_400_000L, 1_100_000L, 910_000L, 620_000L); + ImmutableList.of(2_100_000L, 1_400_000L, 1_000_000L, 890_000L, 640_000L); /** Default initial 4G bitrate estimates in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_4G = - ImmutableList.of(3_000_000L, 1_900_000L, 1_400_000L, 1_000_000L, 660_000L); + ImmutableList.of(2_600_000L, 1_700_000L, 1_300_000L, 1_000_000L, 700_000L); /** Default initial 5G-NSA bitrate estimates in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_NSA = - ImmutableList.of(6_000_000L, 4_100_000L, 3_200_000L, 1_800_000L, 1_000_000L); + ImmutableList.of(5_700_000L, 3_700_000L, 2_300_000L, 1_700_000L, 990_000L); /** Default initial 5G-SA bitrate estimates in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_SA = - ImmutableList.of(2_800_000L, 2_400_000L, 1_600_000L, 1_100_000L, 950_000L); + ImmutableList.of(2_800_000L, 1_800_000L, 1_400_000L, 1_100_000L, 870_000L); /** * Default initial bitrate estimate used when the device is offline or the network type cannot be @@ -483,394 +483,410 @@ private static boolean isTransferAtFullNetworkSpeed(DataSpec dataSpec, boolean i */ private static int[] getInitialBitrateCountryGroupAssignment(String country) { switch (country) { + case "AD": + case "CW": + return new int[] {2, 2, 0, 0, 2, 2}; case "AE": - return new int[] {1, 4, 4, 4, 4, 0}; + return new int[] {1, 4, 3, 4, 4, 2}; case "AG": - return new int[] {2, 4, 1, 2, 2, 2}; - case "AI": - return new int[] {0, 2, 0, 3, 2, 2}; + return new int[] {2, 4, 3, 4, 2, 2}; + case "AL": + return new int[] {1, 1, 1, 3, 2, 2}; case "AM": return new int[] {2, 3, 2, 3, 2, 2}; case "AO": - return new int[] {4, 4, 3, 2, 2, 2}; + return new int[] {4, 4, 4, 3, 2, 2}; case "AS": return new int[] {2, 2, 3, 3, 2, 2}; case "AT": - return new int[] {1, 0, 1, 1, 0, 0}; + return new int[] {1, 2, 1, 4, 1, 4}; case "AU": - return new int[] {0, 1, 1, 1, 2, 0}; - case "AW": - return new int[] {1, 3, 4, 4, 2, 2}; - case "BA": - return new int[] {1, 2, 1, 1, 2, 2}; - case "BD": - return new int[] {2, 1, 3, 3, 2, 2}; + return new int[] {0, 2, 1, 1, 3, 0}; case "BE": return new int[] {0, 1, 4, 4, 3, 2}; - case "BF": - return new int[] {4, 3, 4, 3, 2, 2}; case "BH": - return new int[] {1, 2, 1, 3, 4, 2}; + return new int[] {1, 3, 1, 4, 4, 2}; case "BJ": - return new int[] {4, 4, 3, 3, 2, 2}; + return new int[] {4, 4, 2, 3, 2, 2}; + case "BN": + return new int[] {3, 2, 0, 1, 2, 2}; case "BO": return new int[] {1, 2, 3, 2, 2, 2}; - case "BS": - return new int[] {4, 4, 2, 2, 2, 2}; - case "BT": - return new int[] {3, 1, 3, 2, 2, 2}; + case "BR": + return new int[] {1, 1, 2, 1, 1, 0}; case "BW": return new int[] {3, 2, 1, 0, 2, 2}; case "BY": - return new int[] {0, 1, 2, 3, 2, 2}; - case "BZ": - return new int[] {2, 4, 2, 1, 2, 2}; + return new int[] {1, 1, 2, 3, 2, 2}; case "CA": - return new int[] {0, 2, 2, 2, 3, 2}; - case "CD": - return new int[] {4, 2, 3, 2, 2, 2}; + return new int[] {0, 2, 3, 3, 3, 3}; case "CH": - return new int[] {0, 0, 0, 1, 0, 2}; + return new int[] {0, 0, 0, 0, 0, 3}; + case "BZ": + case "CK": + return new int[] {2, 2, 2, 1, 2, 2}; + case "CL": + return new int[] {1, 1, 2, 1, 3, 2}; case "CM": - return new int[] {3, 3, 3, 3, 2, 2}; + return new int[] {4, 3, 3, 4, 2, 2}; case "CN": - return new int[] {2, 0, 1, 1, 3, 2}; + return new int[] {2, 0, 4, 3, 3, 1}; case "CO": - return new int[] {2, 3, 4, 3, 2, 2}; + return new int[] {2, 3, 4, 2, 2, 2}; case "CR": - return new int[] {2, 3, 4, 4, 2, 2}; + return new int[] {2, 4, 4, 4, 2, 2}; case "CV": - return new int[] {2, 1, 0, 0, 2, 2}; - case "BN": - case "CW": - return new int[] {2, 2, 0, 0, 2, 2}; + return new int[] {2, 3, 0, 1, 2, 2}; + case "CZ": + return new int[] {0, 0, 2, 0, 1, 2}; case "DE": - return new int[] {0, 1, 2, 2, 2, 3}; - case "DK": - return new int[] {0, 0, 3, 2, 0, 2}; + return new int[] {0, 1, 3, 2, 2, 2}; case "DO": return new int[] {3, 4, 4, 4, 4, 2}; + case "AZ": + case "BF": + case "DZ": + return new int[] {3, 3, 4, 4, 2, 2}; case "EC": - return new int[] {2, 3, 2, 1, 2, 2}; - case "ET": - return new int[] {4, 3, 3, 1, 2, 2}; + return new int[] {1, 3, 2, 1, 2, 2}; + case "CI": + case "EG": + return new int[] {3, 4, 3, 3, 2, 2}; case "FI": - return new int[] {0, 0, 0, 3, 0, 2}; + return new int[] {0, 0, 0, 2, 0, 2}; case "FJ": - return new int[] {3, 1, 2, 2, 2, 2}; + return new int[] {3, 1, 2, 3, 2, 2}; case "FM": - return new int[] {4, 2, 4, 1, 2, 2}; - case "FR": - return new int[] {1, 2, 3, 1, 0, 2}; - case "GB": - return new int[] {0, 0, 1, 1, 1, 1}; - case "GE": - return new int[] {1, 1, 1, 2, 2, 2}; + return new int[] {4, 2, 3, 0, 2, 2}; + case "AI": case "BB": + case "BM": + case "BQ": case "DM": case "FO": - case "GI": return new int[] {0, 2, 0, 0, 2, 2}; - case "AF": + case "FR": + return new int[] {1, 1, 2, 1, 1, 2}; + case "GB": + return new int[] {0, 1, 1, 2, 1, 2}; + case "GE": + return new int[] {1, 0, 0, 2, 2, 2}; + case "GG": + return new int[] {0, 2, 1, 0, 2, 2}; + case "CG": + case "GH": + return new int[] {3, 3, 3, 3, 2, 2}; case "GM": - return new int[] {4, 3, 3, 4, 2, 2}; + return new int[] {4, 3, 2, 4, 2, 2}; case "GN": - return new int[] {4, 3, 4, 2, 2, 2}; + return new int[] {4, 4, 4, 2, 2, 2}; + case "GP": + return new int[] {3, 1, 1, 3, 2, 2}; case "GQ": - return new int[] {4, 2, 1, 4, 2, 2}; + return new int[] {4, 4, 3, 3, 2, 2}; case "GT": - return new int[] {2, 3, 2, 2, 2, 2}; - case "CG": - case "EG": + return new int[] {2, 2, 2, 1, 1, 2}; + case "AW": + case "GU": + return new int[] {1, 2, 4, 4, 2, 2}; case "GW": - return new int[] {3, 4, 3, 3, 2, 2}; + return new int[] {4, 4, 2, 2, 2, 2}; case "GY": - return new int[] {3, 2, 2, 1, 2, 2}; + return new int[] {3, 0, 1, 1, 2, 2}; case "HK": - return new int[] {0, 1, 2, 3, 2, 0}; - case "HU": - return new int[] {0, 0, 0, 1, 3, 2}; + return new int[] {0, 1, 1, 3, 2, 0}; + case "HN": + return new int[] {3, 3, 2, 2, 2, 2}; case "ID": - return new int[] {3, 1, 2, 2, 3, 2}; - case "ES": + return new int[] {3, 1, 1, 2, 3, 2}; + case "BA": case "IE": - return new int[] {0, 1, 1, 1, 2, 2}; - case "CL": + return new int[] {1, 1, 1, 1, 2, 2}; case "IL": - return new int[] {1, 2, 2, 2, 3, 2}; + return new int[] {1, 2, 2, 3, 4, 2}; + case "IM": + return new int[] {0, 2, 0, 1, 2, 2}; case "IN": - return new int[] {1, 1, 3, 2, 3, 3}; - case "IQ": - return new int[] {3, 2, 2, 3, 2, 2}; + return new int[] {1, 1, 2, 1, 2, 1}; case "IR": - return new int[] {3, 0, 1, 1, 4, 1}; + return new int[] {4, 2, 3, 3, 4, 2}; + case "IS": + return new int[] {0, 0, 1, 0, 0, 2}; case "IT": - return new int[] {0, 0, 0, 1, 1, 2}; + return new int[] {0, 0, 1, 1, 1, 2}; + case "GI": + case "JE": + return new int[] {1, 2, 0, 1, 2, 2}; case "JM": - return new int[] {2, 4, 3, 2, 2, 2}; + return new int[] {2, 4, 2, 1, 2, 2}; case "JO": - return new int[] {2, 1, 1, 2, 2, 2}; + return new int[] {2, 0, 1, 1, 2, 2}; case "JP": - return new int[] {0, 1, 1, 2, 2, 4}; + return new int[] {0, 3, 3, 3, 4, 4}; + case "KE": + return new int[] {3, 2, 2, 1, 2, 2}; case "KH": - return new int[] {2, 1, 4, 2, 2, 2}; - case "CF": + return new int[] {1, 0, 4, 2, 2, 2}; + case "CU": case "KI": - return new int[] {4, 2, 4, 2, 2, 2}; - case "FK": - case "KE": - case "KP": - return new int[] {3, 2, 2, 2, 2, 2}; + return new int[] {4, 2, 4, 3, 2, 2}; + case "CD": + case "KM": + return new int[] {4, 3, 3, 2, 2, 2}; case "KR": - return new int[] {0, 1, 1, 3, 4, 4}; - case "CY": + return new int[] {0, 2, 2, 4, 4, 4}; case "KW": - return new int[] {1, 0, 0, 0, 0, 2}; + return new int[] {1, 0, 1, 0, 0, 2}; + case "BD": case "KZ": return new int[] {2, 1, 2, 2, 2, 2}; case "LA": return new int[] {1, 2, 1, 3, 2, 2}; + case "BS": case "LB": - return new int[] {3, 3, 2, 4, 2, 2}; + return new int[] {3, 2, 1, 2, 2, 2}; case "LK": - return new int[] {3, 1, 3, 3, 4, 2}; - case "CI": - case "DZ": + return new int[] {3, 2, 3, 4, 4, 2}; case "LR": - return new int[] {3, 4, 4, 4, 2, 2}; - case "LS": - return new int[] {3, 3, 2, 2, 2, 2}; - case "LT": - return new int[] {0, 0, 0, 0, 2, 2}; + return new int[] {3, 4, 3, 4, 2, 2}; case "LU": - return new int[] {1, 0, 3, 2, 1, 4}; + return new int[] {1, 1, 4, 2, 0, 2}; + case "CY": + case "HR": + case "LV": + return new int[] {1, 0, 0, 0, 0, 2}; case "MA": - return new int[] {3, 3, 1, 1, 2, 2}; + return new int[] {3, 3, 2, 1, 2, 2}; case "MC": return new int[] {0, 2, 2, 0, 2, 2}; + case "MD": + return new int[] {1, 0, 0, 0, 2, 2}; case "ME": - return new int[] {2, 0, 0, 1, 2, 2}; + return new int[] {2, 0, 0, 1, 1, 2}; + case "MH": + return new int[] {4, 2, 1, 3, 2, 2}; case "MK": - return new int[] {1, 0, 0, 1, 3, 2}; + return new int[] {2, 0, 0, 1, 3, 2}; case "MM": - return new int[] {2, 4, 2, 3, 2, 2}; + return new int[] {2, 2, 2, 3, 4, 2}; case "MN": return new int[] {2, 0, 1, 2, 2, 2}; case "MO": - case "MP": - return new int[] {0, 2, 4, 4, 2, 2}; - case "GP": + return new int[] {0, 2, 4, 4, 4, 2}; + case "KG": case "MQ": - return new int[] {2, 1, 2, 3, 2, 2}; - case "MU": - return new int[] {3, 1, 1, 2, 2, 2}; + return new int[] {2, 1, 1, 2, 2, 2}; + case "MR": + return new int[] {4, 2, 3, 4, 2, 2}; + case "DK": + case "EE": + case "HU": + case "LT": + case "MT": + return new int[] {0, 0, 0, 0, 0, 2}; case "MV": - return new int[] {3, 4, 1, 4, 2, 2}; + return new int[] {3, 4, 1, 3, 3, 2}; case "MW": return new int[] {4, 2, 3, 3, 2, 2}; case "MX": - return new int[] {2, 4, 3, 4, 2, 2}; + return new int[] {3, 4, 4, 4, 2, 2}; case "MY": - return new int[] {1, 0, 3, 1, 3, 2}; - case "MZ": - return new int[] {3, 1, 2, 1, 2, 2}; + return new int[] {1, 0, 4, 1, 2, 2}; + case "NA": + return new int[] {3, 4, 3, 2, 2, 2}; case "NC": - return new int[] {3, 3, 4, 4, 2, 2}; + return new int[] {3, 2, 3, 4, 2, 2}; case "NG": return new int[] {3, 4, 2, 1, 2, 2}; + case "NI": + return new int[] {2, 3, 4, 3, 2, 2}; case "NL": - return new int[] {0, 2, 2, 3, 0, 3}; - case "CZ": + return new int[] {0, 2, 3, 3, 0, 4}; case "NO": - return new int[] {0, 0, 2, 0, 1, 2}; + return new int[] {0, 1, 2, 1, 1, 2}; case "NP": - return new int[] {2, 2, 4, 3, 2, 2}; + return new int[] {2, 1, 4, 3, 2, 2}; case "NR": + return new int[] {4, 0, 3, 2, 2, 2}; case "NU": return new int[] {4, 2, 2, 1, 2, 2}; + case "NZ": + return new int[] {1, 0, 2, 2, 4, 2}; case "OM": return new int[] {2, 3, 1, 3, 4, 2}; - case "GU": + case "PA": + return new int[] {2, 3, 3, 3, 2, 2}; case "PE": - return new int[] {1, 2, 4, 4, 4, 2}; - case "CK": - case "PF": - return new int[] {2, 2, 2, 1, 2, 2}; - case "ML": + return new int[] {1, 2, 4, 4, 3, 2}; + case "AF": case "PG": - return new int[] {4, 3, 3, 2, 2, 2}; + return new int[] {4, 3, 3, 3, 2, 2}; case "PH": - return new int[] {2, 1, 3, 3, 3, 0}; - case "NZ": + return new int[] {2, 1, 3, 2, 2, 0}; case "PL": - return new int[] {1, 1, 2, 2, 4, 2}; + return new int[] {2, 1, 2, 2, 4, 2}; case "PR": - return new int[] {2, 0, 2, 1, 2, 1}; + return new int[] {2, 0, 2, 0, 2, 1}; case "PS": - return new int[] {3, 4, 1, 2, 2, 2}; + return new int[] {3, 4, 1, 4, 2, 2}; + case "PT": + return new int[] {1, 0, 0, 0, 1, 2}; case "PW": - return new int[] {2, 2, 4, 1, 2, 2}; - case "QA": - return new int[] {2, 4, 4, 4, 4, 2}; + return new int[] {2, 2, 4, 2, 2, 2}; + case "BL": case "MF": + case "PY": + return new int[] {1, 2, 2, 2, 2, 2}; + case "QA": + return new int[] {1, 4, 4, 4, 4, 2}; case "RE": - return new int[] {1, 2, 1, 2, 2, 2}; + return new int[] {1, 2, 2, 3, 1, 2}; case "RO": return new int[] {0, 0, 1, 2, 1, 2}; - case "MD": case "RS": - return new int[] {1, 0, 0, 0, 2, 2}; + return new int[] {2, 0, 0, 0, 2, 2}; case "RU": - return new int[] {1, 0, 0, 0, 4, 3}; + return new int[] {1, 0, 0, 0, 3, 3}; case "RW": - return new int[] {3, 4, 2, 0, 2, 2}; + return new int[] {3, 3, 1, 0, 2, 2}; + case "MU": case "SA": - return new int[] {3, 1, 1, 1, 2, 2}; + return new int[] {3, 1, 1, 2, 2, 2}; + case "CF": case "SB": - return new int[] {4, 2, 4, 3, 2, 2}; + return new int[] {4, 2, 4, 2, 2, 2}; + case "SC": + return new int[] {4, 3, 1, 1, 2, 2}; + case "SD": + return new int[] {4, 3, 4, 2, 2, 2}; + case "SE": + return new int[] {0, 1, 1, 1, 0, 2}; case "SG": - return new int[] {1, 1, 2, 2, 2, 1}; + return new int[] {2, 3, 3, 3, 3, 3}; case "AQ": case "ER": case "SH": return new int[] {4, 2, 2, 2, 2, 2}; + case "BG": + case "ES": case "GR": - case "HR": case "SI": - return new int[] {1, 0, 0, 0, 1, 2}; - case "BG": - case "MT": - case "SK": return new int[] {0, 0, 0, 0, 1, 2}; - case "AX": - case "LI": - case "MS": - case "PM": - case "SM": - return new int[] {0, 2, 2, 2, 2, 2}; + case "IQ": + case "SJ": + return new int[] {3, 2, 2, 2, 2, 2}; + case "SK": + return new int[] {1, 1, 1, 1, 3, 2}; + case "GF": + case "PK": + case "SL": + return new int[] {3, 2, 3, 3, 2, 2}; + case "ET": case "SN": - return new int[] {4, 4, 4, 3, 2, 2}; + return new int[] {4, 4, 3, 2, 2, 2}; + case "SO": + return new int[] {3, 2, 2, 4, 4, 2}; case "SR": return new int[] {2, 4, 3, 0, 2, 2}; - case "SS": - return new int[] {4, 3, 2, 3, 2, 2}; case "ST": return new int[] {2, 2, 1, 2, 2, 2}; - case "NI": - case "PA": + case "PF": case "SV": - return new int[] {2, 3, 3, 3, 2, 2}; + return new int[] {2, 3, 3, 1, 2, 2}; case "SZ": - return new int[] {3, 3, 3, 4, 2, 2}; - case "SX": + return new int[] {4, 4, 3, 4, 2, 2}; case "TC": - return new int[] {1, 2, 1, 0, 2, 2}; + return new int[] {2, 2, 1, 3, 2, 2}; case "GA": case "TG": return new int[] {3, 4, 1, 0, 2, 2}; case "TH": - return new int[] {0, 2, 2, 3, 3, 4}; - case "TK": - return new int[] {2, 2, 2, 4, 2, 2}; - case "CU": + return new int[] {0, 1, 2, 1, 2, 2}; case "DJ": case "SY": case "TJ": - case "TL": return new int[] {4, 3, 4, 4, 2, 2}; - case "SC": + case "GL": + case "TK": + return new int[] {2, 2, 2, 4, 2, 2}; + case "TL": + return new int[] {4, 2, 4, 4, 2, 2}; + case "SS": case "TM": - return new int[] {4, 2, 1, 1, 2, 2}; - case "AZ": - case "GF": - case "LY": - case "PK": - case "SO": - case "TO": - return new int[] {3, 2, 3, 3, 2, 2}; + return new int[] {4, 2, 2, 3, 2, 2}; case "TR": - return new int[] {1, 1, 0, 0, 2, 2}; + return new int[] {1, 0, 0, 1, 3, 2}; case "TT": - return new int[] {1, 4, 1, 3, 2, 2}; - case "EE": - case "IS": - case "LV": - case "PT": - case "SE": + return new int[] {1, 4, 0, 0, 2, 2}; case "TW": - return new int[] {0, 0, 0, 0, 0, 2}; + return new int[] {0, 2, 0, 0, 0, 0}; + case "ML": case "TZ": - return new int[] {3, 4, 3, 2, 2, 2}; - case "IM": + return new int[] {3, 4, 2, 2, 2, 2}; case "UA": - return new int[] {0, 2, 1, 1, 2, 2}; - case "SL": + return new int[] {0, 1, 1, 2, 4, 2}; + case "LS": case "UG": - return new int[] {3, 3, 4, 3, 2, 2}; + return new int[] {3, 3, 3, 2, 2, 2}; case "US": - return new int[] {1, 0, 2, 2, 3, 1}; - case "AR": - case "KG": + return new int[] {1, 1, 4, 1, 3, 1}; case "TN": case "UY": return new int[] {2, 1, 1, 1, 2, 2}; case "UZ": - return new int[] {2, 2, 3, 4, 2, 2}; - case "BL": + return new int[] {2, 2, 3, 4, 3, 2}; + case "AX": case "CX": + case "LI": + case "MP": + case "MS": + case "PM": + case "SM": case "VA": - return new int[] {1, 2, 2, 2, 2, 2}; - case "AD": - case "BM": - case "BQ": + return new int[] {0, 2, 2, 2, 2, 2}; case "GD": - case "GL": case "KN": case "KY": case "LC": + case "SX": case "VC": return new int[] {1, 2, 0, 0, 2, 2}; case "VG": - return new int[] {2, 2, 1, 1, 2, 2}; - case "GG": + return new int[] {2, 2, 0, 1, 2, 2}; case "VI": - return new int[] {0, 2, 0, 1, 2, 2}; + return new int[] {0, 2, 1, 2, 2, 2}; case "VN": - return new int[] {0, 3, 3, 4, 2, 2}; - case "GH": - case "NA": + return new int[] {0, 0, 1, 2, 2, 1}; case "VU": - return new int[] {3, 3, 3, 2, 2, 2}; + return new int[] {4, 3, 3, 1, 2, 2}; case "IO": - case "MH": case "TV": case "WF": return new int[] {4, 2, 2, 4, 2, 2}; + case "BT": + case "MZ": case "WS": - return new int[] {3, 1, 3, 1, 2, 2}; - case "AL": + return new int[] {3, 1, 2, 1, 2, 2}; case "XK": - return new int[] {1, 1, 1, 1, 2, 2}; + return new int[] {1, 2, 1, 1, 2, 2}; case "BI": case "HT": - case "KM": case "MG": case "NE": - case "SD": case "TD": case "VE": case "YE": return new int[] {4, 4, 4, 4, 2, 2}; - case "JE": case "YT": - return new int[] {4, 2, 2, 3, 2, 2}; + return new int[] {2, 3, 3, 4, 2, 2}; case "ZA": - return new int[] {3, 2, 2, 1, 1, 2}; + return new int[] {2, 3, 2, 1, 2, 2}; case "ZM": - return new int[] {3, 3, 4, 2, 2, 2}; - case "MR": + return new int[] {4, 4, 4, 3, 3, 2}; + case "LY": + case "TO": case "ZW": - return new int[] {4, 2, 4, 4, 2, 2}; + return new int[] {3, 2, 4, 3, 2, 2}; default: return new int[] {2, 2, 2, 2, 2, 2}; } From 84545e0e470c03b4b71ca75652f06af3734c254a Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 10 Jan 2023 21:19:55 +0000 Subject: [PATCH 097/141] Add focusSkipButtonWhenAvailable to focus UI on ATV For TV devices the skip button needs to have the focus to be accessible with the remote control. This property makes this configurable while being set to true by default. PiperOrigin-RevId: 501077608 (cherry picked from commit 9882a207836bdc089796bde7238f5357b0c23e76) --- RELEASENOTES.md | 3 ++ .../ImaServerSideAdInsertionMediaSource.java | 36 ++++++++++++++++--- .../media3/exoplayer/ima/ImaUtil.java | 3 ++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 62c32363322..4816c82c74a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -54,6 +54,9 @@ * IMA extension * Remove player listener of the `ImaServerSideAdInsertionMediaSource` on the application thread to avoid threading issues. + * Add a property `focusSkipButtonWhenAvailable` to the + `ImaServerSideAdInsertionMediaSource.AdsLoader.Builder` to request + focusing the skip button on TV devices and set it to true by default. * Bump IMA SDK version to 3.29.0. ### 1.0.0-beta03 (2022-11-22) diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index 70fccc76558..831bf183be7 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -57,6 +57,7 @@ import androidx.media3.common.util.Util; import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.drm.DrmSessionManagerProvider; +import androidx.media3.exoplayer.ima.ImaUtil.ServerSideAdInsertionConfiguration; import androidx.media3.exoplayer.source.CompositeMediaSource; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.ForwardingTimeline; @@ -193,6 +194,7 @@ public static final class Builder { @Nullable private AdErrorEvent.AdErrorListener adErrorListener; private State state; private ImmutableList companionAdSlots; + private boolean focusSkipButtonWhenAvailable; /** * Creates an instance. @@ -205,6 +207,7 @@ public Builder(Context context, AdViewProvider adViewProvider) { this.adViewProvider = adViewProvider; companionAdSlots = ImmutableList.of(); state = new State(ImmutableMap.of()); + focusSkipButtonWhenAvailable = true; } /** @@ -274,6 +277,22 @@ public AdsLoader.Builder setAdsLoaderState(State state) { return this; } + /** + * Sets whether to focus the skip button (when available) on Android TV devices. The default + * setting is {@code true}. + * + * @param focusSkipButtonWhenAvailable Whether to focus the skip button (when available) on + * Android TV devices. + * @return This builder, for convenience. + * @see AdsRenderingSettings#setFocusSkipButtonWhenAvailable(boolean) + */ + @CanIgnoreReturnValue + public AdsLoader.Builder setFocusSkipButtonWhenAvailable( + boolean focusSkipButtonWhenAvailable) { + this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; + return this; + } + /** Returns a new {@link AdsLoader}. */ public AdsLoader build() { @Nullable ImaSdkSettings imaSdkSettings = this.imaSdkSettings; @@ -281,13 +300,14 @@ public AdsLoader build() { imaSdkSettings = ImaSdkFactory.getInstance().createImaSdkSettings(); imaSdkSettings.setLanguage(Util.getSystemLanguageCodes()[0]); } - ImaUtil.ServerSideAdInsertionConfiguration configuration = - new ImaUtil.ServerSideAdInsertionConfiguration( + ServerSideAdInsertionConfiguration configuration = + new ServerSideAdInsertionConfiguration( adViewProvider, imaSdkSettings, adEventListener, adErrorListener, companionAdSlots, + focusSkipButtonWhenAvailable, imaSdkSettings.isDebugMode()); return new AdsLoader(context, configuration, state); } @@ -354,7 +374,7 @@ private static State fromBundle(Bundle bundle) { } } - private final ImaUtil.ServerSideAdInsertionConfiguration configuration; + private final ServerSideAdInsertionConfiguration configuration; private final Context context; private final Map mediaSourceResources; @@ -363,7 +383,7 @@ private static State fromBundle(Bundle bundle) { @Nullable private Player player; private AdsLoader( - Context context, ImaUtil.ServerSideAdInsertionConfiguration configuration, State state) { + Context context, ServerSideAdInsertionConfiguration configuration, State state) { this.context = context.getApplicationContext(); this.configuration = configuration; mediaSourceResources = new HashMap<>(); @@ -504,6 +524,7 @@ public void prepareSourceInternal(@Nullable TransferListener mediaTransferListen StreamManagerLoadable streamManagerLoadable = new StreamManagerLoadable( sdkAdsLoader, + adsLoader.configuration, streamRequest, streamPlayer, applicationAdErrorListener, @@ -932,6 +953,7 @@ private static class StreamManagerLoadable implements Loadable, AdsLoadedListener, AdErrorListener { private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private final ServerSideAdInsertionConfiguration serverSideAdInsertionConfiguration; private final StreamRequest request; private final StreamPlayer streamPlayer; @Nullable private final AdErrorListener adErrorListener; @@ -948,11 +970,13 @@ private static class StreamManagerLoadable /** Creates an instance. */ private StreamManagerLoadable( com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader, + ServerSideAdInsertionConfiguration serverSideAdInsertionConfiguration, StreamRequest request, StreamPlayer streamPlayer, @Nullable AdErrorListener adErrorListener, int loadVideoTimeoutMs) { this.adsLoader = adsLoader; + this.serverSideAdInsertionConfiguration = serverSideAdInsertionConfiguration; this.request = request; this.streamPlayer = streamPlayer; this.adErrorListener = adErrorListener; @@ -1029,6 +1053,8 @@ public void onAdsManagerLoaded(AdsManagerLoadedEvent event) { AdsRenderingSettings adsRenderingSettings = ImaSdkFactory.getInstance().createAdsRenderingSettings(); adsRenderingSettings.setLoadVideoTimeout(loadVideoTimeoutMs); + adsRenderingSettings.setFocusSkipButtonWhenAvailable( + serverSideAdInsertionConfiguration.focusSkipButtonWhenAvailable); // After initialization completed the streamUri will be reported to the streamPlayer. streamManager.init(adsRenderingSettings); this.streamManager = streamManager; @@ -1261,7 +1287,7 @@ private static boolean isCurrentAdPlaying( private static StreamDisplayContainer createStreamDisplayContainer( ImaSdkFactory imaSdkFactory, - ImaUtil.ServerSideAdInsertionConfiguration config, + ServerSideAdInsertionConfiguration config, StreamPlayer streamPlayer) { StreamDisplayContainer container = ImaSdkFactory.createStreamDisplayContainer( diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java index 9c24e620096..2e900e7c6de 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java @@ -166,6 +166,7 @@ public static final class ServerSideAdInsertionConfiguration { @Nullable public final AdEvent.AdEventListener applicationAdEventListener; @Nullable public final AdErrorEvent.AdErrorListener applicationAdErrorListener; public final ImmutableList companionAdSlots; + public final boolean focusSkipButtonWhenAvailable; public final boolean debugModeEnabled; public ServerSideAdInsertionConfiguration( @@ -174,12 +175,14 @@ public ServerSideAdInsertionConfiguration( @Nullable AdEvent.AdEventListener applicationAdEventListener, @Nullable AdErrorEvent.AdErrorListener applicationAdErrorListener, List companionAdSlots, + boolean focusSkipButtonWhenAvailable, boolean debugModeEnabled) { this.imaSdkSettings = imaSdkSettings; this.adViewProvider = adViewProvider; this.applicationAdEventListener = applicationAdEventListener; this.applicationAdErrorListener = applicationAdErrorListener; this.companionAdSlots = ImmutableList.copyOf(companionAdSlots); + this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; this.debugModeEnabled = debugModeEnabled; } } From 5d848040708d7b1544defe050c3418032493848f Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 10 Jan 2023 21:31:35 +0000 Subject: [PATCH 098/141] Use onMediaMetadataChanged for updating the legacy session Issue: androidx/media#219 PiperOrigin-RevId: 501080612 (cherry picked from commit 375299bf364041ccef17b81020a13af7db997433) --- RELEASENOTES.md | 2 + .../session/MediaSessionLegacyStub.java | 62 ++++++++----- .../androidx/media3/session/MediaUtils.java | 38 ++++---- .../session/common/IRemoteMediaSession.aidl | 1 + ...lerCompatCallbackWithMediaSessionTest.java | 89 ++++++++++++++++--- ...aControllerWithMediaSessionCompatTest.java | 44 +++++++-- .../media3/session/MediaUtilsTest.java | 13 ++- .../session/MediaSessionProviderService.java | 10 +++ .../media3/session/RemoteMediaSession.java | 4 + 9 files changed, 207 insertions(+), 56 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4816c82c74a..7ec50c06749 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,8 @@ for custom players. * Add helper method to convert platform session token to Media3 `SessionToken` ([#171](https://github.com/androidx/media/issues/171)). + * Use `onMediaMetadataChanged` to trigger updates of the platform media + session ([#219](https://github.com/androidx/media/issues/219)). * Metadata: * Parse multiple null-separated values from ID3 frames, as permitted by ID3 v2.4. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 2215b070711..069534ffad4 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -91,6 +91,7 @@ import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -850,12 +851,15 @@ public boolean equals(@Nullable Object obj) { private final class ControllerLegacyCbForBroadcast implements ControllerCb { - @Nullable private MediaItem currentMediaItemForMetadataUpdate; - - private long durationMsForMetadataUpdate; + private MediaMetadata lastMediaMetadata; + private String lastMediaId; + @Nullable private Uri lastMediaUri; + private long lastDurationMs; public ControllerLegacyCbForBroadcast() { - durationMsForMetadataUpdate = C.TIME_UNSET; + lastMediaMetadata = MediaMetadata.EMPTY; + lastMediaId = MediaItem.DEFAULT_MEDIA_ID; + lastDurationMs = C.TIME_UNSET; } @Override @@ -992,6 +996,7 @@ public void onPlaybackParametersChanged(int seq, PlaybackParameters playbackPara public void onMediaItemTransition( int seq, @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) throws RemoteException { + // MediaMetadataCompat needs to be updated when the media ID or URI of the media item changes. updateMetadataIfChanged(); if (mediaItem == null) { sessionCompat.setRatingType(RatingCompat.RATING_NONE); @@ -1004,6 +1009,11 @@ public void onMediaItemTransition( .setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat()); } + @Override + public void onMediaMetadataChanged(int seq, MediaMetadata mediaMetadata) { + updateMetadataIfChanged(); + } + @Override public void onTimelineChanged( int seq, Timeline timeline, @Player.TimelineChangeReason int reason) @@ -1014,7 +1024,6 @@ public void onTimelineChanged( } updateQueue(timeline); - // Duration might be unknown at onMediaItemTransition and become available afterward. updateMetadataIfChanged(); } @@ -1146,22 +1155,30 @@ public void onPeriodicSessionPositionInfoChanged( .setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat()); } - @Override - public void onMediaMetadataChanged(int seq, MediaMetadata mediaMetadata) { - // Metadata change will be notified by onMediaItemTransition. - } - private void updateMetadataIfChanged() { - @Nullable MediaItem currentMediaItem = sessionImpl.getPlayerWrapper().getCurrentMediaItem(); - long durationMs = sessionImpl.getPlayerWrapper().getDuration(); - - if (ObjectsCompat.equals(currentMediaItemForMetadataUpdate, currentMediaItem) - && durationMsForMetadataUpdate == durationMs) { + Player player = sessionImpl.getPlayerWrapper(); + @Nullable MediaItem currentMediaItem = player.getCurrentMediaItem(); + MediaMetadata newMediaMetadata = player.getMediaMetadata(); + long newDurationMs = player.getDuration(); + String newMediaId = + currentMediaItem != null ? currentMediaItem.mediaId : MediaItem.DEFAULT_MEDIA_ID; + @Nullable + Uri newMediaUri = + currentMediaItem != null && currentMediaItem.localConfiguration != null + ? currentMediaItem.localConfiguration.uri + : null; + + if (Objects.equals(lastMediaMetadata, newMediaMetadata) + && Objects.equals(lastMediaId, newMediaId) + && Objects.equals(lastMediaUri, newMediaUri) + && lastDurationMs == newDurationMs) { return; } - currentMediaItemForMetadataUpdate = currentMediaItem; - durationMsForMetadataUpdate = durationMs; + lastMediaId = newMediaId; + lastMediaUri = newMediaUri; + lastMediaMetadata = newMediaMetadata; + lastDurationMs = newDurationMs; if (currentMediaItem == null) { setMetadata(sessionCompat, /* metadataCompat= */ null); @@ -1170,7 +1187,7 @@ private void updateMetadataIfChanged() { @Nullable Bitmap artworkBitmap = null; ListenableFuture bitmapFuture = - sessionImpl.getBitmapLoader().loadBitmapFromMetadata(currentMediaItem.mediaMetadata); + sessionImpl.getBitmapLoader().loadBitmapFromMetadata(newMediaMetadata); if (bitmapFuture != null) { pendingBitmapLoadCallback = null; if (bitmapFuture.isDone()) { @@ -1190,7 +1207,11 @@ public void onSuccess(Bitmap result) { setMetadata( sessionCompat, MediaUtils.convertToMediaMetadataCompat( - currentMediaItem, durationMs, result)); + newMediaMetadata, + newMediaId, + newMediaUri, + newDurationMs, + /* artworkBitmap= */ result)); sessionImpl.onNotificationRefreshRequired(); } @@ -1210,7 +1231,8 @@ public void onFailure(Throwable t) { } setMetadata( sessionCompat, - MediaUtils.convertToMediaMetadataCompat(currentMediaItem, durationMs, artworkBitmap)); + MediaUtils.convertToMediaMetadataCompat( + newMediaMetadata, newMediaId, newMediaUri, newDurationMs, artworkBitmap)); } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 11735dd6ac7..716f08a29dd 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -531,14 +531,25 @@ private static CharSequence getFirstText( return null; } - /** Converts a {@link MediaItem} to a {@link MediaMetadataCompat}. */ + /** + * Converts a {@link MediaMetadata} to a {@link MediaMetadataCompat}. + * + * @param metadata The {@link MediaMetadata} instance to convert. + * @param mediaId The corresponding media ID. + * @param mediaUri The corresponding media URI, or null if unknown. + * @param durationMs The duration of the media, in milliseconds or {@link C#TIME_UNSET}, if no + * duration should be included. + * @return An instance of the legacy {@link MediaMetadataCompat}. + */ public static MediaMetadataCompat convertToMediaMetadataCompat( - MediaItem mediaItem, long durationMs, @Nullable Bitmap artworkBitmap) { + MediaMetadata metadata, + String mediaId, + @Nullable Uri mediaUri, + long durationMs, + @Nullable Bitmap artworkBitmap) { MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder() - .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaItem.mediaId); - - MediaMetadata metadata = mediaItem.mediaMetadata; + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId); if (metadata.title != null) { builder.putText(MediaMetadataCompat.METADATA_KEY_TITLE, metadata.title); @@ -569,10 +580,8 @@ public static MediaMetadataCompat convertToMediaMetadataCompat( builder.putLong(MediaMetadataCompat.METADATA_KEY_YEAR, metadata.recordingYear); } - if (mediaItem.requestMetadata.mediaUri != null) { - builder.putString( - MediaMetadataCompat.METADATA_KEY_MEDIA_URI, - mediaItem.requestMetadata.mediaUri.toString()); + if (mediaUri != null) { + builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, mediaUri.toString()); } if (metadata.artworkUri != null) { @@ -597,21 +606,18 @@ public static MediaMetadataCompat convertToMediaMetadataCompat( builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMs); } - @Nullable - RatingCompat userRatingCompat = convertToRatingCompat(mediaItem.mediaMetadata.userRating); + @Nullable RatingCompat userRatingCompat = convertToRatingCompat(metadata.userRating); if (userRatingCompat != null) { builder.putRating(MediaMetadataCompat.METADATA_KEY_USER_RATING, userRatingCompat); } - @Nullable - RatingCompat overallRatingCompat = convertToRatingCompat(mediaItem.mediaMetadata.overallRating); + @Nullable RatingCompat overallRatingCompat = convertToRatingCompat(metadata.overallRating); if (overallRatingCompat != null) { builder.putRating(MediaMetadataCompat.METADATA_KEY_RATING, overallRatingCompat); } - if (mediaItem.mediaMetadata.mediaType != null) { - builder.putLong( - MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT, mediaItem.mediaMetadata.mediaType); + if (metadata.mediaType != null) { + builder.putLong(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT, metadata.mediaType); } return builder.build(); diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl index 5220083112e..c7b50fa114a 100644 --- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl +++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl @@ -67,6 +67,7 @@ interface IRemoteMediaSession { void setTimeline(String sessionId, in Bundle timeline); void createAndSetFakeTimeline(String sessionId, int windowCount); + void setMediaMetadata(String sessionId, in Bundle metadata); void setPlaylistMetadata(String sessionId, in Bundle metadata); void setShuffleModeEnabled(String sessionId, boolean shuffleMode); void setRepeatMode(String sessionId, int repeatMode); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java index 77d06b72449..71543ae0fed 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java @@ -127,6 +127,7 @@ public void gettersAfterConnected() throws Exception { .setBufferedPosition(testBufferingPosition) .setPlaybackParameters(new PlaybackParameters(testSpeed)) .setTimeline(testTimeline) + .setMediaMetadata(testMediaItems.get(testItemIndex).mediaMetadata) .setPlaylistMetadata(testPlaylistMetadata) .setCurrentMediaItemIndex(testItemIndex) .setShuffleModeEnabled(testShuffleModeEnabled) @@ -370,6 +371,7 @@ public void onShuffleModeChanged(int shuffleMode) { .setDuration(testDurationMs) .setPlaybackParameters(playbackParameters) .setTimeline(testTimeline) + .setMediaMetadata(testMediaItems.get(testItemIndex).mediaMetadata) .setPlaylistMetadata(testPlaylistMetadata) .setCurrentMediaItemIndex(testItemIndex) .setShuffleModeEnabled(testShuffleModeEnabled) @@ -979,53 +981,68 @@ public void onExtrasChanged(Bundle extras) { } @Test - public void currentMediaItemChange() throws Exception { + public void onMediaItemTransition_updatesLegacyMetadataAndPlaybackState_correctModelConversion() + throws Exception { int testItemIndex = 3; long testPosition = 1234; String testDisplayTitle = "displayTitle"; + long testDurationMs = 30_000; List testMediaItems = MediaTestUtils.createMediaItems(/* size= */ 5); + String testCurrentMediaId = testMediaItems.get(testItemIndex).mediaId; + MediaMetadata testMediaMetadata = + new MediaMetadata.Builder().setTitle(testDisplayTitle).build(); testMediaItems.set( testItemIndex, new MediaItem.Builder() .setMediaId(testMediaItems.get(testItemIndex).mediaId) - .setMediaMetadata(new MediaMetadata.Builder().setTitle(testDisplayTitle).build()) + .setMediaMetadata(testMediaMetadata) .build()); - Timeline timeline = new PlaylistTimeline(testMediaItems); - session.getMockPlayer().setTimeline(timeline); + session.getMockPlayer().setTimeline(new PlaylistTimeline(testMediaItems)); + session.getMockPlayer().setCurrentMediaItemIndex(testItemIndex); + session.getMockPlayer().setCurrentPosition(testPosition); + session.getMockPlayer().setDuration(testDurationMs); + session.getMockPlayer().setMediaMetadata(testMediaMetadata); AtomicReference metadataRef = new AtomicReference<>(); AtomicReference playbackStateRef = new AtomicReference<>(); CountDownLatch latchForMetadata = new CountDownLatch(1); CountDownLatch latchForPlaybackState = new CountDownLatch(1); + List callbackOrder = new ArrayList<>(); MediaControllerCompat.Callback callback = new MediaControllerCompat.Callback() { @Override public void onMetadataChanged(MediaMetadataCompat metadata) { metadataRef.set(metadata); + callbackOrder.add("onMetadataChanged"); latchForMetadata.countDown(); } @Override public void onPlaybackStateChanged(PlaybackStateCompat state) { playbackStateRef.set(state); + callbackOrder.add("onPlaybackStateChanged"); latchForPlaybackState.countDown(); } }; controllerCompat.registerCallback(callback, handler); - session.getMockPlayer().setCurrentMediaItemIndex(testItemIndex); - session.getMockPlayer().setCurrentPosition(testPosition); session .getMockPlayer() .notifyMediaItemTransition(testItemIndex, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + // Assert metadata. assertThat(latchForMetadata.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(metadataRef.get().getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)) + MediaMetadataCompat parameterMetadataCompat = metadataRef.get(); + MediaMetadataCompat getterMetadataCompat = controllerCompat.getMetadata(); + assertThat(parameterMetadataCompat.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)) .isEqualTo(testDisplayTitle); - assertThat( - controllerCompat - .getMetadata() - .getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)) + assertThat(getterMetadataCompat.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)) .isEqualTo(testDisplayTitle); + assertThat(parameterMetadataCompat.getLong(METADATA_KEY_DURATION)).isEqualTo(testDurationMs); + assertThat(getterMetadataCompat.getLong(METADATA_KEY_DURATION)).isEqualTo(testDurationMs); + assertThat(parameterMetadataCompat.getString(METADATA_KEY_MEDIA_ID)) + .isEqualTo(testCurrentMediaId); + assertThat(getterMetadataCompat.getString(METADATA_KEY_MEDIA_ID)).isEqualTo(testCurrentMediaId); + // Assert the playback state. assertThat(latchForPlaybackState.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(playbackStateRef.get().getPosition()).isEqualTo(testPosition); assertThat(controllerCompat.getPlaybackState().getPosition()).isEqualTo(testPosition); @@ -1033,6 +1050,56 @@ public void onPlaybackStateChanged(PlaybackStateCompat state) { .isEqualTo(MediaUtils.convertToQueueItemId(testItemIndex)); assertThat(controllerCompat.getPlaybackState().getActiveQueueItemId()) .isEqualTo(MediaUtils.convertToQueueItemId(testItemIndex)); + assertThat(callbackOrder) + .containsExactly("onMetadataChanged", "onPlaybackStateChanged") + .inOrder(); + } + + @Test + public void onMediaMetadataChanged_updatesLegacyMetadata_correctModelConversion() + throws Exception { + int testItemIndex = 3; + String testDisplayTitle = "displayTitle"; + long testDurationMs = 30_000; + List testMediaItems = MediaTestUtils.createMediaItems(/* size= */ 5); + String testCurrentMediaId = testMediaItems.get(testItemIndex).mediaId; + MediaMetadata testMediaMetadata = + new MediaMetadata.Builder().setTitle(testDisplayTitle).build(); + testMediaItems.set( + testItemIndex, + new MediaItem.Builder() + .setMediaId(testMediaItems.get(testItemIndex).mediaId) + .setMediaMetadata(testMediaMetadata) + .build()); + session.getMockPlayer().setTimeline(new PlaylistTimeline(testMediaItems)); + session.getMockPlayer().setCurrentMediaItemIndex(testItemIndex); + session.getMockPlayer().setDuration(testDurationMs); + AtomicReference metadataRef = new AtomicReference<>(); + CountDownLatch latchForMetadata = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onMetadataChanged(MediaMetadataCompat metadata) { + metadataRef.set(metadata); + latchForMetadata.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.getMockPlayer().notifyMediaMetadataChanged(testMediaMetadata); + + assertThat(latchForMetadata.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + MediaMetadataCompat parameterMetadataCompat = metadataRef.get(); + MediaMetadataCompat getterMetadataCompat = controllerCompat.getMetadata(); + assertThat(parameterMetadataCompat.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)) + .isEqualTo(testDisplayTitle); + assertThat(getterMetadataCompat.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)) + .isEqualTo(testDisplayTitle); + assertThat(parameterMetadataCompat.getLong(METADATA_KEY_DURATION)).isEqualTo(testDurationMs); + assertThat(getterMetadataCompat.getLong(METADATA_KEY_DURATION)).isEqualTo(testDurationMs); + assertThat(parameterMetadataCompat.getString(METADATA_KEY_MEDIA_ID)) + .isEqualTo(testCurrentMediaId); + assertThat(getterMetadataCompat.getString(METADATA_KEY_MEDIA_ID)).isEqualTo(testCurrentMediaId); } @Test diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java index b735477ce79..e07afb4422d 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java @@ -714,7 +714,11 @@ public void getMediaItemCount_withInvalidQueueIdWithMetadata_returnsAdjustedCoun MediaItem testRemoveMediaItem = MediaTestUtils.createMediaItem("removed"); MediaMetadataCompat testMetadataCompat = MediaUtils.convertToMediaMetadataCompat( - testRemoveMediaItem, /* durationMs= */ 100L, /* artworkBitmap= */ null); + testRemoveMediaItem.mediaMetadata, + "mediaId", + Uri.parse("http://example.com"), + /* durationMs= */ 100L, + /* artworkBitmap= */ null); session.setQueue(testQueue); session.setMetadata(testMetadataCompat); MediaController controller = controllerTestRule.createController(session.getSessionToken()); @@ -732,7 +736,11 @@ public void getMediaItemCount_whenQueueIdIsChangedFromInvalidToValid_returnOrigi MediaItem testRemoveMediaItem = MediaTestUtils.createMediaItem("removed"); MediaMetadataCompat testMetadataCompat = MediaUtils.convertToMediaMetadataCompat( - testRemoveMediaItem, /* durationMs= */ 100L, /* artworkBitmap= */ null); + testRemoveMediaItem.mediaMetadata, + "mediaId", + Uri.parse("http://example.com"), + /* durationMs= */ 100L, + /* artworkBitmap= */ null); session.setQueue(testQueue); session.setMetadata(testMetadataCompat); MediaController controller = controllerTestRule.createController(session.getSessionToken()); @@ -767,7 +775,11 @@ public void getCurrentMediaItemIndex_withInvalidQueueIdWithMetadata_returnsEndOf MediaItem testRemoveMediaItem = MediaTestUtils.createMediaItem("removed"); MediaMetadataCompat testMetadataCompat = MediaUtils.convertToMediaMetadataCompat( - testRemoveMediaItem, /* durationMs= */ 100L, /* artworkBitmap= */ null); + testRemoveMediaItem.mediaMetadata, + "mediaId", + Uri.parse("http://example.com"), + /* durationMs= */ 100L, + /* artworkBitmap= */ null); session.setQueue(testQueue); session.setMetadata(testMetadataCompat); MediaController controller = controllerTestRule.createController(session.getSessionToken()); @@ -785,7 +797,11 @@ public void getMediaMetadata_withMediaMetadataCompat_returnsConvertedMediaMetada MediaMetadata testMediaMetadata = testMediaItem.mediaMetadata; MediaMetadataCompat testMediaMetadataCompat = MediaUtils.convertToMediaMetadataCompat( - testMediaItem, /* durationMs= */ 100L, /* artworkBitmap= */ null); + testMediaMetadata, + "mediaId", + Uri.parse("http://example.com"), + /* durationMs= */ 100L, + /* artworkBitmap= */ null); session.setMetadata(testMediaMetadataCompat); MediaController controller = controllerTestRule.createController(session.getSessionToken()); @@ -803,7 +819,11 @@ public void getMediaMetadata_withMediaMetadataCompatAndArtworkData_returnsConver @Nullable Bitmap artworkBitmap = getBitmapFromMetadata(testMediaMetadata); MediaMetadataCompat testMediaMetadataCompat = MediaUtils.convertToMediaMetadataCompat( - testMediaItem, /* durationMs= */ 100L, artworkBitmap); + testMediaMetadata, + "mediaId", + Uri.parse("http://example.com"), + /* durationMs= */ 100L, + artworkBitmap); session.setMetadata(testMediaMetadataCompat); MediaController controller = controllerTestRule.createController(session.getSessionToken()); @@ -1141,9 +1161,14 @@ public void onPositionDiscontinuity( @Test public void setPlaybackState_fromStateBufferingToPlaying_notifiesReadyState() throws Exception { List testPlaylist = MediaTestUtils.createMediaItems(/* size= */ 1); + MediaItem firstMediaItemInPlaylist = testPlaylist.get(0); MediaMetadataCompat metadata = MediaUtils.convertToMediaMetadataCompat( - testPlaylist.get(0), /* durationMs= */ 50_000, /* artworkBitmap= */ null); + firstMediaItemInPlaylist.mediaMetadata, + "mediaId", + Uri.parse("http://example.com"), + /* durationMs= */ 50_000, + /* artworkBitmap= */ null); long testBufferedPosition = 5_000; session.setMetadata(metadata); session.setPlaybackState( @@ -1186,9 +1211,14 @@ public void onPlaybackStateChanged(@State int playbackState) { public void setPlaybackState_fromStatePlayingToBuffering_notifiesBufferingState() throws Exception { List testPlaylist = MediaTestUtils.createMediaItems(1); + MediaItem firstMediaItemInPlaylist = testPlaylist.get(0); MediaMetadataCompat metadata = MediaUtils.convertToMediaMetadataCompat( - testPlaylist.get(0), /* durationMs= */ 1_000, /* artworkBitmap= */ null); + firstMediaItemInPlaylist.mediaMetadata, + "mediaId", + Uri.parse("http://example.com"), + /* durationMs= */ 1_000, + /* artworkBitmap= */ null); long testBufferingPosition = 0; session.setMetadata(metadata); session.setPlaybackState( diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java index 59209c334ad..9511124a5b6 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java @@ -29,6 +29,7 @@ import android.content.Context; import android.graphics.Bitmap; +import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.service.media.MediaBrowserService; @@ -217,7 +218,11 @@ public void convertToMediaMetadata_roundTripViaMediaMetadataCompat_returnsEqualM } MediaMetadataCompat testMediaMetadataCompat = MediaUtils.convertToMediaMetadataCompat( - testMediaItem, /* durationMs= */ 100L, testArtworkBitmap); + testMediaMetadata, + "mediaId", + Uri.parse("http://example.com"), + /* durationMs= */ 100L, + testArtworkBitmap); MediaMetadata mediaMetadata = MediaUtils.convertToMediaMetadata(testMediaMetadataCompat, RatingCompat.RATING_NONE); @@ -258,7 +263,11 @@ public void convertToMediaMetadataCompat_withMediaType_setsMediaType() { MediaMetadataCompat mediaMetadataCompat = MediaUtils.convertToMediaMetadataCompat( - mediaItem, /* durotionsMs= */ C.TIME_UNSET, /* artworkBitmap= */ null); + mediaItem.mediaMetadata, + "mediaId", + Uri.parse("http://www.example.com"), + /* durotionsMs= */ C.TIME_UNSET, + /* artworkBitmap= */ null); assertThat( mediaMetadataCompat.getLong( diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java index ce0c68eb09f..d48253eed78 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java @@ -801,6 +801,16 @@ public void createAndSetFakeTimeline(String sessionId, int windowCount) throws R }); } + @Override + public void setMediaMetadata(String sessionId, Bundle metadataBundle) throws RemoteException { + runOnHandler( + () -> { + MediaSession session = sessionMap.get(sessionId); + MockPlayer player = (MockPlayer) session.getPlayer(); + player.mediaMetadata = MediaMetadata.CREATOR.fromBundle(metadataBundle); + }); + } + @Override public void setPlaylistMetadata(String sessionId, Bundle playlistMetadataBundle) throws RemoteException { diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java index 7d5cadfa8d4..ca2840480d5 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java @@ -360,6 +360,10 @@ public void setTrackSelectionParameters(TrackSelectionParameters parameters) binder.setTrackSelectionParameters(sessionId, parameters.toBundle()); } + public void setMediaMetadata(MediaMetadata mediaMetadata) throws RemoteException { + binder.setMediaMetadata(sessionId, mediaMetadata.toBundle()); + } + public void notifyTimelineChanged(@Player.TimelineChangeReason int reason) throws RemoteException { binder.notifyTimelineChanged(sessionId, reason); From 764daff4179d9eb98204b51b7c09318a1b12f1f2 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 11 Jan 2023 16:24:55 +0000 Subject: [PATCH 099/141] Improve Java doc about how to override notification drawables Issue: androidx/media#140 PiperOrigin-RevId: 501288267 (cherry picked from commit a2cf2221170e333d1d1883e0e86c5efca32f55ba) --- .../session/DefaultMediaNotificationProvider.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index b0fd6bc36a7..c3acb2a83d4 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -84,8 +84,10 @@ * *

    Drawables

    * - * The drawables used can be overridden by drawables with the same names defined the application. - * The drawables are: + * The drawables used can be overridden by drawables with the same file names in {@code + * res/drawables} of the application module. Alternatively, you can override the drawable resource + * ID with a {@code drawable} element in a resource file in {@code res/values}. The drawable + * resource IDs are: * *
      *
    • {@code media3_notification_play} - The play icon. @@ -99,8 +101,8 @@ * *

      String resources

      * - * String resources used can be overridden by resources with the same names defined the application. - * These are: + * String resources used can be overridden by resources with the same resource IDs defined by the + * application. The string resource IDs are: * *
        *
      • {@code media3_controls_play_description} - The description of the play icon. From 1b8608f1794af2ae643439f2109feff7481bbfda Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 11 Jan 2023 18:39:11 +0000 Subject: [PATCH 100/141] Request notification permission in demo app for API 33+ Starting with API 33 the POST_NOTIFICATION permission needs to be requested at runtime or the notification is not shown. Note that with an app with targetSdkVersion < 33 but on a device with API 33 the notification permission is automatically requested when the app starts for the first time. If the user does not grant the permission, requesting the permission at runtime result in an empty array of grant results. Issue: google/ExoPlayer#10884 PiperOrigin-RevId: 501320632 (cherry picked from commit 6484c14acd4197d335cab0b5f2ab9d3eba8c2b39) --- RELEASENOTES.md | 3 + demos/main/src/main/AndroidManifest.xml | 1 + .../demo/main/SampleChooserActivity.java | 62 ++++++++++++++++--- demos/main/src/main/res/values/strings.xml | 2 + 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7ec50c06749..cb41231c1a7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -60,6 +60,9 @@ `ImaServerSideAdInsertionMediaSource.AdsLoader.Builder` to request focusing the skip button on TV devices and set it to true by default. * Bump IMA SDK version to 3.29.0. +* Demo app + * Request notification permission for download notifications at runtime + ([#10884](https://github.com/google/ExoPlayer/issues/10884)). ### 1.0.0-beta03 (2022-11-22) diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 401d73a8e61..21d07e4ee59 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ + diff --git a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java index fc7144dc91c..ef01b148ca6 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -41,8 +42,10 @@ import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; import androidx.annotation.OptIn; +import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem.ClippingConfiguration; @@ -76,6 +79,7 @@ public class SampleChooserActivity extends AppCompatActivity private static final String TAG = "SampleChooserActivity"; private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position"; private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position"; + private static final int POST_NOTIFICATION_PERMISSION_REQUEST_CODE = 100; private String[] uris; private boolean useExtensionRenderers; @@ -83,6 +87,8 @@ public class SampleChooserActivity extends AppCompatActivity private SampleAdapter sampleAdapter; private MenuItem preferExtensionDecodersMenuItem; private ExpandableListView sampleListView; + @Nullable private MediaItem downloadMediaItemWaitingForNotificationPermission; + private boolean notificationPermissionToastShown; @Override public void onCreate(Bundle savedInstanceState) { @@ -172,12 +178,34 @@ public void onDownloadsChanged() { public void onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == POST_NOTIFICATION_PERMISSION_REQUEST_CODE) { + handlePostNotificationPermissionGrantResults(grantResults); + } else { + handleExternalStoragePermissionGrantResults(grantResults); + } + } + + private void handlePostNotificationPermissionGrantResults(int[] grantResults) { + if (!notificationPermissionToastShown + && (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED)) { + Toast.makeText( + getApplicationContext(), R.string.post_notification_not_granted, Toast.LENGTH_LONG) + .show(); + notificationPermissionToastShown = true; + } + if (downloadMediaItemWaitingForNotificationPermission != null) { + // Download with or without permission to post notifications. + toggleDownload(downloadMediaItemWaitingForNotificationPermission); + downloadMediaItemWaitingForNotificationPermission = null; + } + } + + private void handleExternalStoragePermissionGrantResults(int[] grantResults) { if (grantResults.length == 0) { // Empty results are triggered if a permission is requested while another request was already // pending and can be safely ignored in this case. return; - } - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { loadSample(); } else { Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) @@ -244,15 +272,26 @@ private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) { if (downloadUnsupportedStringId != 0) { Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG) .show(); + } else if (!notificationPermissionToastShown + && Util.SDK_INT >= 33 + && checkSelfPermission(Api33.getPostNotificationPermissionString()) + != PackageManager.PERMISSION_GRANTED) { + downloadMediaItemWaitingForNotificationPermission = playlistHolder.mediaItems.get(0); + requestPermissions( + new String[] {Api33.getPostNotificationPermissionString()}, + /* requestCode= */ POST_NOTIFICATION_PERMISSION_REQUEST_CODE); } else { - RenderersFactory renderersFactory = - DemoUtil.buildRenderersFactory( - /* context= */ this, isNonNullAndChecked(preferExtensionDecodersMenuItem)); - downloadTracker.toggleDownload( - getSupportFragmentManager(), playlistHolder.mediaItems.get(0), renderersFactory); + toggleDownload(playlistHolder.mediaItems.get(0)); } } + private void toggleDownload(MediaItem mediaItem) { + RenderersFactory renderersFactory = + DemoUtil.buildRenderersFactory( + /* context= */ this, isNonNullAndChecked(preferExtensionDecodersMenuItem)); + downloadTracker.toggleDownload(getSupportFragmentManager(), mediaItem, renderersFactory); + } + private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) { if (playlistHolder.mediaItems.size() > 1) { return R.string.download_playlist_unsupported; @@ -630,4 +669,13 @@ public PlaylistGroup(String title) { this.playlists = new ArrayList<>(); } } + + @RequiresApi(33) + private static class Api33 { + + @DoNotInline + public static String getPostNotificationPermissionString() { + return Manifest.permission.POST_NOTIFICATIONS; + } + } } diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index 49441ef7dac..ce9c90d0c24 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -45,6 +45,8 @@ One or more sample lists failed to load + Notifications suppressed. Grant permission to see download notifications. + Failed to start download Failed to obtain offline license From 2c088269c66eacfa46d626b8f81035155db9b9ea Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 11 Jan 2023 20:19:00 +0000 Subject: [PATCH 101/141] Document that `DownloadService` needs notification permissions Starting with Android 13 (API 33) an app needs to request the permission to post notifications or notifications are suppressed. This change documents this in the class level JavaDoc of the `DownloadService`. Issue: google/ExoPlayer#10884 PiperOrigin-RevId: 501346908 (cherry picked from commit 20aa5bd9263f594e4f1f8029c5b80e9f204bff3a) --- .../media3/exoplayer/offline/DownloadService.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java index f2df8effff0..9b6e63be00e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadService.java @@ -40,7 +40,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; -/** A {@link Service} for downloading media. */ +/** + * A {@link Service} for downloading media. + * + *

        Apps with target SDK 33 and greater need to add the {@code + * android.permission.POST_NOTIFICATIONS} permission to the manifest and request the permission at + * runtime before starting downloads. Without that permission granted by the user, notifications + * posted by this service are not displayed. See the + * official UI guide for more detailed information. + */ @UnstableApi public abstract class DownloadService extends Service { From b644c67924d4b868cd2c4f4c5555ce3cb0213316 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 11 Jan 2023 23:46:58 +0000 Subject: [PATCH 102/141] Add AdsLoader.focusSkipButton() This method allows to call through to `StreamManager.focus()` of the currently playing SSAI stream. PiperOrigin-RevId: 501399144 (cherry picked from commit 16285ca5dfd4461334f5e97d4de47ae07e49e883) --- RELEASENOTES.md | 3 ++ .../ImaServerSideAdInsertionMediaSource.java | 34 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cb41231c1a7..69a5f177a37 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -59,6 +59,9 @@ * Add a property `focusSkipButtonWhenAvailable` to the `ImaServerSideAdInsertionMediaSource.AdsLoader.Builder` to request focusing the skip button on TV devices and set it to true by default. + * Add a method `focusSkipButton()` to the + `ImaServerSideAdInsertionMediaSource.AdsLoader` to programmatically + request to focus the skip button. * Bump IMA SDK version to 3.29.0. * Demo app * Request notification permission for download notifications at runtime diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index 831bf183be7..959d873cf84 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -376,8 +376,7 @@ private static State fromBundle(Bundle bundle) { private final ServerSideAdInsertionConfiguration configuration; private final Context context; - private final Map - mediaSourceResources; + private final Map mediaSourceResources; private final Map adPlaybackStateMap; @Nullable private Player player; @@ -403,6 +402,35 @@ public void setPlayer(Player player) { this.player = player; } + /** + * Puts the focus on the skip button, if a skip button is present and an ad is playing. + * + * @see StreamManager#focus() + */ + public void focusSkipButton() { + if (player == null) { + return; + } + if (player.getPlaybackState() != Player.STATE_IDLE + && player.getPlaybackState() != Player.STATE_ENDED + && player.getMediaItemCount() > 0) { + int currentPeriodIndex = player.getCurrentPeriodIndex(); + Object adsId = + player + .getCurrentTimeline() + .getPeriod(currentPeriodIndex, new Timeline.Period()) + .getAdsId(); + if (adsId instanceof String) { + MediaSourceResourceHolder mediaSourceResourceHolder = mediaSourceResources.get(adsId); + if (mediaSourceResourceHolder != null + && mediaSourceResourceHolder.imaServerSideAdInsertionMediaSource.streamManager + != null) { + mediaSourceResourceHolder.imaServerSideAdInsertionMediaSource.streamManager.focus(); + } + } + } + } + /** * Releases resources. * @@ -429,7 +457,7 @@ private void addMediaSourceResources( StreamPlayer streamPlayer, com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) { mediaSourceResources.put( - mediaSource, new MediaSourceResourceHolder(mediaSource, streamPlayer, adsLoader)); + mediaSource.adsId, new MediaSourceResourceHolder(mediaSource, streamPlayer, adsLoader)); } private AdPlaybackState getAdPlaybackState(String adsId) { From a2aaad65a88c42a0fa3f1e671412035a874d5e2f Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Fri, 13 Jan 2023 11:25:35 +0000 Subject: [PATCH 103/141] Catch FgSStartNotAllowedException when playback resumes This fix applies to Android 12 and above. In this fix, the `MediaSessionService` will try to start in the foreground before the session playback resumes, if ForegroundServiceStartNotAllowedException is thrown, then the app can handle the exception with their customized implementation of MediaSessionService.Listener.onForegroundServiceStartNotAllowedException. If no exception thrown, the a media notification corresponding to paused state will be sent as the consequence of successfully starting in the foreground. And when the player actually resumes, another media notification corresponding to playing state will be sent. PiperOrigin-RevId: 501803930 (cherry picked from commit 0d0cd786264aa82bf9301d4bcde6e5c78e332340) --- .../session/MediaNotificationManager.java | 100 ++++++---- .../androidx/media3/session/MediaSession.java | 16 +- .../media3/session/MediaSessionImpl.java | 13 +- .../session/MediaSessionLegacyStub.java | 4 +- .../media3/session/MediaSessionService.java | 176 +++++++++++++++++- .../media3/session/MediaSessionStub.java | 15 +- 6 files changed, 277 insertions(+), 47 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java index 6ae6968d932..27c0cc4ece6 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java @@ -66,6 +66,7 @@ private int totalNotificationCount; @Nullable private MediaNotification mediaNotification; + private boolean startedInForeground; public MediaNotificationManager( MediaSessionService mediaSessionService, @@ -80,6 +81,7 @@ public MediaNotificationManager( startSelfIntent = new Intent(mediaSessionService, mediaSessionService.getClass()); controllerMap = new HashMap<>(); customLayoutMap = new HashMap<>(); + startedInForeground = false; } public void addSession(MediaSession session) { @@ -163,9 +165,14 @@ public void onFailure(Throwable t) { } } - public void updateNotification(MediaSession session) { - if (!mediaSessionService.isSessionAdded(session) - || !shouldShowNotification(session.getPlayer())) { + /** + * Updates the notification. + * + * @param session A session that needs notification update. + * @param startInForegroundRequired Whether the service is required to start in the foreground. + */ + public void updateNotification(MediaSession session, boolean startInForegroundRequired) { + if (!mediaSessionService.isSessionAdded(session) || !shouldShowNotification(session)) { maybeStopForegroundService(/* removeNotifications= */ true); return; } @@ -179,18 +186,27 @@ public void updateNotification(MediaSession session) { MediaNotification mediaNotification = this.mediaNotificationProvider.createNotification( session, checkStateNotNull(customLayoutMap.get(session)), actionFactory, callback); - updateNotificationInternal(session, mediaNotification); + updateNotificationInternal(session, mediaNotification, startInForegroundRequired); + } + + public boolean isStartedInForeground() { + return startedInForeground; } private void onNotificationUpdated( int notificationSequence, MediaSession session, MediaNotification mediaNotification) { if (notificationSequence == totalNotificationCount) { - updateNotificationInternal(session, mediaNotification); + boolean startInForegroundRequired = + MediaSessionService.shouldRunInForeground( + session, /* startInForegroundWhenPaused= */ false); + updateNotificationInternal(session, mediaNotification, startInForegroundRequired); } } private void updateNotificationInternal( - MediaSession session, MediaNotification mediaNotification) { + MediaSession session, + MediaNotification mediaNotification, + boolean startInForegroundRequired) { if (Util.SDK_INT >= 21) { // Call Notification.MediaStyle#setMediaSession() indirectly. android.media.session.MediaSession.Token fwkToken = @@ -199,17 +215,9 @@ private void updateNotificationInternal( mediaNotification.notification.extras.putParcelable( Notification.EXTRA_MEDIA_SESSION, fwkToken); } - this.mediaNotification = mediaNotification; - Player player = session.getPlayer(); - if (shouldRunInForeground(player)) { - ContextCompat.startForegroundService(mediaSessionService, startSelfIntent); - if (Util.SDK_INT >= 29) { - Api29.startForeground(mediaSessionService, mediaNotification); - } else { - mediaSessionService.startForeground( - mediaNotification.notificationId, mediaNotification.notification); - } + if (startInForegroundRequired) { + startForeground(mediaNotification); } else { maybeStopForegroundService(/* removeNotifications= */ false); notificationManagerCompat.notify( @@ -226,19 +234,12 @@ private void updateNotificationInternal( private void maybeStopForegroundService(boolean removeNotifications) { List sessions = mediaSessionService.getSessions(); for (int i = 0; i < sessions.size(); i++) { - if (shouldRunInForeground(sessions.get(i).getPlayer())) { + if (MediaSessionService.shouldRunInForeground( + sessions.get(i), /* startInForegroundWhenPaused= */ false)) { return; } } - // To hide the notification on all API levels, we need to call both Service.stopForeground(true) - // and notificationManagerCompat.cancel(notificationId). - if (Util.SDK_INT >= 24) { - Api24.stopForeground(mediaSessionService, removeNotifications); - } else { - // For pre-L devices, we must call Service.stopForeground(true) anyway as a workaround - // that prevents the media notification from being undismissable. - mediaSessionService.stopForeground(removeNotifications || Util.SDK_INT < 21); - } + stopForeground(removeNotifications); if (removeNotifications && mediaNotification != null) { notificationManagerCompat.cancel(mediaNotification.notificationId); // Update the notification count so that if a pending notification callback arrives (e.g., a @@ -248,16 +249,11 @@ private void maybeStopForegroundService(boolean removeNotifications) { } } - private static boolean shouldShowNotification(Player player) { + private static boolean shouldShowNotification(MediaSession session) { + Player player = session.getPlayer(); return !player.getCurrentTimeline().isEmpty() && player.getPlaybackState() != Player.STATE_IDLE; } - private static boolean shouldRunInForeground(Player player) { - return player.getPlayWhenReady() - && (player.getPlaybackState() == Player.STATE_READY - || player.getPlaybackState() == Player.STATE_BUFFERING); - } - private static final class MediaControllerListener implements MediaController.Listener, Player.Listener { private final MediaSessionService mediaSessionService; @@ -274,8 +270,9 @@ public MediaControllerListener( } public void onConnected() { - if (shouldShowNotification(session.getPlayer())) { - mediaSessionService.onUpdateNotification(session); + if (shouldShowNotification(session)) { + mediaSessionService.onUpdateNotificationInternal( + session, /* startInForegroundWhenPaused= */ false); } } @@ -283,7 +280,8 @@ public void onConnected() { public ListenableFuture onSetCustomLayout( MediaController controller, List layout) { customLayoutMap.put(session, ImmutableList.copyOf(layout)); - mediaSessionService.onUpdateNotification(session); + mediaSessionService.onUpdateNotificationInternal( + session, /* startInForegroundWhenPaused= */ false); return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); } @@ -296,7 +294,8 @@ public void onEvents(Player player, Player.Events events) { Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_MEDIA_METADATA_CHANGED, Player.EVENT_TIMELINE_CHANGED)) { - mediaSessionService.onUpdateNotification(session); + mediaSessionService.onUpdateNotificationInternal( + session, /* startInForegroundWhenPaused= */ false); } } @@ -304,8 +303,33 @@ public void onEvents(Player player, Player.Events events) { public void onDisconnected(MediaController controller) { mediaSessionService.removeSession(session); // We may need to hide the notification. - mediaSessionService.onUpdateNotification(session); + mediaSessionService.onUpdateNotificationInternal( + session, /* startInForegroundWhenPaused= */ false); + } + } + + private void startForeground(MediaNotification mediaNotification) { + ContextCompat.startForegroundService(mediaSessionService, startSelfIntent); + if (Util.SDK_INT >= 29) { + Api29.startForeground(mediaSessionService, mediaNotification); + } else { + mediaSessionService.startForeground( + mediaNotification.notificationId, mediaNotification.notification); + } + startedInForeground = true; + } + + private void stopForeground(boolean removeNotifications) { + // To hide the notification on all API levels, we need to call both Service.stopForeground(true) + // and notificationManagerCompat.cancel(notificationId). + if (Util.SDK_INT >= 24) { + Api24.stopForeground(mediaSessionService, removeNotifications); + } else { + // For pre-L devices, we must call Service.stopForeground(true) anyway as a workaround + // that prevents the media notification from being undismissable. + mediaSessionService.stopForeground(removeNotifications || Util.SDK_INT < 21); } + startedInForeground = false; } @RequiresApi(24) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index d03f24e0bff..5fead90f84f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -877,10 +877,15 @@ public MediaSessionCompat.Token getSessionCompatToken() { } /** Sets the {@linkplain Listener listener}. */ - /* package */ void setListener(@Nullable Listener listener) { + /* package */ void setListener(Listener listener) { impl.setMediaSessionListener(listener); } + /** Clears the {@linkplain Listener listener}. */ + /* package */ void clearListener() { + impl.clearMediaSessionListener(); + } + private Uri getUri() { return impl.getUri(); } @@ -1272,6 +1277,15 @@ default void onRenderedFirstFrame(int seq) throws RemoteException {} * @param session The media session for which the notification requires to be refreshed. */ void onNotificationRefreshRequired(MediaSession session); + + /** + * Called when the {@linkplain MediaSession session} receives the play command and requests from + * the listener on whether the media can be played. + * + * @param session The media session which requests if the media can be played. + * @return True if the media can be played, false otherwise. + */ + boolean onPlayRequested(MediaSession session); } /** diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 4e075c18bd9..ec03a8525e9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -579,16 +579,27 @@ protected MediaSessionServiceLegacyStub getLegacyBrowserService() { } } - /* package */ void setMediaSessionListener(@Nullable MediaSession.Listener listener) { + /* package */ void setMediaSessionListener(MediaSession.Listener listener) { this.mediaSessionListener = listener; } + /* package */ void clearMediaSessionListener() { + this.mediaSessionListener = null; + } + /* package */ void onNotificationRefreshRequired() { if (this.mediaSessionListener != null) { this.mediaSessionListener.onNotificationRefreshRequired(instance); } } + /* package */ boolean onPlayRequested() { + if (this.mediaSessionListener != null) { + return this.mediaSessionListener.onPlayRequested(instance); + } + return true; + } + private void dispatchRemoteControllerTaskToLegacyStub(RemoteControllerTask task) { try { task.run(sessionLegacyStub.getControllerLegacyCbForBroadcast(), /* seq= */ 0); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 069534ffad4..3c25022e9d1 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -313,7 +313,9 @@ public void onPlay() { playerWrapper.seekTo( playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); } - playerWrapper.play(); + if (sessionImpl.onPlayRequested()) { + playerWrapper.play(); + } }, sessionCompat.getCurrentControllerInfo()); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java index 0e8d21cca4b..a93a6cf8a79 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.postOrRun; +import android.app.ForegroundServiceStartNotAllowedException; import android.app.Service; import android.content.Context; import android.content.Intent; @@ -32,13 +33,17 @@ import android.os.RemoteException; import android.view.KeyEvent; import androidx.annotation.CallSuper; +import androidx.annotation.DoNotInline; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.collection.ArrayMap; import androidx.media.MediaBrowserServiceCompat; import androidx.media.MediaSessionManager; +import androidx.media3.common.Player; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import androidx.media3.session.MediaSession.ControllerInfo; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -134,6 +139,21 @@ */ public abstract class MediaSessionService extends Service { + /** + * Listener for {@link MediaSessionService}. + * + *

        The methods will be called on the main thread. + */ + @UnstableApi + public interface Listener { + /** + * Called when the service fails to start in the foreground and a {@link + * ForegroundServiceStartNotAllowedException} is thrown on Android 12 or later. + */ + @RequiresApi(31) + default void onForegroundServiceStartNotAllowedException() {} + } + /** The action for {@link Intent} filter that must be declared by the service. */ public static final String SERVICE_INTERFACE = "androidx.media3.session.MediaSessionService"; @@ -158,11 +178,19 @@ public abstract class MediaSessionService extends Service { @GuardedBy("lock") private @MonotonicNonNull DefaultActionFactory actionFactory; + @GuardedBy("lock") + @Nullable + private Listener listener; + + @GuardedBy("lock") + private boolean defaultMethodCalled; + /** Creates a service. */ public MediaSessionService() { lock = new Object(); mainHandler = new Handler(Looper.getMainLooper()); sessions = new ArrayMap<>(); + defaultMethodCalled = false; } /** @@ -239,7 +267,7 @@ public final void addSession(MediaSession session) { // TODO(b/191644474): Check whether the session is registered to multiple services. MediaNotificationManager notificationManager = getMediaNotificationManager(); postOrRun(mainHandler, () -> notificationManager.addSession(session)); - session.setListener(this::onUpdateNotification); + session.setListener(new MediaSessionListener()); } } @@ -259,7 +287,7 @@ public final void removeSession(MediaSession session) { } MediaNotificationManager notificationManager = getMediaNotificationManager(); postOrRun(mainHandler, () -> notificationManager.removeSession(session)); - session.setListener(null); + session.clearListener(); } /** @@ -282,6 +310,22 @@ public final boolean isSessionAdded(MediaSession session) { } } + /** Sets the {@linkplain Listener listener}. */ + @UnstableApi + public final void setListener(Listener listener) { + synchronized (lock) { + this.listener = listener; + } + } + + /** Clears the {@linkplain Listener listener}. */ + @UnstableApi + public final void clearListener() { + synchronized (lock) { + this.listener = null; + } + } + /** * Called when a component is about to bind to the service. * @@ -395,8 +439,10 @@ public void onDestroy() { *

        Override this method to create your own notification and customize the foreground handling * of your service. * - *

        The default implementation will present a default notification or the notification provided - * by the {@link MediaNotification.Provider} that is {@link + *

        At most one of {@link #onUpdateNotification(MediaSession, boolean)} and this method should + * be overridden. If neither of the two methods is overridden, the default implementation will + * present a default notification or the notification provided by the {@link + * MediaNotification.Provider} that is {@link * #setMediaNotificationProvider(MediaNotification.Provider) set} by the app. Further, the service * is started in the foreground when @@ -408,7 +454,42 @@ public void onDestroy() { * @param session A session that needs notification update. */ public void onUpdateNotification(MediaSession session) { - getMediaNotificationManager().updateNotification(session); + setDefaultMethodCalled(true); + } + + /** + * Called when a notification needs to be updated. Override this method to show or cancel your own + * notifications. + * + *

        This method is called whenever the service has detected a change that requires to show, + * update or cancel a notification with a flag {@code startInForegroundRequired} suggested by the + * service whether starting in the foreground is required. The method will be called on the + * application thread of the app that the service belongs to. + * + *

        Override this method to create your own notification and customize the foreground handling + * of your service. + * + *

        At most one of {@link #onUpdateNotification(MediaSession)} and this method should be + * overridden. If neither of the two methods is overridden, the default implementation will + * present a default notification or the notification provided by the {@link + * MediaNotification.Provider} that is {@link + * #setMediaNotificationProvider(MediaNotification.Provider) set} by the app. Further, the service + * is started in the foreground when + * playback is ongoing and put back into background otherwise. + * + *

        Apps targeting {@code SDK_INT >= 28} must request the permission, {@link + * android.Manifest.permission#FOREGROUND_SERVICE}. + * + * @param session A session that needs notification update. + * @param startInForegroundRequired Whether the service is required to start in the foreground. + */ + @UnstableApi + public void onUpdateNotification(MediaSession session, boolean startInForegroundRequired) { + onUpdateNotification(session); + if (isDefaultMethodCalled()) { + getMediaNotificationManager().updateNotification(session, startInForegroundRequired); + } } /** @@ -431,6 +512,31 @@ protected final void setMediaNotificationProvider( } } + /* package */ boolean onUpdateNotificationInternal( + MediaSession session, boolean startInForegroundWhenPaused) { + try { + boolean startInForegroundRequired = + shouldRunInForeground(session, startInForegroundWhenPaused); + onUpdateNotification(session, startInForegroundRequired); + } catch (/* ForegroundServiceStartNotAllowedException */ IllegalStateException e) { + if ((Util.SDK_INT >= 31) && Api31.instanceOfForegroundServiceStartNotAllowedException(e)) { + Log.e(TAG, "Failed to start foreground", e); + onForegroundServiceStartNotAllowedException(); + return false; + } + throw e; + } + return true; + } + + /* package */ static boolean shouldRunInForeground( + MediaSession session, boolean startInForegroundWhenPaused) { + Player player = session.getPlayer(); + return (player.getPlayWhenReady() || startInForegroundWhenPaused) + && (player.getPlaybackState() == Player.STATE_READY + || player.getPlaybackState() == Player.STATE_BUFFERING); + } + private MediaNotificationManager getMediaNotificationManager() { synchronized (lock) { if (mediaNotificationManager == null) { @@ -455,6 +561,57 @@ private DefaultActionFactory getActionFactory() { } } + @Nullable + private Listener getListener() { + synchronized (lock) { + return this.listener; + } + } + + private boolean isDefaultMethodCalled() { + synchronized (lock) { + return this.defaultMethodCalled; + } + } + + private void setDefaultMethodCalled(boolean defaultMethodCalled) { + synchronized (lock) { + this.defaultMethodCalled = defaultMethodCalled; + } + } + + @RequiresApi(31) + private void onForegroundServiceStartNotAllowedException() { + mainHandler.post( + () -> { + @Nullable MediaSessionService.Listener serviceListener = getListener(); + if (serviceListener != null) { + serviceListener.onForegroundServiceStartNotAllowedException(); + } + }); + } + + private final class MediaSessionListener implements MediaSession.Listener { + + @Override + public void onNotificationRefreshRequired(MediaSession session) { + MediaSessionService.this.onUpdateNotificationInternal( + session, /* startInForegroundWhenPaused= */ false); + } + + @Override + public boolean onPlayRequested(MediaSession session) { + if (Util.SDK_INT < 31 || Util.SDK_INT >= 33) { + return true; + } + // Check if service can start foreground successfully on Android 12 and 12L. + if (!getMediaNotificationManager().isStartedInForeground()) { + return onUpdateNotificationInternal(session, /* startInForegroundWhenPaused= */ true); + } + return true; + } + } + private static final class MediaSessionServiceStub extends IMediaSessionService.Stub { private final WeakReference serviceReference; @@ -575,4 +732,13 @@ public void release() { } } } + + @RequiresApi(31) + private static final class Api31 { + @DoNotInline + public static boolean instanceOfForegroundServiceStartNotAllowedException( + IllegalStateException e) { + return e instanceof ForegroundServiceStartNotAllowedException; + } + } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index b13b4d61fb5..866e92d80ea 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -611,7 +611,20 @@ public void play(@Nullable IMediaController caller, int sequenceNumber) throws R return; } queueSessionTaskWithPlayerCommand( - caller, sequenceNumber, COMMAND_PLAY_PAUSE, sendSessionResultSuccess(Player::play)); + caller, + sequenceNumber, + COMMAND_PLAY_PAUSE, + sendSessionResultSuccess( + player -> { + @Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get(); + if (sessionImpl == null || sessionImpl.isReleased()) { + return; + } + + if (sessionImpl.onPlayRequested()) { + player.play(); + } + })); } @Override From 13dc59fc0fef2574a5fffcd30caeeaeb91f5d6c8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 16 Jan 2023 11:16:03 +0000 Subject: [PATCH 104/141] Correctly map deprecated methods in MediaController to replacement This avoids throwing exceptions for correct (but deprecated) Player method invocations. PiperOrigin-RevId: 502341428 (cherry picked from commit 86a95c2a4afd861986376f9dc31e0d65910e6e74) --- .../media3/session/MediaController.java | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index e9855421d68..529be85dcc2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -478,7 +478,10 @@ public void stop() { @Deprecated @Override public void stop(boolean reset) { - throw new UnsupportedOperationException(); + stop(); + if (reset) { + clearMediaItems(); + } } /** @@ -1174,7 +1177,7 @@ public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { @Deprecated @Override public boolean isCurrentWindowDynamic() { - throw new UnsupportedOperationException(); + return isCurrentMediaItemDynamic(); } @Override @@ -1191,7 +1194,7 @@ public boolean isCurrentMediaItemDynamic() { @Deprecated @Override public boolean isCurrentWindowLive() { - throw new UnsupportedOperationException(); + return isCurrentMediaItemLive(); } @Override @@ -1208,7 +1211,7 @@ public boolean isCurrentMediaItemLive() { @Deprecated @Override public boolean isCurrentWindowSeekable() { - throw new UnsupportedOperationException(); + return isCurrentMediaItemSeekable(); } @Override @@ -1260,7 +1263,7 @@ public int getCurrentPeriodIndex() { @Deprecated @Override public int getCurrentWindowIndex() { - throw new UnsupportedOperationException(); + return getCurrentMediaItemIndex(); } @Override @@ -1276,7 +1279,7 @@ public int getCurrentMediaItemIndex() { @Deprecated @Override public int getPreviousWindowIndex() { - throw new UnsupportedOperationException(); + return getPreviousMediaItemIndex(); } /** @@ -1299,7 +1302,7 @@ public int getPreviousMediaItemIndex() { @Deprecated @Override public int getNextWindowIndex() { - throw new UnsupportedOperationException(); + return getNextMediaItemIndex(); } /** @@ -1322,7 +1325,7 @@ public int getNextMediaItemIndex() { @Deprecated @Override public boolean hasPrevious() { - throw new UnsupportedOperationException(); + return hasPreviousMediaItem(); } /** @@ -1332,7 +1335,7 @@ public boolean hasPrevious() { @Deprecated @Override public boolean hasNext() { - throw new UnsupportedOperationException(); + return hasNextMediaItem(); } /** @@ -1342,7 +1345,7 @@ public boolean hasNext() { @Deprecated @Override public boolean hasPreviousWindow() { - throw new UnsupportedOperationException(); + return hasPreviousMediaItem(); } /** @@ -1352,7 +1355,7 @@ public boolean hasPreviousWindow() { @Deprecated @Override public boolean hasNextWindow() { - throw new UnsupportedOperationException(); + return hasNextMediaItem(); } @Override @@ -1374,7 +1377,7 @@ public boolean hasNextMediaItem() { @Deprecated @Override public void previous() { - throw new UnsupportedOperationException(); + seekToPreviousMediaItem(); } /** @@ -1384,7 +1387,7 @@ public void previous() { @Deprecated @Override public void next() { - throw new UnsupportedOperationException(); + seekToNextMediaItem(); } /** @@ -1394,7 +1397,7 @@ public void next() { @Deprecated @Override public void seekToPreviousWindow() { - throw new UnsupportedOperationException(); + seekToPreviousMediaItem(); } /** @@ -1420,7 +1423,7 @@ public void seekToPreviousMediaItem() { @Deprecated @Override public void seekToNextWindow() { - throw new UnsupportedOperationException(); + seekToNextMediaItem(); } /** From 55903af2f8b72195f706ef91d32da12108833c62 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 16 Jan 2023 11:19:13 +0000 Subject: [PATCH 105/141] Remove unneccesary parameter taking Player.Command The method to dispatch actions in MediaControllerImplBase takes a Player.Command, but the value is only used to check if we are setting a surface and need to handle the special blocking call. This can be cleaned up by removing the parameter and calling a dedicated blocking method where needed. This also ensures we have to mention the relevant Player.Command only once in each method. PiperOrigin-RevId: 502341862 (cherry picked from commit 664ab72d090196625b5f533e9f0a2112951c5741) --- .../session/MediaControllerImplBase.java | 221 ++++++------------ 1 file changed, 66 insertions(+), 155 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index e3a2ca4a339..c16a8a61d8e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -91,7 +91,6 @@ import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; import org.checkerframework.checker.initialization.qual.UnderInitialization; import org.checkerframework.checker.nullness.qual.NonNull; @@ -217,13 +216,7 @@ public void stop() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_STOP, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.stop(controllerStub, seq); - } - }); + (iSession, seq) -> iSession.stop(controllerStub, seq)); playerInfo = playerInfo.copyWithSessionPositionInfo( @@ -306,12 +299,31 @@ private interface RemoteSessionTask { void run(IMediaSession iSession, int seq) throws RemoteException; } - private ListenableFuture dispatchRemoteSessionTaskWithPlayerCommand( - @Player.Command int command, RemoteSessionTask task) { - if (command != Player.COMMAND_SET_VIDEO_SURFACE) { - flushCommandQueueHandler.sendFlushCommandQueueMessage(); + private void dispatchRemoteSessionTaskWithPlayerCommand(RemoteSessionTask task) { + flushCommandQueueHandler.sendFlushCommandQueueMessage(); + dispatchRemoteSessionTask(iSession, task, /* addToPendingMaskingOperations= */ true); + } + + private void dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture(RemoteSessionTask task) { + // Do not send a flush command queue message as we are actively waiting for task. + ListenableFuture future = + dispatchRemoteSessionTask(iSession, task, /* addToPendingMaskingOperations= */ true); + try { + MediaUtils.getFutureResult(future, /* timeoutMs= */ 3_000); + } catch (ExecutionException e) { + // Never happens because future.setException will not be called. + throw new IllegalStateException(e); + } catch (TimeoutException e) { + if (future instanceof SequencedFutureManager.SequencedFuture) { + int sequenceNumber = + ((SequencedFutureManager.SequencedFuture) future).getSequenceNumber(); + pendingMaskingSequencedFutureNumbers.remove(sequenceNumber); + sequencedFutureManager.setFutureResult( + sequenceNumber, new SessionResult(SessionResult.RESULT_ERROR_UNKNOWN)); + } + Log.w(TAG, "Synchronous command takes too long on the session side.", e); + // TODO(b/188888693): Let developers know the failure in their code. } - return dispatchRemoteSessionTask(iSession, task, /* addToPendingMaskingOperations= */ true); } private ListenableFuture dispatchRemoteSessionTaskWithSessionCommand( @@ -328,7 +340,7 @@ private ListenableFuture dispatchRemoteSessionTaskWithSessionComm private ListenableFuture dispatchRemoteSessionTaskWithSessionCommandInternal( @SessionCommand.CommandCode int commandCode, - SessionCommand sessionCommand, + @Nullable SessionCommand sessionCommand, RemoteSessionTask task) { return dispatchRemoteSessionTask( sessionCommand != null @@ -339,7 +351,9 @@ private ListenableFuture dispatchRemoteSessionTaskWithSessionComm } private ListenableFuture dispatchRemoteSessionTask( - IMediaSession iSession, RemoteSessionTask task, boolean addToPendingMaskingOperations) { + @Nullable IMediaSession iSession, + RemoteSessionTask task, + boolean addToPendingMaskingOperations) { if (iSession != null) { SequencedFutureManager.SequencedFuture result = sequencedFutureManager.createSequencedFuture( @@ -373,13 +387,7 @@ public void play() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_PLAY_PAUSE, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.play(controllerStub, seq); - } - }); + (iSession, seq) -> iSession.play(controllerStub, seq)); setPlayWhenReady( /* playWhenReady= */ true, @@ -394,13 +402,7 @@ public void pause() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_PLAY_PAUSE, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.pause(controllerStub, seq); - } - }); + (iSession, seq) -> iSession.pause(controllerStub, seq)); setPlayWhenReady( /* playWhenReady= */ false, @@ -415,13 +417,7 @@ public void prepare() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_PREPARE, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.prepare(controllerStub, seq); - } - }); + (iSession, seq) -> iSession.prepare(controllerStub, seq)); if (playerInfo.playbackState == Player.STATE_IDLE) { PlayerInfo playerInfo = @@ -447,13 +443,7 @@ public void seekToDefaultPosition() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_TO_DEFAULT_POSITION, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.seekToDefaultPosition(controllerStub, seq); - } - }); + (iSession, seq) -> iSession.seekToDefaultPosition(controllerStub, seq)); seekToInternal(getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); } @@ -466,13 +456,8 @@ public void seekToDefaultPosition(int mediaItemIndex) { checkArgument(mediaItemIndex >= 0); dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_TO_MEDIA_ITEM, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.seekToDefaultPositionWithMediaItemIndex(controllerStub, seq, mediaItemIndex); - } - }); + (iSession, seq) -> + iSession.seekToDefaultPositionWithMediaItemIndex(controllerStub, seq, mediaItemIndex)); seekToInternal(mediaItemIndex, /* positionMs= */ C.TIME_UNSET); } @@ -484,13 +469,7 @@ public void seekTo(long positionMs) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.seekTo(controllerStub, seq, positionMs); - } - }); + (iSession, seq) -> iSession.seekTo(controllerStub, seq, positionMs)); seekToInternal(getCurrentMediaItemIndex(), positionMs); } @@ -503,13 +482,8 @@ public void seekTo(int mediaItemIndex, long positionMs) { checkArgument(mediaItemIndex >= 0); dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_TO_MEDIA_ITEM, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.seekToWithMediaItemIndex(controllerStub, seq, mediaItemIndex, positionMs); - } - }); + (iSession, seq) -> + iSession.seekToWithMediaItemIndex(controllerStub, seq, mediaItemIndex, positionMs)); seekToInternal(mediaItemIndex, positionMs); } @@ -526,7 +500,7 @@ public void seekBack() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_BACK, (iSession, seq) -> iSession.seekBack(controllerStub, seq)); + (iSession, seq) -> iSession.seekBack(controllerStub, seq)); seekToInternalByOffset(-getSeekBackIncrement()); } @@ -543,7 +517,7 @@ public void seekForward() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_FORWARD, (iSession, seq) -> iSession.seekForward(controllerStub, seq)); + (iSession, seq) -> iSession.seekForward(controllerStub, seq)); seekToInternalByOffset(getSeekForwardIncrement()); } @@ -560,7 +534,6 @@ public void setPlayWhenReady(boolean playWhenReady) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_PLAY_PAUSE, (iSession, seq) -> iSession.setPlayWhenReady(controllerStub, seq, playWhenReady)); setPlayWhenReady( @@ -697,7 +670,6 @@ public void setPlaybackParameters(PlaybackParameters playbackParameters) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_SPEED_AND_PITCH, (iSession, seq) -> iSession.setPlaybackParameters(controllerStub, seq, playbackParameters.toBundle())); @@ -723,7 +695,6 @@ public void setPlaybackSpeed(float speed) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_SPEED_AND_PITCH, (iSession, seq) -> iSession.setPlaybackSpeed(controllerStub, seq, speed)); if (playerInfo.playbackParameters.speed != speed) { @@ -746,24 +717,15 @@ public AudioAttributes getAudioAttributes() { public ListenableFuture setRating(String mediaId, Rating rating) { return dispatchRemoteSessionTaskWithSessionCommand( SessionCommand.COMMAND_CODE_SESSION_SET_RATING, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.setRatingWithMediaId(controllerStub, seq, mediaId, rating.toBundle()); - } - }); + (iSession, seq) -> + iSession.setRatingWithMediaId(controllerStub, seq, mediaId, rating.toBundle())); } @Override public ListenableFuture setRating(Rating rating) { return dispatchRemoteSessionTaskWithSessionCommand( SessionCommand.COMMAND_CODE_SESSION_SET_RATING, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.setRating(controllerStub, seq, rating.toBundle()); - } - }); + (iSession, seq) -> iSession.setRating(controllerStub, seq, rating.toBundle())); } @Override @@ -785,7 +747,6 @@ public void setMediaItem(MediaItem mediaItem) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_MEDIA_ITEM, (iSession, seq) -> iSession.setMediaItem(controllerStub, seq, mediaItem.toBundle())); setMediaItemsInternal( @@ -802,7 +763,6 @@ public void setMediaItem(MediaItem mediaItem, long startPositionMs) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_MEDIA_ITEM, (iSession, seq) -> iSession.setMediaItemWithStartPosition( controllerStub, seq, mediaItem.toBundle(), startPositionMs)); @@ -821,7 +781,6 @@ public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_MEDIA_ITEM, (iSession, seq) -> iSession.setMediaItemWithResetPosition( controllerStub, seq, mediaItem.toBundle(), resetPosition)); @@ -840,7 +799,6 @@ public void setMediaItems(List mediaItems) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.setMediaItems( controllerStub, @@ -861,7 +819,6 @@ public void setMediaItems(List mediaItems, boolean resetPosition) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.setMediaItemsWithResetPosition( controllerStub, @@ -883,7 +840,6 @@ public void setMediaItems(List mediaItems, int startIndex, long start } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.setMediaItemsWithStartIndex( controllerStub, @@ -903,7 +859,6 @@ public void setPlaylistMetadata(MediaMetadata playlistMetadata) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_MEDIA_ITEMS_METADATA, (iSession, seq) -> iSession.setPlaylistMetadata(controllerStub, seq, playlistMetadata.toBundle())); @@ -928,7 +883,6 @@ public void addMediaItem(MediaItem mediaItem) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.addMediaItem(controllerStub, seq, mediaItem.toBundle())); addMediaItemsInternal( @@ -943,7 +897,6 @@ public void addMediaItem(int index, MediaItem mediaItem) { checkArgument(index >= 0); dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.addMediaItemWithIndex(controllerStub, seq, index, mediaItem.toBundle())); @@ -957,7 +910,6 @@ public void addMediaItems(List mediaItems) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.addMediaItems( controllerStub, @@ -975,7 +927,6 @@ public void addMediaItems(int index, List mediaItems) { checkArgument(index >= 0); dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.addMediaItemsWithIndex( controllerStub, @@ -1045,7 +996,6 @@ public void removeMediaItem(int index) { checkArgument(index >= 0); dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.removeMediaItem(controllerStub, seq, index)); removeMediaItemsInternal(/* fromIndex= */ index, /* toIndex= */ index + 1); @@ -1059,7 +1009,6 @@ public void removeMediaItems(int fromIndex, int toIndex) { checkArgument(fromIndex >= 0 && toIndex >= fromIndex); dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.removeMediaItems(controllerStub, seq, fromIndex, toIndex)); removeMediaItemsInternal(fromIndex, toIndex); @@ -1072,7 +1021,6 @@ public void clearMediaItems() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.clearMediaItems(controllerStub, seq)); removeMediaItemsInternal(/* fromIndex= */ 0, /* toIndex= */ Integer.MAX_VALUE); @@ -1224,7 +1172,6 @@ public void moveMediaItem(int currentIndex, int newIndex) { checkArgument(currentIndex >= 0 && newIndex >= 0); dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.moveMediaItem(controllerStub, seq, currentIndex, newIndex)); moveMediaItemsInternal( @@ -1239,7 +1186,6 @@ public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { checkArgument(fromIndex >= 0 && fromIndex <= toIndex && newIndex >= 0); dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.moveMediaItems(controllerStub, seq, fromIndex, toIndex, newIndex)); @@ -1297,7 +1243,6 @@ public void seekToPreviousMediaItem() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, (iSession, seq) -> iSession.seekToPreviousMediaItem(controllerStub, seq)); if (getPreviousMediaItemIndex() != C.INDEX_UNSET) { @@ -1312,7 +1257,6 @@ public void seekToNextMediaItem() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, (iSession, seq) -> iSession.seekToNextMediaItem(controllerStub, seq)); if (getNextMediaItemIndex() != C.INDEX_UNSET) { @@ -1327,7 +1271,6 @@ public void seekToPrevious() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_TO_PREVIOUS, (iSession, seq) -> iSession.seekToPrevious(controllerStub, seq)); Timeline timeline = getCurrentTimeline(); @@ -1359,7 +1302,7 @@ public void seekToNext() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_TO_NEXT, (iSession, seq) -> iSession.seekToNext(controllerStub, seq)); + (iSession, seq) -> iSession.seekToNext(controllerStub, seq)); Timeline timeline = getCurrentTimeline(); if (timeline.isEmpty() || isPlayingAd()) { @@ -1387,13 +1330,7 @@ public void setRepeatMode(@Player.RepeatMode int repeatMode) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_REPEAT_MODE, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.setRepeatMode(controllerStub, seq, repeatMode); - } - }); + (iSession, seq) -> iSession.setRepeatMode(controllerStub, seq, repeatMode)); if (playerInfo.repeatMode != repeatMode) { playerInfo = playerInfo.copyWithRepeatMode(repeatMode); @@ -1417,13 +1354,7 @@ public void setShuffleModeEnabled(boolean shuffleModeEnabled) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_SHUFFLE_MODE, - new RemoteSessionTask() { - @Override - public void run(IMediaSession iSession, int seq) throws RemoteException { - iSession.setShuffleModeEnabled(controllerStub, seq, shuffleModeEnabled); - } - }); + (iSession, seq) -> iSession.setShuffleModeEnabled(controllerStub, seq, shuffleModeEnabled)); if (playerInfo.shuffleModeEnabled != shuffleModeEnabled) { playerInfo = playerInfo.copyWithShuffleModeEnabled(shuffleModeEnabled); @@ -1452,7 +1383,6 @@ public void setVolume(float volume) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_VOLUME, (iSession, seq) -> iSession.setVolume(controllerStub, seq, volume)); if (playerInfo.volume != volume) { @@ -1486,7 +1416,6 @@ public void setDeviceVolume(int volume) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_DEVICE_VOLUME, (iSession, seq) -> iSession.setDeviceVolume(controllerStub, seq, volume)); if (playerInfo.deviceVolume != volume) { @@ -1506,7 +1435,6 @@ public void increaseDeviceVolume() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_ADJUST_DEVICE_VOLUME, (iSession, seq) -> iSession.increaseDeviceVolume(controllerStub, seq)); int newDeviceVolume = playerInfo.deviceVolume + 1; @@ -1526,7 +1454,6 @@ public void decreaseDeviceVolume() { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_ADJUST_DEVICE_VOLUME, (iSession, seq) -> iSession.decreaseDeviceVolume(controllerStub, seq)); int newDeviceVolume = playerInfo.deviceVolume - 1; @@ -1546,7 +1473,6 @@ public void setDeviceMuted(boolean muted) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_DEVICE_VOLUME, (iSession, seq) -> iSession.setDeviceMuted(controllerStub, seq, muted)); if (playerInfo.deviceMuted != muted) { @@ -1575,7 +1501,8 @@ public void clearVideoSurface() { } clearSurfacesAndCallbacks(); - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null); + /* surface= */ dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, null)); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } @@ -1599,7 +1526,8 @@ public void setVideoSurface(@Nullable Surface surface) { clearSurfacesAndCallbacks(); videoSurface = surface; - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(surface); + dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, surface)); int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET; maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize); } @@ -1625,12 +1553,14 @@ public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { @Nullable Surface surface = surfaceHolder.getSurface(); if (surface != null && surface.isValid()) { videoSurface = surface; - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(surface); + dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, surface)); Rect surfaceSize = surfaceHolder.getSurfaceFrame(); maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height()); } else { videoSurface = null; - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null); + /* surface= */ dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, null)); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } } @@ -1688,11 +1618,13 @@ public void setVideoTextureView(@Nullable TextureView textureView) { @Nullable SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); if (surfaceTexture == null) { - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null); + /* surface= */ dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, null)); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } else { videoSurface = new Surface(surfaceTexture); - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(videoSurface); + dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, videoSurface)); maybeNotifySurfaceSizeChanged(textureView.getWidth(), textureView.getHeight()); } } @@ -1736,7 +1668,6 @@ public void setTrackSelectionParameters(TrackSelectionParameters parameters) { } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, (iSession, seq) -> iSession.setTrackSelectionParameters(controllerStub, seq, parameters.toBundle())); @@ -2157,30 +2088,6 @@ private boolean requestConnectToSession(Bundle connectionHints) { return true; } - private void dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(@Nullable Surface surface) { - Future future = - dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_VIDEO_SURFACE, - (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, surface)); - - try { - MediaUtils.getFutureResult(future, /* timeoutMs= */ 3_000); - } catch (ExecutionException e) { - // Never happens because future.setException will not be called. - throw new IllegalStateException(e); - } catch (TimeoutException e) { - if (future instanceof SequencedFutureManager.SequencedFuture) { - int sequenceNumber = - ((SequencedFutureManager.SequencedFuture) future).getSequenceNumber(); - pendingMaskingSequencedFutureNumbers.remove(sequenceNumber); - sequencedFutureManager.setFutureResult( - sequenceNumber, new SessionResult(SessionResult.RESULT_ERROR_UNKNOWN)); - } - Log.w(TAG, "set/clearVideoSurface takes too long on the session side.", e); - // TODO(b/188888693): Let developers know the failure in their code. - } - } - private void clearSurfacesAndCallbacks() { if (videoTextureView != null) { videoTextureView.setSurfaceTextureListener(null); @@ -2988,7 +2895,8 @@ public void surfaceCreated(SurfaceHolder holder) { return; } videoSurface = holder.getSurface(); - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(videoSurface); + dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, videoSurface)); Rect surfaceSize = holder.getSurfaceFrame(); maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height()); } @@ -3007,7 +2915,8 @@ public void surfaceDestroyed(SurfaceHolder holder) { return; } videoSurface = null; - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null); + /* surface= */ dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, null)); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } @@ -3019,7 +2928,8 @@ public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, return; } videoSurface = new Surface(surfaceTexture); - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(videoSurface); + dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, videoSurface)); maybeNotifySurfaceSizeChanged(width, height); } @@ -3037,7 +2947,8 @@ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { return true; } videoSurface = null; - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null); + /* surface= */ dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, null)); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); return true; } From 24b0367374f5030f4aeafc312534fbcdf997f517 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 16 Jan 2023 12:39:38 +0000 Subject: [PATCH 106/141] Add missing command checks to MediaSessionLegacyStub and PlayerWrapper This player didn't fully check all player commands before calling the respective methods. PiperOrigin-RevId: 502353704 (cherry picked from commit a2a44cdc02abadd473e26e1fd9f973210d4c5f0e) --- .../media3/session/MediaSessionImpl.java | 2 +- .../session/MediaSessionLegacyStub.java | 92 ++++--- .../media3/session/PlayerWrapper.java | 197 +++++++++++++-- .../media3/session/PlayerWrapperTest.java | 2 + ...CallbackWithMediaControllerCompatTest.java | 231 +++++++++++++++++- .../session/MediaSessionKeyEventTest.java | 2 +- .../session/MediaSessionPermissionTest.java | 169 +++++++++++++ 7 files changed, 638 insertions(+), 57 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index ec03a8525e9..7ad0de53e35 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -1289,7 +1289,7 @@ public void handleMessage(Message msg) { if (msg.what == MSG_PLAYER_INFO_CHANGED) { playerInfo = playerInfo.copyWithTimelineAndSessionPositionInfo( - getPlayerWrapper().getCurrentTimeline(), + getPlayerWrapper().getCurrentTimelineWithCommandCheck(), getPlayerWrapper().createSessionPositionInfoForBundling()); dispatchOnPlayerInfoChanged(playerInfo, excludeTimeline, excludeTracks); excludeTimeline = true; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 3c25022e9d1..e130763ea21 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -23,7 +23,9 @@ import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT; +import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; +import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; @@ -256,10 +258,9 @@ private void handleMediaPlayPauseOnHandler(RemoteUserInfo remoteUserInfo) { || playbackState == STATE_ENDED || playbackState == STATE_IDLE) { if (playbackState == STATE_IDLE) { - playerWrapper.prepare(); + playerWrapper.prepareIfCommandAvailable(); } else if (playbackState == STATE_ENDED) { - playerWrapper.seekTo( - playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); + playerWrapper.seekToDefaultPositionIfCommandAvailable(); } playerWrapper.play(); } else { @@ -308,10 +309,9 @@ public void onPlay() { PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); @Player.State int playbackState = playerWrapper.getPlaybackState(); if (playbackState == Player.STATE_IDLE) { - playerWrapper.prepare(); + playerWrapper.prepareIfCommandAvailable(); } else if (playbackState == Player.STATE_ENDED) { - playerWrapper.seekTo( - playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); + playerWrapper.seekToDefaultPositionIfCommandAvailable(); } if (sessionImpl.onPlayRequested()) { playerWrapper.play(); @@ -369,18 +369,32 @@ public void onSeekTo(long pos) { @Override public void onSkipToNext() { - dispatchSessionTaskWithPlayerCommand( - COMMAND_SEEK_TO_NEXT, - controller -> sessionImpl.getPlayerWrapper().seekToNext(), - sessionCompat.getCurrentControllerInfo()); + if (sessionImpl.getPlayerWrapper().isCommandAvailable(COMMAND_SEEK_TO_NEXT)) { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_NEXT, + controller -> sessionImpl.getPlayerWrapper().seekToNext(), + sessionCompat.getCurrentControllerInfo()); + } else { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, + controller -> sessionImpl.getPlayerWrapper().seekToNextMediaItem(), + sessionCompat.getCurrentControllerInfo()); + } } @Override public void onSkipToPrevious() { - dispatchSessionTaskWithPlayerCommand( - COMMAND_SEEK_TO_PREVIOUS, - controller -> sessionImpl.getPlayerWrapper().seekToPrevious(), - sessionCompat.getCurrentControllerInfo()); + if (sessionImpl.getPlayerWrapper().isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_PREVIOUS, + controller -> sessionImpl.getPlayerWrapper().seekToPrevious(), + sessionCompat.getCurrentControllerInfo()); + } else { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, + controller -> sessionImpl.getPlayerWrapper().seekToPreviousMediaItem(), + sessionCompat.getCurrentControllerInfo()); + } } @Override @@ -435,7 +449,9 @@ public void onSetRating(RatingCompat ratingCompat, @Nullable Bundle unusedExtras dispatchSessionTaskWithSessionCommand( SessionCommand.COMMAND_CODE_SESSION_SET_RATING, controller -> { - @Nullable MediaItem currentItem = sessionImpl.getPlayerWrapper().getCurrentMediaItem(); + @Nullable + MediaItem currentItem = + sessionImpl.getPlayerWrapper().getCurrentMediaItemWithCommandCheck(); if (currentItem == null) { return; } @@ -494,12 +510,17 @@ public void onRemoveQueueItem(@Nullable MediaDescriptionCompat description) { Log.w(TAG, "onRemoveQueueItem(): Media ID shouldn't be null"); return; } - Timeline timeline = sessionImpl.getPlayerWrapper().getCurrentTimeline(); + PlayerWrapper player = sessionImpl.getPlayerWrapper(); + if (!player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) { + Log.w(TAG, "Can't remove item by id without availabe COMMAND_GET_TIMELINE"); + return; + } + Timeline timeline = player.getCurrentTimeline(); Timeline.Window window = new Timeline.Window(); for (int i = 0; i < timeline.getWindowCount(); i++) { MediaItem mediaItem = timeline.getWindow(i, window).mediaItem; if (TextUtils.equals(mediaItem.mediaId, mediaId)) { - sessionImpl.getPlayerWrapper().removeMediaItem(i); + player.removeMediaItem(i); return; } } @@ -700,16 +721,16 @@ public void onSuccess(List mediaItems) { postOrRun( sessionImpl.getApplicationHandler(), () -> { - Player player = sessionImpl.getPlayerWrapper(); + PlayerWrapper player = sessionImpl.getPlayerWrapper(); player.setMediaItems(mediaItems); @Player.State int playbackState = player.getPlaybackState(); if (playbackState == Player.STATE_IDLE) { - player.prepare(); + player.prepareIfCommandAvailable(); } else if (playbackState == Player.STATE_ENDED) { - player.seekTo(/* positionMs= */ C.TIME_UNSET); + player.seekToDefaultPositionIfCommandAvailable(); } if (play) { - player.play(); + player.playIfCommandAvailable(); } }); } @@ -875,19 +896,21 @@ public void onPlayerChanged( throws RemoteException { // Tells the playlist change first, so current media item index change notification // can point to the valid current media item in the playlist. - Timeline newTimeline = newPlayerWrapper.getCurrentTimeline(); + Timeline newTimeline = newPlayerWrapper.getCurrentTimelineWithCommandCheck(); if (oldPlayerWrapper == null - || !Util.areEqual(oldPlayerWrapper.getCurrentTimeline(), newTimeline)) { + || !Util.areEqual(oldPlayerWrapper.getCurrentTimelineWithCommandCheck(), newTimeline)) { onTimelineChanged(seq, newTimeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); } - MediaMetadata newPlaylistMetadata = newPlayerWrapper.getPlaylistMetadata(); + MediaMetadata newPlaylistMetadata = newPlayerWrapper.getPlaylistMetadataWithCommandCheck(); if (oldPlayerWrapper == null - || !Util.areEqual(oldPlayerWrapper.getPlaylistMetadata(), newPlaylistMetadata)) { + || !Util.areEqual( + oldPlayerWrapper.getPlaylistMetadataWithCommandCheck(), newPlaylistMetadata)) { onPlaylistMetadataChanged(seq, newPlaylistMetadata); } - MediaMetadata newMediaMetadata = newPlayerWrapper.getMediaMetadata(); + MediaMetadata newMediaMetadata = newPlayerWrapper.getMediaMetadataWithCommandCheck(); if (oldPlayerWrapper == null - || !Util.areEqual(oldPlayerWrapper.getMediaMetadata(), newMediaMetadata)) { + || !Util.areEqual( + oldPlayerWrapper.getMediaMetadataWithCommandCheck(), newMediaMetadata)) { onMediaMetadataChanged(seq, newMediaMetadata); } if (oldPlayerWrapper == null @@ -904,9 +927,9 @@ public void onPlayerChanged( onDeviceInfoChanged(seq, newPlayerWrapper.getDeviceInfo()); // Rest of changes are all notified via PlaybackStateCompat. - @Nullable MediaItem newMediaItem = newPlayerWrapper.getCurrentMediaItem(); + @Nullable MediaItem newMediaItem = newPlayerWrapper.getCurrentMediaItemWithCommandCheck(); if (oldPlayerWrapper == null - || !Util.areEqual(oldPlayerWrapper.getCurrentMediaItem(), newMediaItem)) { + || !Util.areEqual(oldPlayerWrapper.getCurrentMediaItemWithCommandCheck(), newMediaItem)) { // Note: This will update both PlaybackStateCompat and metadata. onMediaItemTransition( seq, newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); @@ -1135,7 +1158,8 @@ public void onDeviceInfoChanged(int seq, DeviceInfo deviceInfo) { PlayerWrapper player = sessionImpl.getPlayerWrapper(); volumeProviderCompat = player.createVolumeProviderCompat(); if (volumeProviderCompat == null) { - int streamType = MediaUtils.getLegacyStreamType(player.getAudioAttributes()); + int streamType = + MediaUtils.getLegacyStreamType(player.getAudioAttributesWithCommandCheck()); sessionCompat.setPlaybackToLocal(streamType); } else { sessionCompat.setPlaybackToRemote(volumeProviderCompat); @@ -1158,10 +1182,10 @@ public void onPeriodicSessionPositionInfoChanged( } private void updateMetadataIfChanged() { - Player player = sessionImpl.getPlayerWrapper(); - @Nullable MediaItem currentMediaItem = player.getCurrentMediaItem(); - MediaMetadata newMediaMetadata = player.getMediaMetadata(); - long newDurationMs = player.getDuration(); + PlayerWrapper player = sessionImpl.getPlayerWrapper(); + @Nullable MediaItem currentMediaItem = player.getCurrentMediaItemWithCommandCheck(); + MediaMetadata newMediaMetadata = player.getMediaMetadataWithCommandCheck(); + long newDurationMs = player.getDurationWithCommandCheck(); String newMediaId = currentMediaItem != null ? currentMediaItem.mediaId : MediaItem.DEFAULT_MEDIA_ID; @Nullable diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index a9eab3f1a36..97a85e3ffbc 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -26,6 +26,7 @@ import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.view.Surface; import android.view.SurfaceHolder; @@ -34,6 +35,7 @@ import androidx.annotation.Nullable; import androidx.media.VolumeProviderCompat; import androidx.media3.common.AudioAttributes; +import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaItem; @@ -43,9 +45,11 @@ import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.Tracks; import androidx.media3.common.VideoSize; import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Log; +import androidx.media3.common.util.Size; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; import java.util.List; @@ -133,6 +137,12 @@ public void play() { super.play(); } + public void playIfCommandAvailable() { + if (isCommandAvailable(COMMAND_PLAY_PAUSE)) { + play(); + } + } + @Override public void pause() { verifyApplicationThread(); @@ -145,6 +155,12 @@ public void prepare() { super.prepare(); } + public void prepareIfCommandAvailable() { + if (isCommandAvailable(COMMAND_PREPARE)) { + prepare(); + } + } + @Override public void stop() { verifyApplicationThread(); @@ -163,6 +179,18 @@ public void seekToDefaultPosition(int mediaItemIndex) { super.seekToDefaultPosition(mediaItemIndex); } + @Override + public void seekToDefaultPosition() { + verifyApplicationThread(); + super.seekToDefaultPosition(); + } + + public void seekToDefaultPositionIfCommandAvailable() { + if (isCommandAvailable(Player.COMMAND_SEEK_TO_DEFAULT_POSITION)) { + seekToDefaultPosition(); + } + } + @Override public void seekTo(long positionMs) { verifyApplicationThread(); @@ -223,6 +251,10 @@ public long getDuration() { return super.getDuration(); } + public long getDurationWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM) ? getDuration() : C.TIME_UNSET; + } + @Override public long getBufferedPosition() { verifyApplicationThread(); @@ -355,6 +387,12 @@ public AudioAttributes getAudioAttributes() { return super.getAudioAttributes(); } + public AudioAttributes getAudioAttributesWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES) + ? getAudioAttributes() + : AudioAttributes.DEFAULT; + } + @Override public void setMediaItem(MediaItem mediaItem) { verifyApplicationThread(); @@ -549,12 +587,22 @@ public Timeline getCurrentTimeline() { return super.getCurrentTimeline(); } + public Timeline getCurrentTimelineWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_TIMELINE) ? getCurrentTimeline() : Timeline.EMPTY; + } + @Override public MediaMetadata getPlaylistMetadata() { verifyApplicationThread(); return super.getPlaylistMetadata(); } + public MediaMetadata getPlaylistMetadataWithCommandCheck() { + return isCommandAvailable(Player.COMMAND_GET_MEDIA_ITEMS_METADATA) + ? getPlaylistMetadata() + : MediaMetadata.EMPTY; + } + @Override public int getRepeatMode() { verifyApplicationThread(); @@ -574,6 +622,11 @@ public MediaItem getCurrentMediaItem() { return super.getCurrentMediaItem(); } + @Nullable + public MediaItem getCurrentMediaItemWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM) ? getCurrentMediaItem() : null; + } + @Override public int getMediaItemCount() { verifyApplicationThread(); @@ -631,6 +684,10 @@ public float getVolume() { return super.getVolume(); } + public float getVolumeWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_VOLUME) ? getVolume() : 0; + } + @Override public void setVolume(float volume) { verifyApplicationThread(); @@ -643,6 +700,10 @@ public CueGroup getCurrentCues() { return super.getCurrentCues(); } + public CueGroup getCurrentCuesWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_TEXT) ? getCurrentCues() : CueGroup.EMPTY_TIME_ZERO; + } + @Override public DeviceInfo getDeviceInfo() { verifyApplicationThread(); @@ -655,18 +716,32 @@ public int getDeviceVolume() { return super.getDeviceVolume(); } + public int getDeviceVolumeWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_DEVICE_VOLUME) ? getDeviceVolume() : 0; + } + @Override public boolean isDeviceMuted() { verifyApplicationThread(); return super.isDeviceMuted(); } + public boolean isDeviceMutedWithCommandCheck() { + return isCommandAvailable(Player.COMMAND_GET_DEVICE_VOLUME) && isDeviceMuted(); + } + @Override public void setDeviceVolume(int volume) { verifyApplicationThread(); super.setDeviceVolume(volume); } + public void setDeviceVolumeIfCommandAvailable(int volume) { + if (isCommandAvailable(COMMAND_SET_DEVICE_VOLUME)) { + setDeviceVolume(volume); + } + } + @Override public void increaseDeviceVolume() { verifyApplicationThread(); @@ -729,6 +804,12 @@ public MediaMetadata getMediaMetadata() { return super.getMediaMetadata(); } + public MediaMetadata getMediaMetadataWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA) + ? getMediaMetadata() + : MediaMetadata.EMPTY; + } + @Override public boolean isCommandAvailable(@Command int command) { verifyApplicationThread(); @@ -753,6 +834,71 @@ public void setTrackSelectionParameters(TrackSelectionParameters parameters) { super.setTrackSelectionParameters(parameters); } + @Override + public void seekToPrevious() { + verifyApplicationThread(); + super.seekToPrevious(); + } + + @Override + public long getMaxSeekToPreviousPosition() { + verifyApplicationThread(); + return super.getMaxSeekToPreviousPosition(); + } + + @Override + public void seekToNext() { + verifyApplicationThread(); + super.seekToNext(); + } + + @Override + public Tracks getCurrentTracks() { + verifyApplicationThread(); + return super.getCurrentTracks(); + } + + public Tracks getCurrentTracksWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_TRACKS) ? getCurrentTracks() : Tracks.EMPTY; + } + + @Nullable + @Override + public Object getCurrentManifest() { + verifyApplicationThread(); + return super.getCurrentManifest(); + } + + @Override + public int getCurrentPeriodIndex() { + verifyApplicationThread(); + return super.getCurrentPeriodIndex(); + } + + @Override + public boolean isCurrentMediaItemDynamic() { + verifyApplicationThread(); + return super.isCurrentMediaItemDynamic(); + } + + @Override + public boolean isCurrentMediaItemLive() { + verifyApplicationThread(); + return super.isCurrentMediaItemLive(); + } + + @Override + public boolean isCurrentMediaItemSeekable() { + verifyApplicationThread(); + return super.isCurrentMediaItemSeekable(); + } + + @Override + public Size getSurfaceSize() { + verifyApplicationThread(); + return super.getSurfaceSize(); + } + public PlaybackStateCompat createPlaybackStateCompat() { if (legacyStatusCode != STATUS_CODE_SUCCESS_COMPAT) { return new PlaybackStateCompat.Builder() @@ -799,22 +945,28 @@ public PlaybackStateCompat createPlaybackStateCompat() { || getAvailableCommands().contains(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)) { allActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT; } - long queueItemId = MediaUtils.convertToQueueItemId(getCurrentMediaItemIndex()); + long queueItemId = + isCommandAvailable(COMMAND_GET_TIMELINE) + ? MediaUtils.convertToQueueItemId(getCurrentMediaItemIndex()) + : MediaSessionCompat.QueueItem.UNKNOWN_ID; float playbackSpeed = getPlaybackParameters().speed; float sessionPlaybackSpeed = isPlaying() ? playbackSpeed : 0f; Bundle extras = new Bundle(); extras.putFloat(EXTRAS_KEY_PLAYBACK_SPEED_COMPAT, playbackSpeed); - @Nullable MediaItem currentMediaItem = getCurrentMediaItem(); + @Nullable MediaItem currentMediaItem = getCurrentMediaItemWithCommandCheck(); if (currentMediaItem != null && !MediaItem.DEFAULT_MEDIA_ID.equals(currentMediaItem.mediaId)) { extras.putString(EXTRAS_KEY_MEDIA_ID_COMPAT, currentMediaItem.mediaId); } + boolean canReadPositions = isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM); + long compatPosition = + canReadPositions ? getCurrentPosition() : PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN; + long compatBufferedPosition = canReadPositions ? getBufferedPosition() : 0; PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder() - .setState( - state, getCurrentPosition(), sessionPlaybackSpeed, SystemClock.elapsedRealtime()) + .setState(state, compatPosition, sessionPlaybackSpeed, SystemClock.elapsedRealtime()) .setActions(allActions) .setActiveQueueItemId(queueItemId) - .setBufferedPosition(getBufferedPosition()) + .setBufferedPosition(compatBufferedPosition) .setExtras(extras); for (int i = 0; i < customLayout.size(); i++) { @@ -853,11 +1005,11 @@ public VolumeProviderCompat createVolumeProviderCompat() { } } Handler handler = new Handler(getApplicationLooper()); - return new VolumeProviderCompat( - volumeControlType, getDeviceInfo().maxVolume, getDeviceVolume()) { + int currentVolume = getDeviceVolumeWithCommandCheck(); + return new VolumeProviderCompat(volumeControlType, getDeviceInfo().maxVolume, currentVolume) { @Override public void onSetVolumeTo(int volume) { - postOrRun(handler, () -> setDeviceVolume(volume)); + postOrRun(handler, () -> setDeviceVolumeIfCommandAvailable(volume)); } @Override @@ -865,6 +1017,9 @@ public void onAdjustVolume(int direction) { postOrRun( handler, () -> { + if (!isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)) { + return; + } switch (direction) { case AudioManager.ADJUST_RAISE: increaseDeviceVolume(); @@ -879,7 +1034,7 @@ public void onAdjustVolume(int direction) { setDeviceMuted(false); break; case AudioManager.ADJUST_TOGGLE_MUTE: - setDeviceMuted(!isDeviceMuted()); + setDeviceMuted(!isDeviceMutedWithCommandCheck()); break; default: Log.w( @@ -898,6 +1053,9 @@ public void onAdjustVolume(int direction) { *

        This excludes window uid and period uid that wouldn't be preserved when bundling. */ public PositionInfo createPositionInfoForBundling() { + if (!isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) { + return SessionPositionInfo.DEFAULT_POSITION_INFO; + } return new PositionInfo( /* windowUid= */ null, getCurrentMediaItemIndex(), @@ -916,6 +1074,9 @@ public PositionInfo createPositionInfoForBundling() { *

        This excludes window uid and period uid that wouldn't be preserved when bundling. */ public SessionPositionInfo createSessionPositionInfoForBundling() { + if (!isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) { + return SessionPositionInfo.DEFAULT; + } return new SessionPositionInfo( createPositionInfoForBundling(), isPlayingAd(), @@ -941,25 +1102,25 @@ public PlayerInfo createPlayerInfoForBundling() { getRepeatMode(), getShuffleModeEnabled(), getVideoSize(), - getCurrentTimeline(), - getPlaylistMetadata(), - getVolume(), - getAudioAttributes(), - getCurrentCues(), + getCurrentTimelineWithCommandCheck(), + getPlaylistMetadataWithCommandCheck(), + getVolumeWithCommandCheck(), + getAudioAttributesWithCommandCheck(), + getCurrentCuesWithCommandCheck(), getDeviceInfo(), - getDeviceVolume(), - isDeviceMuted(), + getDeviceVolumeWithCommandCheck(), + isDeviceMutedWithCommandCheck(), getPlayWhenReady(), PlayerInfo.PLAY_WHEN_READY_CHANGE_REASON_DEFAULT, getPlaybackSuppressionReason(), getPlaybackState(), isPlaying(), isLoading(), - getMediaMetadata(), + getMediaMetadataWithCommandCheck(), getSeekBackIncrement(), getSeekForwardIncrement(), getMaxSeekToPreviousPosition(), - getCurrentTracks(), + getCurrentTracksWithCommandCheck(), getTrackSelectionParameters()); } diff --git a/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java b/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java index 85e26f4cc38..430e14c61cc 100644 --- a/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java @@ -16,6 +16,7 @@ package androidx.media3.session; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.when; import android.os.Looper; @@ -42,6 +43,7 @@ public class PlayerWrapperTest { @Before public void setUp() { playerWrapper = new PlayerWrapper(player); + when(player.isCommandAvailable(anyInt())).thenReturn(true); when(player.getApplicationLooper()).thenReturn(Looper.myLooper()); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java index 6fd12bc37ef..504c48ed639 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java @@ -18,7 +18,9 @@ import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE; import static androidx.media3.common.Player.COMMAND_PREPARE; +import static androidx.media3.common.Player.STATE_ENDED; import static androidx.media3.common.Player.STATE_IDLE; +import static androidx.media3.common.Player.STATE_READY; import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; @@ -204,7 +206,8 @@ public void onDisconnected(MediaSession session, ControllerInfo controller) { } @Test - public void play() throws Exception { + public void play_whileReady_callsPlay() throws Exception { + player.playbackState = STATE_READY; session = new MediaSession.Builder(context, player) .setId("play") @@ -217,6 +220,92 @@ public void play() throws Exception { controller.getTransportControls().play(); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); + } + + @Test + public void play_whileIdle_callsPrepareAndPlay() throws Exception { + player.playbackState = STATE_IDLE; + session = + new MediaSession.Builder(context, player) + .setId("play") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().play(); + + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); + } + + @Test + public void play_whileIdleWithoutPrepareCommandAvailable_callsJustPlay() throws Exception { + player.playbackState = STATE_IDLE; + player.commands = + new Player.Commands.Builder().addAllCommands().remove(Player.COMMAND_PREPARE).build(); + session = + new MediaSession.Builder(context, player) + .setId("play") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().play(); + + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); + } + + @Test + public void play_whileEnded_callsSeekToDefaultPositionAndPlay() throws Exception { + player.playbackState = STATE_ENDED; + session = + new MediaSession.Builder(context, player) + .setId("play") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().play(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + } + + @Test + public void play_whileEndedWithoutSeekToDefaultPositionCommandAvailable_callsJustPlay() + throws Exception { + player.playbackState = STATE_ENDED; + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_DEFAULT_POSITION) + .build(); + session = + new MediaSession.Builder(context, player) + .setId("play") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().play(); + + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); } @Test @@ -428,7 +517,7 @@ public void removeQueueItem() throws Exception { } @Test - public void skipToPrevious() throws Exception { + public void skipToPrevious_withAllCommandsAvailable_callsSeekToPrevious() throws Exception { session = new MediaSession.Builder(context, player) .setId("skipToPrevious") @@ -444,7 +533,29 @@ public void skipToPrevious() throws Exception { } @Test - public void skipToNext() throws Exception { + public void skipToPrevious_withoutSeekToPreviousCommandAvailable_callsSeekToPreviousMediaItem() + throws Exception { + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_PREVIOUS) + .build(); + session = + new MediaSession.Builder(context, player) + .setId("skipToPrevious") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().skipToPrevious(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS_MEDIA_ITEM, TIMEOUT_MS); + } + + @Test + public void skipToNext_withAllCommandsAvailable_callsSeekToNext() throws Exception { session = new MediaSession.Builder(context, player) .setId("skipToNext") @@ -459,6 +570,25 @@ public void skipToNext() throws Exception { player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS); } + @Test + public void skipToNext_withoutSeekToNextCommandAvailable_callsSeekToNextMediaItem() + throws Exception { + player.commands = + new Player.Commands.Builder().addAllCommands().remove(Player.COMMAND_SEEK_TO_NEXT).build(); + session = + new MediaSession.Builder(context, player) + .setId("skipToNext") + .setCallback(new TestSessionCallback()) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().skipToNext(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT_MEDIA_ITEM, TIMEOUT_MS); + } + @Test public void skipToQueueItem() throws Exception { session = @@ -1049,6 +1179,101 @@ public ListenableFuture> onAddMediaItems( assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } + @Test + public void prepareFromMediaUri_withoutAvailablePrepareCommand_justCallsSetMediaItems() + throws Exception { + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + return Futures.immediateFuture(ImmutableList.of(resolvedMediaItem)); + } + }; + player.commands = + new Player.Commands.Builder().addAllCommands().remove(COMMAND_PREPARE).build(); + session = + new MediaSession.Builder(context, player) + .setId("prepareFromMediaUri") + .setCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().prepareFromUri(Uri.parse("foo://bar"), Bundle.EMPTY); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); + } + + @Test + public void playFromMediaUri_withoutAvailablePrepareCommand_justCallsSetMediaItemsAndPlay() + throws Exception { + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + return Futures.immediateFuture(ImmutableList.of(resolvedMediaItem)); + } + }; + player.commands = + new Player.Commands.Builder().addAllCommands().remove(COMMAND_PREPARE).build(); + session = + new MediaSession.Builder(context, player) + .setId("prepareFromMediaUri") + .setCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().playFromUri(Uri.parse("foo://bar"), Bundle.EMPTY); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); + } + + @Test + public void playFromMediaUri_withoutAvailablePrepareAndPlayCommand_justCallsSetMediaItems() + throws Exception { + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + return Futures.immediateFuture(ImmutableList.of(resolvedMediaItem)); + } + }; + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .removeAll(COMMAND_PREPARE, COMMAND_PLAY_PAUSE) + .build(); + session = + new MediaSession.Builder(context, player) + .setId("prepareFromMediaUri") + .setCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().playFromUri(Uri.parse("foo://bar"), Bundle.EMPTY); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse(); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); + } + @Test public void setRating() throws Exception { int ratingType = RatingCompat.RATING_5_STARS; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java index a6a1f2327f9..245f9449728 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java @@ -220,7 +220,7 @@ public void playPauseKeyEvent_playWhenReadyAndEnded_seekAndPlay() throws Excepti dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java index 8d82012e32a..d19743b6eea 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java @@ -38,12 +38,19 @@ import android.content.Context; import android.os.Bundle; import android.text.TextUtils; +import androidx.annotation.Nullable; +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.DeviceInfo; +import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.Player; import androidx.media3.common.Rating; import androidx.media3.common.StarRating; +import androidx.media3.common.Timeline; import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.Tracks; +import androidx.media3.common.text.CueGroup; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.MainLooperTestRule; @@ -262,6 +269,168 @@ public void setTrackSelectionParameters() throws Exception { TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT)); } + @Test + public void setPlayer_withoutAvailableCommands_doesNotCallProtectedPlayerGetters() + throws Exception { + MockPlayer mockPlayer = + new MockPlayer.Builder() + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .build(); + // Set remote device info to ensure we also cover the volume provider compat setup. + mockPlayer.deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 100); + Player player = + new ForwardingPlayer(mockPlayer) { + @Override + public boolean isCommandAvailable(int command) { + return false; + } + + @Override + public Tracks getCurrentTracks() { + throw new UnsupportedOperationException(); + } + + @Override + public MediaMetadata getMediaMetadata() { + throw new UnsupportedOperationException(); + } + + @Override + public MediaMetadata getPlaylistMetadata() { + throw new UnsupportedOperationException(); + } + + @Override + public Timeline getCurrentTimeline() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentPeriodIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentMediaItemIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getNextMediaItemIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getPreviousMediaItemIndex() { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public MediaItem getCurrentMediaItem() { + throw new UnsupportedOperationException(); + } + + @Override + public long getDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public long getCurrentPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public long getBufferedPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public long getTotalBufferedDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentMediaItemDynamic() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentMediaItemLive() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPlayingAd() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdGroupIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdIndexInAdGroup() { + throw new UnsupportedOperationException(); + } + + @Override + public long getContentDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public long getContentPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public long getContentBufferedPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public AudioAttributes getAudioAttributes() { + throw new UnsupportedOperationException(); + } + + @Override + public CueGroup getCurrentCues() { + throw new UnsupportedOperationException(); + } + + @Override + public int getDeviceVolume() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDeviceMuted() { + throw new UnsupportedOperationException(); + } + }; + MediaSession session = new MediaSession.Builder(context, player).setId(SESSION_ID).build(); + + MediaController controller = + new MediaController.Builder(context, session.getToken()) + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .buildAsync() + .get(); + + // Test passes if none of the protected player getters have been called. + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.release(); + session.release(); + player.release(); + }); + } + private ControllerInfo getTestControllerInfo() { List controllers = session.getConnectedControllers(); assertThat(controllers).isNotNull(); From 903915de3d33d7547a7dd3e3eeae8d5804d58943 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 16 Jan 2023 12:50:20 +0000 Subject: [PATCH 107/141] Fix command check in MediaControllerImplBase The command check for setDeviceMuted was wrong. PiperOrigin-RevId: 502355332 (cherry picked from commit cfcce9aec9d92a7067f07b2d9c00d705df0368ac) --- .../java/androidx/media3/session/MediaControllerImplBase.java | 2 +- .../main/java/androidx/media3/session/MediaSessionStub.java | 2 +- .../androidx/media3/session/MediaSessionPermissionTest.java | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index c16a8a61d8e..8cca5c4d874 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -1468,7 +1468,7 @@ public void decreaseDeviceVolume() { @Override public void setDeviceMuted(boolean muted) { - if (!isPlayerCommandAvailable(Player.COMMAND_SET_DEVICE_VOLUME)) { + if (!isPlayerCommandAvailable(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { return; } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 866e92d80ea..aa51cb519c2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -1324,7 +1324,7 @@ public void setDeviceMuted(@Nullable IMediaController caller, int sequenceNumber queueSessionTaskWithPlayerCommand( caller, sequenceNumber, - COMMAND_SET_DEVICE_VOLUME, + COMMAND_ADJUST_DEVICE_VOLUME, sendSessionResultSuccess(player -> player.setDeviceMuted(muted))); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java index d19743b6eea..38492052ef8 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java @@ -185,7 +185,8 @@ public void decreaseDeviceVolume() throws Exception { @Test public void setDeviceMuted() throws Exception { - testOnCommandRequest(COMMAND_SET_DEVICE_VOLUME, controller -> controller.setDeviceMuted(true)); + testOnCommandRequest( + COMMAND_ADJUST_DEVICE_VOLUME, controller -> controller.setDeviceMuted(true)); } @Test From 818ce7271ebf2b9bd823052469c1700f21a61a83 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 16 Jan 2023 16:38:12 +0000 Subject: [PATCH 108/141] Clarify what default settings are being used for SSAI AdsLoader PiperOrigin-RevId: 502388865 (cherry picked from commit abe11c88ecdfe56ca31d3bffe1dd8fce6fb293af) --- .../exoplayer/ima/ImaServerSideAdInsertionMediaSource.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index 959d873cf84..e029b74578f 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -213,7 +213,9 @@ public Builder(Context context, AdViewProvider adViewProvider) { /** * Sets the IMA SDK settings. * - *

        If this method is not called the default settings will be used. + *

        If this method is not called, the {@linkplain ImaSdkFactory#createImaSdkSettings() + * default settings} will be used with the language set to {@linkplain + * Util#getSystemLanguageCodes() the preferred system language}. * * @param imaSdkSettings The {@link ImaSdkSettings}. * @return This builder, for convenience. From 79fd80f8b08627ae06cd710b5d0466d860533bd4 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Mon, 16 Jan 2023 18:54:04 +0000 Subject: [PATCH 109/141] Post notification for session app when FgS starting exception is caught PiperOrigin-RevId: 502407886 (cherry picked from commit 6ce3421ca750109acfea35029260dc3f169a1a40) --- .../media3/demo/session/PlaybackService.kt | 70 ++++++++++++++++--- demos/session/src/main/res/values/strings.xml | 5 ++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt index 16ca1a25a5d..b5ba86ab224 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt @@ -15,22 +15,22 @@ */ package androidx.media3.demo.session -import android.app.PendingIntent.FLAG_IMMUTABLE -import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.app.Notification.BigTextStyle +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent.* import android.app.TaskStackBuilder import android.content.Intent import android.os.Build import android.os.Bundle +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import androidx.media3.common.AudioAttributes import androidx.media3.common.MediaItem +import androidx.media3.common.util.Util import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.session.CommandButton -import androidx.media3.session.LibraryResult -import androidx.media3.session.MediaLibraryService -import androidx.media3.session.MediaSession +import androidx.media3.session.* import androidx.media3.session.MediaSession.ControllerInfo -import androidx.media3.session.SessionCommand -import androidx.media3.session.SessionResult import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @@ -51,6 +51,8 @@ class PlaybackService : MediaLibraryService() { "android.media3.session.demo.SHUFFLE_ON" private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = "android.media3.session.demo.SHUFFLE_OFF" + private const val NOTIFICATION_ID = 123 + private const val CHANNEL_ID = "demo_session_notification_channel_id" } override fun onCreate() { @@ -66,6 +68,7 @@ class PlaybackService : MediaLibraryService() { ) customLayout = ImmutableList.of(customCommands[0]) initializeSessionAndPlayer() + setListener(MediaSessionServiceListener()) } override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession { @@ -81,6 +84,7 @@ class PlaybackService : MediaLibraryService() { override fun onDestroy() { player.release() mediaLibrarySession.release() + clearListener() super.onDestroy() } @@ -259,4 +263,54 @@ class PlaybackService : MediaLibraryService() { private fun ignoreFuture(customLayout: ListenableFuture) { /* Do nothing. */ } + + private inner class MediaSessionServiceListener : Listener { + + /** + * This method is only required to be implemented on Android 12 or above when an attempt is made + * by a media controller to resume playback when the {@link MediaSessionService} is in the + * background. + */ + override fun onForegroundServiceStartNotAllowedException() { + createNotificationAndNotify() + } + } + + private fun createNotificationAndNotify() { + var notificationManagerCompat = NotificationManagerCompat.from(this) + ensureNotificationChannel(notificationManagerCompat) + var pendingIntent = + TaskStackBuilder.create(this).run { + addNextIntent(Intent(this@PlaybackService, MainActivity::class.java)) + + val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0 + getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT) + } + + var builder = + NotificationCompat.Builder(this, CHANNEL_ID) + .setContentIntent(pendingIntent) + .setSmallIcon(R.drawable.media3_notification_small_icon) + .setContentTitle(getString(R.string.notification_content_title)) + .setStyle( + NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_content_text)) + ) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + notificationManagerCompat.notify(NOTIFICATION_ID, builder.build()) + } + + private fun ensureNotificationChannel(notificationManagerCompat: NotificationManagerCompat) { + if (Util.SDK_INT < 26 || notificationManagerCompat.getNotificationChannel(CHANNEL_ID) != null) { + return + } + + val channel = + NotificationChannel( + CHANNEL_ID, + getString(R.string.notification_channel_name), + NotificationManager.IMPORTANCE_DEFAULT + ) + notificationManagerCompat.createNotificationChannel(channel) + } } diff --git a/demos/session/src/main/res/values/strings.xml b/demos/session/src/main/res/values/strings.xml index 727772e1906..0add882c72c 100644 --- a/demos/session/src/main/res/values/strings.xml +++ b/demos/session/src/main/res/values/strings.xml @@ -24,4 +24,9 @@ "! No media in the play list !\nPlease try to add more from browser" + Playback cannot be resumed + Press on the play button on the media notification if it + is still present, otherwise please open the app to start the playback and re-connect the session + to the controller + Playback cannot be resumed From dd462e8cdb866d8536173edaa9bb9223ea6176f5 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 17 Jan 2023 13:38:48 +0000 Subject: [PATCH 110/141] Filter what PlaybackStateCompat actions are advertised PlayerWrapper advertises PlaybackStateCompat actions to the legacy MediaSession based on the player's available commands. PiperOrigin-RevId: 502559162 (cherry picked from commit 39f4a17ad4ac3863af22e12711247c7a87b8613e) --- .../media3/session/PlayerWrapper.java | 97 +- ...tateCompatActionsWithMediaSessionTest.java | 1374 +++++++++++++++++ 2 files changed, 1443 insertions(+), 28 deletions(-) create mode 100644 libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index 97a85e3ffbc..bf5f2756c91 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -917,33 +917,11 @@ public PlaybackStateCompat createPlaybackStateCompat() { int state = MediaUtils.convertToPlaybackStateCompatState( playerError, getPlaybackState(), getPlayWhenReady()); - long allActions = - PlaybackStateCompat.ACTION_STOP - | PlaybackStateCompat.ACTION_PAUSE - | PlaybackStateCompat.ACTION_PLAY - | PlaybackStateCompat.ACTION_REWIND - | PlaybackStateCompat.ACTION_FAST_FORWARD - | PlaybackStateCompat.ACTION_SET_RATING - | PlaybackStateCompat.ACTION_SEEK_TO - | PlaybackStateCompat.ACTION_PLAY_PAUSE - | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID - | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH - | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM - | PlaybackStateCompat.ACTION_PLAY_FROM_URI - | PlaybackStateCompat.ACTION_PREPARE - | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID - | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH - | PlaybackStateCompat.ACTION_PREPARE_FROM_URI - | PlaybackStateCompat.ACTION_SET_REPEAT_MODE - | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE - | PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED; - if (getAvailableCommands().contains(COMMAND_SEEK_TO_PREVIOUS) - || getAvailableCommands().contains(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)) { - allActions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; - } - if (getAvailableCommands().contains(COMMAND_SEEK_TO_NEXT) - || getAvailableCommands().contains(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)) { - allActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT; + // Always advertise ACTION_SET_RATING. + long actions = PlaybackStateCompat.ACTION_SET_RATING; + Commands availableCommands = getAvailableCommands(); + for (int i = 0; i < availableCommands.size(); i++) { + actions |= convertCommandToPlaybackStateActions(availableCommands.get(i)); } long queueItemId = isCommandAvailable(COMMAND_GET_TIMELINE) @@ -964,7 +942,7 @@ public PlaybackStateCompat createPlaybackStateCompat() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder() .setState(state, compatPosition, sessionPlaybackSpeed, SystemClock.elapsedRealtime()) - .setActions(allActions) + .setActions(actions) .setActiveQueueItemId(queueItemId) .setBufferedPosition(compatBufferedPosition) .setExtras(extras); @@ -1127,4 +1105,67 @@ public PlayerInfo createPlayerInfoForBundling() { private void verifyApplicationThread() { checkState(Looper.myLooper() == getApplicationLooper()); } + + @SuppressWarnings("deprecation") // Uses deprecated PlaybackStateCompat actions. + private static long convertCommandToPlaybackStateActions(@Command int command) { + switch (command) { + case Player.COMMAND_PLAY_PAUSE: + return PlaybackStateCompat.ACTION_PAUSE + | PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PLAY_PAUSE; + case Player.COMMAND_PREPARE: + return PlaybackStateCompat.ACTION_PREPARE; + case Player.COMMAND_SEEK_BACK: + return PlaybackStateCompat.ACTION_REWIND; + case Player.COMMAND_SEEK_FORWARD: + return PlaybackStateCompat.ACTION_FAST_FORWARD; + case Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM: + return PlaybackStateCompat.ACTION_SEEK_TO; + case Player.COMMAND_SEEK_TO_MEDIA_ITEM: + return PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; + case Player.COMMAND_SEEK_TO_NEXT: + case Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM: + return PlaybackStateCompat.ACTION_SKIP_TO_NEXT; + case Player.COMMAND_SEEK_TO_PREVIOUS: + case Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM: + return PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; + case Player.COMMAND_SET_MEDIA_ITEM: + return PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH + | PlaybackStateCompat.ACTION_PLAY_FROM_URI + | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH + | PlaybackStateCompat.ACTION_PREPARE_FROM_URI; + case Player.COMMAND_SET_REPEAT_MODE: + return PlaybackStateCompat.ACTION_SET_REPEAT_MODE; + case Player.COMMAND_SET_SPEED_AND_PITCH: + return PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED; + case Player.COMMAND_SET_SHUFFLE_MODE: + return PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE + | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED; + case Player.COMMAND_STOP: + return PlaybackStateCompat.ACTION_STOP; + case Player.COMMAND_ADJUST_DEVICE_VOLUME: + case Player.COMMAND_CHANGE_MEDIA_ITEMS: + // TODO(b/227346735): Handle this through + // MediaSessionCompat.setFlags(FLAG_HANDLES_QUEUE_COMMANDS) + case Player.COMMAND_GET_AUDIO_ATTRIBUTES: + case Player.COMMAND_GET_CURRENT_MEDIA_ITEM: + case Player.COMMAND_GET_DEVICE_VOLUME: + case Player.COMMAND_GET_MEDIA_ITEMS_METADATA: + case Player.COMMAND_GET_TEXT: + case Player.COMMAND_GET_TIMELINE: + case Player.COMMAND_GET_TRACKS: + case Player.COMMAND_GET_VOLUME: + case Player.COMMAND_INVALID: + case Player.COMMAND_SEEK_TO_DEFAULT_POSITION: + case Player.COMMAND_SET_DEVICE_VOLUME: + case Player.COMMAND_SET_MEDIA_ITEMS_METADATA: + case Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS: + case Player.COMMAND_SET_VIDEO_SURFACE: + case Player.COMMAND_SET_VOLUME: + default: + return 0; + } + } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java new file mode 100644 index 00000000000..7f50a474550 --- /dev/null +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java @@ -0,0 +1,1374 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.session; + +import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.ForwardingPlayer; +import androidx.media3.common.MediaItem; +import androidx.media3.common.PlaybackParameters; +import androidx.media3.common.Player; +import androidx.media3.common.Timeline; +import androidx.media3.common.util.ConditionVariable; +import androidx.media3.common.util.Consumer; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.test.session.common.HandlerThreadTestRule; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests that {@link MediaControllerCompat} receives the expected {@link + * PlaybackStateCompat.Actions} when connected to a {@link MediaSession}. + */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest { + + private static final String TAG = "MCCPSActionWithMS3"; + + @Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); + + @Test + public void playerWithCommandPlayPause_actionsPlayAndPauseAndPlayPauseAdvertised() + throws Exception { + Player player = + createPlayerWithAvailableCommand(createDefaultPlayer(), Player.COMMAND_PLAY_PAUSE); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + long actions = + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions(); + + assertThat(actions & PlaybackStateCompat.ACTION_PLAY_PAUSE).isNotEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PLAY).isNotEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PAUSE).isNotEqualTo(0); + + CountDownLatch latch = new CountDownLatch(2); + List receivedPlayWhenReady = new ArrayList<>(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + receivedPlayWhenReady.add(playWhenReady); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().play(); + controllerCompat.getTransportControls().pause(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedPlayWhenReady).containsExactly(true, false).inOrder(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithoutCommandPlayPause_actionsPlayAndPauseAndPlayPauseNotAdvertised() + throws Exception { + Player player = + createPlayerWithExcludedCommand(createDefaultPlayer(), Player.COMMAND_PLAY_PAUSE); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + long actions = + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions(); + + assertThat(actions & PlaybackStateCompat.ACTION_PLAY_PAUSE).isEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PLAY).isEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PAUSE).isEqualTo(0); + + AtomicInteger playWhenReadyCalled = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + playWhenReadyCalled.incrementAndGet(); + } + + @Override + public void onPlaybackStateChanged(@Player.State int playbackState) { + if (playbackState == Player.STATE_ENDED) { + latch.countDown(); + } + } + }; + player.addListener(listener); + + // play() & pause() should be a no-op + controllerCompat.getTransportControls().play(); + controllerCompat.getTransportControls().pause(); + // prepare() should transition the player to STATE_ENDED + controllerCompat.getTransportControls().prepare(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playWhenReadyCalled.get()).isEqualTo(0); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithCommandPrepare_actionPrepareAdvertised() throws Exception { + Player player = createPlayerWithAvailableCommand(createDefaultPlayer(), Player.COMMAND_PREPARE); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_PREPARE) + .isNotEqualTo(0); + + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPlaybackStateChanged(@Player.State int playbackState) { + if (playbackState == Player.STATE_ENDED) { + latch.countDown(); + } + } + }; + player.addListener(listener); + + // prepare() should transition the player to STATE_ENDED. + controllerCompat.getTransportControls().prepare(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithoutCommandPrepare_actionPrepareNotAdvertised() throws Exception { + Player player = createPlayerWithExcludedCommand(createDefaultPlayer(), Player.COMMAND_PREPARE); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_PREPARE) + .isEqualTo(0); + + AtomicInteger playbackStateChanges = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPlaybackStateChanged(@Player.State int playbackState) { + playbackStateChanges.incrementAndGet(); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + latch.countDown(); + } + }; + player.addListener(listener); + + // prepare() should be no-op + controllerCompat.getTransportControls().prepare(); + controllerCompat.getTransportControls().setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStateChanges.get()).isEqualTo(0); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithCommandSeekBack_actionRewindAdvertised() throws Exception { + Player player = + createPlayerWithAvailableCommand( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItem( + MediaItem.fromUri("asset://media/wav/sample.wav"), + /* startPositionMs= */ 500); + createdPlayer.prepare(); + }), + Player.COMMAND_SEEK_BACK); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_REWIND) + .isNotEqualTo(0); + + AtomicInteger discontinuityReason = new AtomicInteger(-1); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPositionDiscontinuity( + Player.PositionInfo oldPosition, + Player.PositionInfo newPosition, + @Player.DiscontinuityReason int reason) { + discontinuityReason.set(reason); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().rewind(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(discontinuityReason.get()).isEqualTo(Player.DISCONTINUITY_REASON_SEEK); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithoutCommandSeekBack_actionRewindNotAdvertised() throws Exception { + Player player = + createPlayerWithExcludedCommand( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItem( + MediaItem.fromUri("asset://media/wav/sample.wav"), + /* startPositionMs= */ 500); + createdPlayer.prepare(); + }), + Player.COMMAND_SEEK_BACK); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_REWIND) + .isEqualTo(0); + + AtomicBoolean receivedOnPositionDiscontinuity = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPositionDiscontinuity( + Player.PositionInfo oldPosition, + Player.PositionInfo newPosition, + @Player.DiscontinuityReason int reason) { + receivedOnPositionDiscontinuity.set(true); + } + + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + latch.countDown(); + } + }; + player.addListener(listener); + + // rewind() should be no-op. + controllerCompat.getTransportControls().rewind(); + controllerCompat.getTransportControls().play(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedOnPositionDiscontinuity.get()).isFalse(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithCommandSeekForward_actionFastForwardAdvertised() throws Exception { + Player player = + createPlayerWithAvailableCommand( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItem(MediaItem.fromUri("asset://media/wav/sample.wav")); + createdPlayer.prepare(); + }), + Player.COMMAND_SEEK_FORWARD); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_FAST_FORWARD) + .isNotEqualTo(0); + + AtomicInteger discontinuityReason = new AtomicInteger(-1); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPositionDiscontinuity( + Player.PositionInfo oldPosition, + Player.PositionInfo newPosition, + @Player.DiscontinuityReason int reason) { + discontinuityReason.set(reason); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().fastForward(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(discontinuityReason.get()).isEqualTo(Player.DISCONTINUITY_REASON_SEEK); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithoutCommandSeekForward_actionFastForwardNotAdvertised() throws Exception { + Player player = + createPlayerWithExcludedCommand( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItem(MediaItem.fromUri("asset://media/wav/sample.wav")); + createdPlayer.prepare(); + }), + Player.COMMAND_SEEK_FORWARD); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_FAST_FORWARD) + .isEqualTo(0); + + AtomicBoolean receivedOnPositionDiscontinuity = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPositionDiscontinuity( + Player.PositionInfo oldPosition, + Player.PositionInfo newPosition, + @Player.DiscontinuityReason int reason) { + receivedOnPositionDiscontinuity.set(true); + } + + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + latch.countDown(); + } + }; + player.addListener(listener); + + // fastForward() should be no-op + controllerCompat.getTransportControls().fastForward(); + controllerCompat.getTransportControls().play(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedOnPositionDiscontinuity.get()).isFalse(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithCommandSeekInCurrentMediaItem_actionSeekToAdvertised() throws Exception { + Player player = + createPlayerWithAvailableCommand( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItem(MediaItem.fromUri("asset://media/wav/sample.wav")); + createdPlayer.prepare(); + }), + Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SEEK_TO) + .isNotEqualTo(0); + + AtomicInteger discontinuityReason = new AtomicInteger(-1); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPositionDiscontinuity( + Player.PositionInfo oldPosition, + Player.PositionInfo newPosition, + @Player.DiscontinuityReason int reason) { + discontinuityReason.set(reason); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().seekTo(100); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(discontinuityReason.get()).isEqualTo(Player.DISCONTINUITY_REASON_SEEK); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithoutCommandSeekInCurrentMediaItem_actionSeekToNotAdvertised() + throws Exception { + Player player = + createPlayerWithExcludedCommand( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItem(MediaItem.fromUri("asset://media/wav/sample.wav")); + createdPlayer.prepare(); + }), + Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SEEK_TO) + .isEqualTo(0); + + AtomicBoolean receiovedOnPositionDiscontinuity = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPositionDiscontinuity( + Player.PositionInfo oldPosition, + Player.PositionInfo newPosition, + @Player.DiscontinuityReason int reason) { + receiovedOnPositionDiscontinuity.set(true); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + latch.countDown(); + } + }; + player.addListener(listener); + + // seekTo() should be no-op. + controllerCompat.getTransportControls().seekTo(100); + controllerCompat.getTransportControls().setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receiovedOnPositionDiscontinuity.get()).isFalse(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithCommandSeekToMediaItem_actionSkipToQueueItemAdvertised() throws Exception { + Player player = + createPlayerWithAvailableCommand( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("asset://media/wav/sample.wav"), + MediaItem.fromUri("asset://media/wav/sample_rf64.wav"))); + createdPlayer.prepare(); + }), + Player.COMMAND_SEEK_TO_MEDIA_ITEM); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM) + .isNotEqualTo(0); + + AtomicInteger mediaItemTransitionReason = new AtomicInteger(-1); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { + mediaItemTransitionReason.set(reason); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().skipToNext(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(mediaItemTransitionReason.get()).isEqualTo(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithoutCommandSeekToMediaItem_actionSkipToQueueItemNotAdvertised() + throws Exception { + Player player = + createPlayerWithExcludedCommand( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("asset://media/wav/sample.wav"), + MediaItem.fromUri("asset://media/wav/sample_rf64.wav"))); + createdPlayer.prepare(); + }), + Player.COMMAND_SEEK_TO_MEDIA_ITEM); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM) + .isEqualTo(0); + + AtomicBoolean receivedOnMediaItemTransition = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { + receivedOnMediaItemTransition.set(true); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + latch.countDown(); + } + }; + player.addListener(listener); + + // skipToQueueItem() should be no-op. + controllerCompat.getTransportControls().skipToQueueItem(1); + controllerCompat.getTransportControls().setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedOnMediaItemTransition.get()).isFalse(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void + playerWithCommandSeekToNext_withoutCommandSeeKToNextMediaItem_actionSkipToNextAdvertised() + throws Exception { + Player player = + createPlayerWithCommands( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("asset://media/wav/sample.wav"), + MediaItem.fromUri("asset://media/wav/sample_rf64.wav"))); + createdPlayer.prepare(); + }), + /* availableCommands= */ new Player.Commands.Builder() + .add(Player.COMMAND_SEEK_TO_NEXT) + .build(), + /* excludedCommand= */ new Player.Commands.Builder() + .add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) + .build()); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) + .isNotEqualTo(0); + + AtomicInteger mediaItemTransitionReason = new AtomicInteger(-1); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { + mediaItemTransitionReason.set(reason); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().skipToNext(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(mediaItemTransitionReason.get()).isEqualTo(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void + playerWithCommandSeekToNextMediaItem_withoutCommandSeekToNext_actionSkipToNextAdvertised() + throws Exception { + Player player = + createPlayerWithCommands( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("asset://media/wav/sample.wav"), + MediaItem.fromUri("asset://media/wav/sample_rf64.wav"))); + createdPlayer.prepare(); + }), + /* availableCommands= */ new Player.Commands.Builder() + .add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) + .build(), + /* excludedCommands= */ new Player.Commands.Builder() + .add(Player.COMMAND_SEEK_TO_NEXT) + .build()); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) + .isNotEqualTo(0); + + AtomicInteger mediaItemTransitionReason = new AtomicInteger(-1); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { + mediaItemTransitionReason.set(reason); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().skipToNext(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(mediaItemTransitionReason.get()).isEqualTo(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void + playerWithoutCommandSeekToNextAndCommandSeekToNextMediaItem_actionSkipToNextNotAdvertised() + throws Exception { + Player player = + createPlayerWithCommands( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("asset://media/wav/sample.wav"), + MediaItem.fromUri("asset://media/wav/sample_rf64.wav"))); + createdPlayer.prepare(); + }), + /* availableCommands= */ Player.Commands.EMPTY, + /* excludedCommands= */ new Player.Commands.Builder() + .addAll(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, Player.COMMAND_SEEK_TO_NEXT) + .build()); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) + .isEqualTo(0); + + AtomicBoolean receivedOnMediaItemTransition = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { + receivedOnMediaItemTransition.set(true); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + latch.countDown(); + } + }; + player.addListener(listener); + + // skipToNext() should be no-op. + controllerCompat.getTransportControls().skipToNext(); + controllerCompat.getTransportControls().setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedOnMediaItemTransition.get()).isFalse(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void + playerWithCommandSeekToPrevious_withoutCommandSeekToPreviousMediaItem_actionSkipToPreviousAdvertised() + throws Exception { + Player player = + createPlayerWithCommands( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("asset://media/wav/sample.wav"), + MediaItem.fromUri("asset://media/wav/sample_rf64.wav")), + /* startIndex= */ 1, + /* startPositionMs= */ C.TIME_UNSET); + createdPlayer.prepare(); + }), + /* availableCommands= */ new Player.Commands.Builder() + .add(Player.COMMAND_SEEK_TO_PREVIOUS) + .build(), + /* excludedCommands= */ new Player.Commands.Builder() + .add(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) + .build()); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) + .isNotEqualTo(0); + + AtomicInteger mediaItemTransitionReason = new AtomicInteger(-1); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { + mediaItemTransitionReason.set(reason); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().skipToPrevious(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(mediaItemTransitionReason.get()).isEqualTo(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void + playerWithCommandSeekToPreviousMediaItem_withoutCommandSeekToPrevious_actionSkipToPreviousAdvertised() + throws Exception { + Player player = + createPlayerWithCommands( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("asset://media/wav/sample.wav"), + MediaItem.fromUri("asset://media/wav/sample_rf64.wav")), + /* startIndex= */ 1, + /* startPositionMs= */ C.TIME_UNSET); + createdPlayer.prepare(); + }), + /* availableCommands= */ new Player.Commands.Builder() + .add(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) + .build(), + /* excludedCommands= */ new Player.Commands.Builder() + .add(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) + .isNotEqualTo(0); + + AtomicInteger mediaItemTransitionReason = new AtomicInteger(-1); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { + mediaItemTransitionReason.set(reason); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().skipToPrevious(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(mediaItemTransitionReason.get()).isEqualTo(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void + playerWithoutCommandSeekToPreviousAndCommandSeekToPreviousMediaItem_actionSkipToPreviousNotAdvertised() + throws Exception { + Player player = + createPlayerWithCommands( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("asset://media/wav/sample.wav"), + MediaItem.fromUri("asset://media/wav/sample_rf64.wav")), + /* startIndex= */ 1, + /* startPositionMs= */ C.TIME_UNSET); + createdPlayer.prepare(); + }), + /* availableCommands= */ Player.Commands.EMPTY, + /* excludedCommands= */ new Player.Commands.Builder() + .addAll(Player.COMMAND_SEEK_TO_PREVIOUS, Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) + .build()); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) + .isEqualTo(0); + + AtomicBoolean receivedOnMediaItemTransition = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onMediaItemTransition( + @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { + receivedOnMediaItemTransition.set(true); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + latch.countDown(); + } + }; + player.addListener(listener); + + // skipToPrevious() should be no-op. + controllerCompat.getTransportControls().skipToPrevious(); + controllerCompat.getTransportControls().setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedOnMediaItemTransition.get()).isFalse(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithCommandSetMediaItem_actionsPlayFromXAndPrepareFromXAdvertised() + throws Exception { + Player player = + createPlayerWithAvailableCommand(createDefaultPlayer(), Player.COMMAND_SET_MEDIA_ITEM); + MediaSession mediaSession = + createMediaSession( + player, + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, + MediaSession.ControllerInfo controller, + List mediaItems) { + return Futures.immediateFuture( + ImmutableList.of(MediaItem.fromUri("asset://media/wav/sample.wav"))); + } + }); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + long actions = + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions(); + + assertThat(actions & PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID).isNotEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH).isNotEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PLAY_FROM_URI).isNotEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID).isNotEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH).isNotEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PREPARE_FROM_URI).isNotEqualTo(0); + + ConditionVariable conditionVariable = new ConditionVariable(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onTimelineChanged(Timeline timeline, int reason) { + conditionVariable.open(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().playFromMediaId(/* mediaId= */ "mediaId", Bundle.EMPTY); + assertThat(conditionVariable.block(TIMEOUT_MS)).isTrue(); + + conditionVariable.close(); + controllerCompat + .getTransportControls() + .playFromUri(Uri.parse("https://example.invalid"), Bundle.EMPTY); + assertThat(conditionVariable.block(TIMEOUT_MS)).isTrue(); + + conditionVariable.close(); + controllerCompat.getTransportControls().playFromSearch(/* query= */ "search", Bundle.EMPTY); + assertThat(conditionVariable.block(TIMEOUT_MS)).isTrue(); + + conditionVariable.close(); + controllerCompat + .getTransportControls() + .prepareFromMediaId(/* mediaId= */ "mediaId", Bundle.EMPTY); + assertThat(conditionVariable.block(TIMEOUT_MS)).isTrue(); + + conditionVariable.close(); + controllerCompat + .getTransportControls() + .prepareFromUri(Uri.parse("https://example.invalid"), Bundle.EMPTY); + assertThat(conditionVariable.block(TIMEOUT_MS)).isTrue(); + + conditionVariable.close(); + controllerCompat.getTransportControls().prepareFromSearch(/* query= */ "search", Bundle.EMPTY); + assertThat(conditionVariable.block(TIMEOUT_MS)).isTrue(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithoutCommandSetMediaItem_actionsPlayFromXAndPrepareFromXNotAdvertised() + throws Exception { + Player player = + createPlayerWithExcludedCommand(createDefaultPlayer(), Player.COMMAND_SET_MEDIA_ITEM); + MediaSession mediaSession = + createMediaSession( + player, + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, + MediaSession.ControllerInfo controller, + List mediaItems) { + return Futures.immediateFuture( + ImmutableList.of(MediaItem.fromUri("asset://media/wav/sample.wav"))); + } + }); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + long actions = + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions(); + + assertThat(actions & PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID).isEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH).isEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PLAY_FROM_URI).isEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID).isEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH).isEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PREPARE_FROM_URI).isEqualTo(0); + + AtomicBoolean receivedOnTimelineChanged = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onTimelineChanged(Timeline timeline, int reason) { + receivedOnTimelineChanged.set(true); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + latch.countDown(); + } + }; + player.addListener(listener); + + // prepareFrom and playFrom methods should be no-op. + MediaControllerCompat.TransportControls transportControls = + controllerCompat.getTransportControls(); + transportControls.prepareFromMediaId(/* mediaId= */ "mediaId", Bundle.EMPTY); + transportControls.prepareFromSearch(/* query= */ "search", Bundle.EMPTY); + transportControls.prepareFromUri(Uri.parse("https://example.invalid"), Bundle.EMPTY); + transportControls.playFromMediaId(/* mediaId= */ "mediaId", Bundle.EMPTY); + transportControls.playFromSearch(/* query= */ "search", Bundle.EMPTY); + transportControls.playFromUri(Uri.parse("https://example.invalid"), Bundle.EMPTY); + transportControls.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedOnTimelineChanged.get()).isFalse(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithCommandSetRepeatMode_actionSetRepeatModeAdvertised() throws Exception { + Player player = + createPlayerWithAvailableCommand(createDefaultPlayer(), Player.COMMAND_SET_REPEAT_MODE); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SET_REPEAT_MODE) + .isNotEqualTo(0); + + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onRepeatModeChanged(int repeatMode) { + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().setRepeatMode(PlaybackStateCompat.REPEAT_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithoutCommandSetRepeatMode_actionSetRepeatModeNotAdvertised() + throws Exception { + Player player = + createPlayerWithExcludedCommand(createDefaultPlayer(), Player.COMMAND_SET_REPEAT_MODE); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SET_REPEAT_MODE) + .isEqualTo(0); + + AtomicBoolean repeatModeChanged = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + Player.Listener listener = + new Player.Listener() { + @Override + public void onRepeatModeChanged(int repeatMode) { + repeatModeChanged.set(true); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + latch.countDown(); + } + }; + player.addListener(listener); + + // setRepeatMode() should be no-op + controllerCompat.getTransportControls().setRepeatMode(PlaybackStateCompat.REPEAT_MODE_ALL); + controllerCompat.getTransportControls().setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(repeatModeChanged.get()).isFalse(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithCommandSetSpeedAndPitch_actionSetPlaybackSpeedAdvertised() + throws Exception { + Player player = + createPlayerWithAvailableCommand(createDefaultPlayer(), Player.COMMAND_SET_SPEED_AND_PITCH); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED) + .isNotEqualTo(0); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference playbackParametersRef = new AtomicReference<>(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + playbackParametersRef.set(playbackParameters); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().setPlaybackSpeed(0.5f); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackParametersRef.get().speed).isEqualTo(0.5f); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithoutCommandSetSpeedAndPitch_actionSetPlaybackSpeedNotAdvertised() + throws Exception { + Player player = + createPlayerWithExcludedCommand(createDefaultPlayer(), Player.COMMAND_SET_SPEED_AND_PITCH); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat( + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions() + & PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED) + .isEqualTo(0); + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean receivedPlaybackParameters = new AtomicBoolean(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + receivedPlaybackParameters.set(true); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + latch.countDown(); + } + }; + player.addListener(listener); + + // setPlaybackSpeed() should be no-op. + controllerCompat.getTransportControls().setPlaybackSpeed(0.5f); + controllerCompat.getTransportControls().setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedPlaybackParameters.get()).isFalse(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithCommandSetShuffleMode_actionSetShuffleModeAdvertised() throws Exception { + Player player = + createPlayerWithAvailableCommand(createDefaultPlayer(), Player.COMMAND_SET_SHUFFLE_MODE); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + long actions = + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions(); + + assertThat(actions & PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE).isNotEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED).isNotEqualTo(0); + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean receivedShuffleModeEnabled = new AtomicBoolean(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + receivedShuffleModeEnabled.set(shuffleModeEnabled); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedShuffleModeEnabled.get()).isTrue(); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithoutCommandSetShuffleMode_actionSetShuffleModeNotAdvertised() + throws Exception { + Player player = + createPlayerWithExcludedCommand(createDefaultPlayer(), Player.COMMAND_SET_SHUFFLE_MODE); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + long actions = + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()).getActions(); + + assertThat(actions & PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE).isEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED).isEqualTo(0); + + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean receivedShuffleModeEnabled = new AtomicBoolean(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + receivedShuffleModeEnabled.set(shuffleModeEnabled); + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + latch.countDown(); + } + }; + player.addListener(listener); + + // setShuffleMode() should be no-op + controllerCompat.getTransportControls().setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_ALL); + controllerCompat.getTransportControls().setRepeatMode(PlaybackStateCompat.REPEAT_MODE_ALL); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedShuffleModeEnabled.get()).isFalse(); + + mediaSession.release(); + releasePlayer(player); + } + + private PlaybackStateCompat getFirstPlaybackState( + MediaControllerCompat mediaControllerCompat, Handler handler) throws InterruptedException { + LinkedBlockingDeque playbackStateCompats = new LinkedBlockingDeque<>(); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + playbackStateCompats.add(state); + } + }; + mediaControllerCompat.registerCallback(callback, handler); + PlaybackStateCompat playbackStateCompat = playbackStateCompats.take(); + mediaControllerCompat.unregisterCallback(callback); + return playbackStateCompat; + } + + /** + * Creates a default {@link ExoPlayer} instance on the main thread. Use {@link + * #releasePlayer(Player)} to release the returned instance on the main thread. + */ + private static Player createDefaultPlayer() { + return createPlayer(/* onPostCreationTask= */ player -> {}); + } + + /** + * Creates a player on the main thread. After the player is created, {@code onPostCreationTask} is + * called from the main thread to set any initial state on the player. + */ + private static Player createPlayer(Consumer onPostCreationTask) { + AtomicReference playerRef = new AtomicReference<>(); + getInstrumentation() + .runOnMainSync( + () -> { + ExoPlayer exoPlayer = + new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(); + onPostCreationTask.accept(exoPlayer); + playerRef.set(exoPlayer); + }); + return playerRef.get(); + } + + private static MediaSession createMediaSession(Player player) { + return createMediaSession(player, null); + } + + private static MediaSession createMediaSession( + Player player, @Nullable MediaSession.Callback callback) { + MediaSession.Builder session = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player); + if (callback != null) { + session.setCallback(callback); + } + return session.build(); + } + + private static MediaControllerCompat createMediaControllerCompat(MediaSession mediaSession) { + return new MediaControllerCompat( + ApplicationProvider.getApplicationContext(), + mediaSession.getSessionCompat().getSessionToken()); + } + + /** Releases the {@code player} on the main thread. */ + private static void releasePlayer(Player player) { + getInstrumentation().runOnMainSync(player::release); + } + + /** + * Returns an {@link Player} where {@code availableCommand} is always included in the {@linkplain + * Player#getAvailableCommands() available commands}. + */ + private static Player createPlayerWithAvailableCommand( + Player player, @Player.Command int availableCommand) { + return createPlayerWithCommands( + player, new Player.Commands.Builder().add(availableCommand).build(), Player.Commands.EMPTY); + } + + /** + * Returns a {@link Player} where {@code excludedCommand} is always excluded from the {@linkplain + * Player#getAvailableCommands() available commands}. + */ + private static Player createPlayerWithExcludedCommand( + Player player, @Player.Command int excludedCommand) { + return createPlayerWithCommands( + player, Player.Commands.EMPTY, new Player.Commands.Builder().add(excludedCommand).build()); + } + + /** + * Returns an {@link Player} where {@code availableCommands} are always included and {@code + * excludedCommands} are always excluded from the {@linkplain Player#getAvailableCommands() + * available commands}. + */ + private static Player createPlayerWithCommands( + Player player, Player.Commands availableCommands, Player.Commands excludedCommands) { + return new ForwardingPlayer(player) { + @Override + public Commands getAvailableCommands() { + Commands.Builder commands = + super.getAvailableCommands().buildUpon().addAll(availableCommands); + for (int i = 0; i < excludedCommands.size(); i++) { + commands.remove(excludedCommands.get(i)); + } + return commands.build(); + } + + @Override + public boolean isCommandAvailable(int command) { + return getAvailableCommands().contains(command); + } + }; + } +} From d41eedecb46c8883d6194c0dd2968290b731d809 Mon Sep 17 00:00:00 2001 From: Googler Date: Tue, 17 Jan 2023 14:42:18 +0000 Subject: [PATCH 111/141] Disables play/pause button when there's nothing to play PiperOrigin-RevId: 502571320 (cherry picked from commit d49a16e094d6d4bde0d1dc1ec42876c156b9c55a) --- .../main/java/androidx/media3/ui/PlayerControlView.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java index 461dbd2dd0d..f5aa0dca5d5 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java @@ -980,6 +980,9 @@ private void updatePlayPauseButton() { ((ImageView) playPauseButton) .setImageDrawable(getDrawable(getContext(), resources, drawableRes)); playPauseButton.setContentDescription(resources.getString(stringRes)); + + boolean enablePlayPause = shouldEnablePlayPauseButton(); + updateButton(enablePlayPause, playPauseButton); } } @@ -1497,6 +1500,10 @@ private void onLayoutChange( } } + private boolean shouldEnablePlayPauseButton() { + return player != null && !player.getCurrentTimeline().isEmpty(); + } + private boolean shouldShowPauseButton() { return player != null && player.getPlaybackState() != Player.STATE_ENDED From 2eab93d5c530380c30a4b95ad19bdc3243b1b398 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 17 Jan 2023 17:30:02 +0000 Subject: [PATCH 112/141] Make availableCommands known when bundling PlayerInfo When bundling PlayerInfo, we remove data when the controller is not allowed to access this data via getters. We also remove data for performance reasons. In the toBundle() method, it's currently hard to make the connection between allowed commands and filtering, because the values are checked at a different place. This can be made more readable by forwarding the applicable Commands directly. The only functional fix is to filter the Timeline when sending the first PlayerInfo after a connecting a controller if the command to get the Timeline is not available. This also allows us to remove a path to filter MediaItems from Timelines as it isn't used. PiperOrigin-RevId: 502607391 (cherry picked from commit c90ca7ba5fb9e83956e9494a584ae6b0620e3b14) --- .../java/androidx/media3/common/Timeline.java | 45 +++---------------- .../androidx/media3/common/TimelineTest.java | 6 +-- .../media3/session/ConnectionState.java | 13 ++---- .../androidx/media3/session/MediaSession.java | 4 +- .../media3/session/MediaSessionImpl.java | 21 +++------ .../media3/session/MediaSessionStub.java | 28 +++++------- .../androidx/media3/session/MediaUtils.java | 5 ++- .../androidx/media3/session/PlayerInfo.java | 26 +++++------ 8 files changed, 45 insertions(+), 103 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java index 8e37968a0c3..1d7706f907c 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java +++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java @@ -430,18 +430,17 @@ public int hashCode() { private static final String FIELD_POSITION_IN_FIRST_PERIOD_US = Util.intToStringMaxRadix(13); /** - * Returns a {@link Bundle} representing the information stored in this object. + * {@inheritDoc} * *

        It omits the {@link #uid} and {@link #manifest} fields. The {@link #uid} of an instance * restored by {@link #CREATOR} will be a fake {@link Object} and the {@link #manifest} of the * instance will be {@code null}. - * - * @param excludeMediaItem Whether to exclude {@link #mediaItem} of window. */ @UnstableApi - public Bundle toBundle(boolean excludeMediaItem) { + @Override + public Bundle toBundle() { Bundle bundle = new Bundle(); - if (!excludeMediaItem) { + if (!MediaItem.EMPTY.equals(mediaItem)) { bundle.putBundle(FIELD_MEDIA_ITEM, mediaItem.toBundle()); } if (presentationStartTimeMs != C.TIME_UNSET) { @@ -485,20 +484,6 @@ public Bundle toBundle(boolean excludeMediaItem) { return bundle; } - /** - * {@inheritDoc} - * - *

        It omits the {@link #uid} and {@link #manifest} fields. The {@link #uid} of an instance - * restored by {@link #CREATOR} will be a fake {@link Object} and the {@link #manifest} of the - * instance will be {@code null}. - */ - // TODO(b/166765820): See if missing fields would be okay and add them to the Bundle otherwise. - @UnstableApi - @Override - public Bundle toBundle() { - return toBundle(/* excludeMediaItem= */ false); - } - /** * Object that can restore {@link Period} from a {@link Bundle}. * @@ -1396,18 +1381,15 @@ public int hashCode() { *

        The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of * an instance restored by {@link #CREATOR} may have missing fields as described in {@link * Window#toBundle()} and {@link Period#toBundle()}. - * - * @param excludeMediaItems Whether to exclude all {@link Window#mediaItem media items} of windows - * in the timeline. */ @UnstableApi - public final Bundle toBundle(boolean excludeMediaItems) { + @Override + public final Bundle toBundle() { List windowBundles = new ArrayList<>(); int windowCount = getWindowCount(); Window window = new Window(); for (int i = 0; i < windowCount; i++) { - windowBundles.add( - getWindow(i, window, /* defaultPositionProjectionUs= */ 0).toBundle(excludeMediaItems)); + windowBundles.add(getWindow(i, window, /* defaultPositionProjectionUs= */ 0).toBundle()); } List periodBundles = new ArrayList<>(); @@ -1434,19 +1416,6 @@ public final Bundle toBundle(boolean excludeMediaItems) { return bundle; } - /** - * {@inheritDoc} - * - *

        The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of - * an instance restored by {@link #CREATOR} may have missing fields as described in {@link - * Window#toBundle()} and {@link Period#toBundle()}. - */ - @UnstableApi - @Override - public final Bundle toBundle() { - return toBundle(/* excludeMediaItems= */ false); - } - /** * Object that can restore a {@link Timeline} from a {@link Bundle}. * diff --git a/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java b/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java index 111652b38b9..716e16ec3c0 100644 --- a/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java @@ -350,10 +350,8 @@ public void window_toBundleSkipsDefaultValues_fromBundleRestoresThem() { Bundle windowBundle = window.toBundle(); - // Check that default values are skipped when bundling. MediaItem key is not added to the bundle - // only when excludeMediaItem is true. - assertThat(windowBundle.keySet()).hasSize(1); - assertThat(window.toBundle(/* excludeMediaItem= */ true).keySet()).isEmpty(); + // Check that default values are skipped when bundling. + assertThat(windowBundle.keySet()).isEmpty(); Timeline.Window restoredWindow = Timeline.Window.CREATOR.fromBundle(windowBundle); diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java index 113848eda06..c681ab4420e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java @@ -94,19 +94,12 @@ public Bundle toBundle() { bundle.putBundle(FIELD_PLAYER_COMMANDS_FROM_SESSION, playerCommandsFromSession.toBundle()); bundle.putBundle(FIELD_PLAYER_COMMANDS_FROM_PLAYER, playerCommandsFromPlayer.toBundle()); bundle.putBundle(FIELD_TOKEN_EXTRAS, tokenExtras); + Player.Commands intersectedCommands = + MediaUtils.intersect(playerCommandsFromSession, playerCommandsFromPlayer); bundle.putBundle( FIELD_PLAYER_INFO, playerInfo.toBundle( - /* excludeMediaItems= */ !playerCommandsFromPlayer.contains(Player.COMMAND_GET_TIMELINE) - || !playerCommandsFromSession.contains(Player.COMMAND_GET_TIMELINE), - /* excludeMediaItemsMetadata= */ !playerCommandsFromPlayer.contains( - Player.COMMAND_GET_MEDIA_ITEMS_METADATA) - || !playerCommandsFromSession.contains(Player.COMMAND_GET_MEDIA_ITEMS_METADATA), - /* excludeCues= */ !playerCommandsFromPlayer.contains(Player.COMMAND_GET_TEXT) - || !playerCommandsFromSession.contains(Player.COMMAND_GET_TEXT), - /* excludeTimeline= */ false, - /* excludeTracks= */ !playerCommandsFromPlayer.contains(Player.COMMAND_GET_TRACKS) - || !playerCommandsFromSession.contains(Player.COMMAND_GET_TRACKS))); + intersectedCommands, /* excludeTimeline= */ false, /* excludeTracks= */ false)); bundle.putInt(FIELD_SESSION_INTERFACE_VERSION, sessionInterfaceVersion); return bundle; } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 5fead90f84f..d6bcc84d3cf 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -1155,9 +1155,7 @@ default void onPlayerChanged( default void onPlayerInfoChanged( int seq, PlayerInfo playerInfo, - boolean excludeMediaItems, - boolean excludeMediaItemsMetadata, - boolean excludeCues, + Player.Commands availableCommands, boolean excludeTimeline, boolean excludeTracks, int controllerInterfaceVersion) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 7ad0de53e35..11b6f37b154 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -15,9 +15,6 @@ */ package androidx.media3.session; -import static androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA; -import static androidx.media3.common.Player.COMMAND_GET_TEXT; -import static androidx.media3.common.Player.COMMAND_GET_TIMELINE; import static androidx.media3.common.Player.COMMAND_GET_TRACKS; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; @@ -416,21 +413,17 @@ private void dispatchOnPlayerInfoChanged( // 0 is OK for legacy controllers, because they didn't have sequence numbers. seq = 0; } + Player.Commands intersectedCommands = + MediaUtils.intersect( + controllersManager.getAvailablePlayerCommands(controller), + getPlayerWrapper().getAvailableCommands()); checkStateNotNull(controller.getControllerCb()) .onPlayerInfoChanged( seq, playerInfo, - /* excludeMediaItems= */ !controllersManager.isPlayerCommandAvailable( - controller, COMMAND_GET_TIMELINE), - /* excludeMediaItemsMetadata= */ !controllersManager.isPlayerCommandAvailable( - controller, COMMAND_GET_MEDIA_ITEMS_METADATA), - /* excludeCues= */ !controllersManager.isPlayerCommandAvailable( - controller, COMMAND_GET_TEXT), - excludeTimeline - || !controllersManager.isPlayerCommandAvailable( - controller, COMMAND_GET_TIMELINE), - excludeTracks - || !controllersManager.isPlayerCommandAvailable(controller, COMMAND_GET_TRACKS), + intersectedCommands, + excludeTimeline, + excludeTracks, controller.getInterfaceVersion()); } catch (DeadObjectException e) { onDeadObjectException(controller); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index aa51cb519c2..1cbb8106d57 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -1606,35 +1606,29 @@ public void onLibraryResult(int sequenceNumber, LibraryResult result) public void onPlayerInfoChanged( int sequenceNumber, PlayerInfo playerInfo, - boolean excludeMediaItems, - boolean excludeMediaItemsMetadata, - boolean excludeCues, + Player.Commands availableCommands, boolean excludeTimeline, boolean excludeTracks, int controllerInterfaceVersion) throws RemoteException { Assertions.checkState(controllerInterfaceVersion != 0); + // The bundling exclusions merge the performance overrides with the available commands. + boolean bundlingExclusionsTimeline = + excludeTimeline || !availableCommands.contains(Player.COMMAND_GET_TIMELINE); + boolean bundlingExclusionsTracks = + excludeTracks || !availableCommands.contains(Player.COMMAND_GET_TRACKS); if (controllerInterfaceVersion >= 2) { iController.onPlayerInfoChangedWithExclusions( sequenceNumber, - playerInfo.toBundle( - excludeMediaItems, - excludeMediaItemsMetadata, - excludeCues, - excludeTimeline, - excludeTracks), - new PlayerInfo.BundlingExclusions(excludeTimeline, excludeTracks).toBundle()); + playerInfo.toBundle(availableCommands, excludeTimeline, excludeTracks), + new PlayerInfo.BundlingExclusions(bundlingExclusionsTimeline, bundlingExclusionsTracks) + .toBundle()); } else { //noinspection deprecation iController.onPlayerInfoChanged( sequenceNumber, - playerInfo.toBundle( - excludeMediaItems, - excludeMediaItemsMetadata, - excludeCues, - excludeTimeline, - /* excludeTracks= */ true), - excludeTimeline); + playerInfo.toBundle(availableCommands, excludeTimeline, /* excludeTracks= */ true), + bundlingExclusionsTimeline); } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 716f08a29dd..0d61696904f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -1292,7 +1292,10 @@ public static Commands createPlayerCommandsWithout(@Command int command) { * Returns the intersection of {@link Player.Command commands} from the given two {@link * Commands}. */ - public static Commands intersect(Commands commands1, Commands commands2) { + public static Commands intersect(@Nullable Commands commands1, @Nullable Commands commands2) { + if (commands1 == null || commands2 == null) { + return Commands.EMPTY; + } Commands.Builder intersectCommandsBuilder = new Commands.Builder(); for (int i = 0; i < commands1.size(); i++) { if (commands2.contains(commands1.get(i))) { diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java index dfb94e8a3db..f4bd254eed8 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java @@ -799,11 +799,7 @@ private boolean isPlaying( // Next field key = 31 public Bundle toBundle( - boolean excludeMediaItems, - boolean excludeMediaItemsMetadata, - boolean excludeCues, - boolean excludeTimeline, - boolean excludeTracks) { + Player.Commands availableCommands, boolean excludeTimeline, boolean excludeTracks) { Bundle bundle = new Bundle(); if (playerError != null) { bundle.putBundle(FIELD_PLAYBACK_ERROR, playerError.toBundle()); @@ -816,16 +812,16 @@ public Bundle toBundle( bundle.putBundle(FIELD_PLAYBACK_PARAMETERS, playbackParameters.toBundle()); bundle.putInt(FIELD_REPEAT_MODE, repeatMode); bundle.putBoolean(FIELD_SHUFFLE_MODE_ENABLED, shuffleModeEnabled); - if (!excludeTimeline) { - bundle.putBundle(FIELD_TIMELINE, timeline.toBundle(excludeMediaItems)); + if (!excludeTimeline && availableCommands.contains(Player.COMMAND_GET_TIMELINE)) { + bundle.putBundle(FIELD_TIMELINE, timeline.toBundle()); } bundle.putBundle(FIELD_VIDEO_SIZE, videoSize.toBundle()); - if (!excludeMediaItemsMetadata) { + if (availableCommands.contains(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)) { bundle.putBundle(FIELD_PLAYLIST_METADATA, playlistMetadata.toBundle()); } bundle.putFloat(FIELD_VOLUME, volume); bundle.putBundle(FIELD_AUDIO_ATTRIBUTES, audioAttributes.toBundle()); - if (!excludeCues) { + if (availableCommands.contains(Player.COMMAND_GET_TEXT)) { bundle.putBundle(FIELD_CUE_GROUP, cueGroup.toBundle()); } bundle.putBundle(FIELD_DEVICE_INFO, deviceInfo.toBundle()); @@ -836,13 +832,13 @@ public Bundle toBundle( bundle.putInt(FIELD_PLAYBACK_STATE, playbackState); bundle.putBoolean(FIELD_IS_PLAYING, isPlaying); bundle.putBoolean(FIELD_IS_LOADING, isLoading); - bundle.putBundle( - FIELD_MEDIA_METADATA, - excludeMediaItems ? MediaMetadata.EMPTY.toBundle() : mediaMetadata.toBundle()); + if (availableCommands.contains(Player.COMMAND_GET_TIMELINE)) { + bundle.putBundle(FIELD_MEDIA_METADATA, mediaMetadata.toBundle()); + } bundle.putLong(FIELD_SEEK_BACK_INCREMENT_MS, seekBackIncrementMs); bundle.putLong(FIELD_SEEK_FORWARD_INCREMENT_MS, seekForwardIncrementMs); bundle.putLong(FIELD_MAX_SEEK_TO_PREVIOUS_POSITION_MS, maxSeekToPreviousPositionMs); - if (!excludeTracks) { + if (!excludeTracks && availableCommands.contains(Player.COMMAND_GET_TRACKS)) { bundle.putBundle(FIELD_CURRENT_TRACKS, currentTracks.toBundle()); } bundle.putBundle(FIELD_TRACK_SELECTION_PARAMETERS, trackSelectionParameters.toBundle()); @@ -853,9 +849,7 @@ public Bundle toBundle( @Override public Bundle toBundle() { return toBundle( - /* excludeMediaItems= */ false, - /* excludeMediaItemsMetadata= */ false, - /* excludeCues= */ false, + /* availableCommands= */ new Player.Commands.Builder().addAllCommands().build(), /* excludeTimeline= */ false, /* excludeTracks= */ false); } From 0606ab0cbb020c6fe9370faa75126585c80ce480 Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 18 Jan 2023 10:54:42 +0000 Subject: [PATCH 113/141] Fix javadoc references to `writeSampleData` PiperOrigin-RevId: 502821506 (cherry picked from commit 6c14ffc1ecd13393930b9f5ee7ad7a52391d0f65) --- .../androidx/media3/extractor/mkv/MatroskaExtractor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mkv/MatroskaExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mkv/MatroskaExtractor.java index 9bd0503ab0c..b8ff74a6794 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mkv/MatroskaExtractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mkv/MatroskaExtractor.java @@ -1652,8 +1652,8 @@ private int writeSampleData(ExtractorInput input, Track track, int size, boolean } /** - * Called by {@link #writeSampleData(ExtractorInput, Track, int)} when the sample has been - * written. Returns the final sample size and resets state for the next sample. + * Called by {@link #writeSampleData(ExtractorInput, Track, int, boolean)} when the sample has + * been written. Returns the final sample size and resets state for the next sample. */ private int finishWriteSampleData() { int sampleSize = sampleBytesWritten; @@ -1661,7 +1661,7 @@ private int finishWriteSampleData() { return sampleSize; } - /** Resets state used by {@link #writeSampleData(ExtractorInput, Track, int)}. */ + /** Resets state used by {@link #writeSampleData(ExtractorInput, Track, int, boolean)}. */ private void resetWriteSampleData() { sampleBytesRead = 0; sampleBytesWritten = 0; From b8b6ddf34769398da017ea8846dcbfc3d06dfc21 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 18 Jan 2023 13:49:01 +0000 Subject: [PATCH 114/141] Correctly filter PlayerInfo by available getter commands. When bundling PlayerInfo, we need to remove information if the controller is not allowed to access it. This was only partially done at the moment. PiperOrigin-RevId: 502852798 (cherry picked from commit 69cfba7c53b563577390e4074fd270f078bf6069) --- .../java/androidx/media3/common/Player.java | 44 +- .../androidx/media3/session/MediaSession.java | 6 +- .../media3/session/MediaSessionImpl.java | 35 +- .../session/MediaSessionLegacyStub.java | 6 +- .../media3/session/MediaSessionStub.java | 9 +- .../androidx/media3/session/PlayerInfo.java | 34 +- .../media3/session/PlayerWrapper.java | 39 +- .../media3/session/SessionPositionInfo.java | 34 +- .../media3/session/PlayerInfoTest.java | 500 ++++++++++++++++++ 9 files changed, 646 insertions(+), 61 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index c9e7d4d360e..17183a7f963 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -286,16 +286,31 @@ public int hashCode() { @UnstableApi @Override public Bundle toBundle() { + return toBundle(/* canAccessCurrentMediaItem= */ true, /* canAccessTimeline= */ true); + } + + /** + * Returns a {@link Bundle} representing the information stored in this object, filtered by + * available commands. + * + * @param canAccessCurrentMediaItem Whether the {@link Bundle} should contain information + * accessbile with {@link #COMMAND_GET_CURRENT_MEDIA_ITEM}. + * @param canAccessTimeline Whether the {@link Bundle} should contain information accessbile + * with {@link #COMMAND_GET_TIMELINE}. + */ + @UnstableApi + public Bundle toBundle(boolean canAccessCurrentMediaItem, boolean canAccessTimeline) { Bundle bundle = new Bundle(); - bundle.putInt(FIELD_MEDIA_ITEM_INDEX, mediaItemIndex); - if (mediaItem != null) { + bundle.putInt(FIELD_MEDIA_ITEM_INDEX, canAccessTimeline ? mediaItemIndex : 0); + if (mediaItem != null && canAccessCurrentMediaItem) { bundle.putBundle(FIELD_MEDIA_ITEM, mediaItem.toBundle()); } - bundle.putInt(FIELD_PERIOD_INDEX, periodIndex); - bundle.putLong(FIELD_POSITION_MS, positionMs); - bundle.putLong(FIELD_CONTENT_POSITION_MS, contentPositionMs); - bundle.putInt(FIELD_AD_GROUP_INDEX, adGroupIndex); - bundle.putInt(FIELD_AD_INDEX_IN_AD_GROUP, adIndexInAdGroup); + bundle.putInt(FIELD_PERIOD_INDEX, canAccessTimeline ? periodIndex : 0); + bundle.putLong(FIELD_POSITION_MS, canAccessCurrentMediaItem ? positionMs : 0); + bundle.putLong(FIELD_CONTENT_POSITION_MS, canAccessCurrentMediaItem ? contentPositionMs : 0); + bundle.putInt(FIELD_AD_GROUP_INDEX, canAccessCurrentMediaItem ? adGroupIndex : C.INDEX_UNSET); + bundle.putInt( + FIELD_AD_INDEX_IN_AD_GROUP, canAccessCurrentMediaItem ? adIndexInAdGroup : C.INDEX_UNSET); return bundle; } @@ -303,15 +318,14 @@ public Bundle toBundle() { @UnstableApi public static final Creator CREATOR = PositionInfo::fromBundle; private static PositionInfo fromBundle(Bundle bundle) { - int mediaItemIndex = bundle.getInt(FIELD_MEDIA_ITEM_INDEX, /* defaultValue= */ C.INDEX_UNSET); + int mediaItemIndex = bundle.getInt(FIELD_MEDIA_ITEM_INDEX, /* defaultValue= */ 0); @Nullable Bundle mediaItemBundle = bundle.getBundle(FIELD_MEDIA_ITEM); @Nullable MediaItem mediaItem = mediaItemBundle == null ? null : MediaItem.CREATOR.fromBundle(mediaItemBundle); - int periodIndex = bundle.getInt(FIELD_PERIOD_INDEX, /* defaultValue= */ C.INDEX_UNSET); - long positionMs = bundle.getLong(FIELD_POSITION_MS, /* defaultValue= */ C.TIME_UNSET); - long contentPositionMs = - bundle.getLong(FIELD_CONTENT_POSITION_MS, /* defaultValue= */ C.TIME_UNSET); + int periodIndex = bundle.getInt(FIELD_PERIOD_INDEX, /* defaultValue= */ 0); + long positionMs = bundle.getLong(FIELD_POSITION_MS, /* defaultValue= */ 0); + long contentPositionMs = bundle.getLong(FIELD_CONTENT_POSITION_MS, /* defaultValue= */ 0); int adGroupIndex = bundle.getInt(FIELD_AD_GROUP_INDEX, /* defaultValue= */ C.INDEX_UNSET); int adIndexInAdGroup = bundle.getInt(FIELD_AD_INDEX_IN_AD_GROUP, /* defaultValue= */ C.INDEX_UNSET); @@ -2281,6 +2295,9 @@ default void onMetadata(Metadata metadata) {} *

        Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when * the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more * details. + * + *

        This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + * #getAvailableCommands() available}. */ boolean hasPreviousMediaItem(); @@ -2367,6 +2384,9 @@ default void onMetadata(Metadata metadata) {} *

        Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when * the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more * details. + * + *

        This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + * #getAvailableCommands() available}. */ boolean hasNextMediaItem(); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index d6bcc84d3cf..7f09c4280ba 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -1162,7 +1162,11 @@ default void onPlayerInfoChanged( throws RemoteException {} default void onPeriodicSessionPositionInfoChanged( - int seq, SessionPositionInfo sessionPositionInfo) throws RemoteException {} + int seq, + SessionPositionInfo sessionPositionInfo, + boolean canAccessCurrentMediaItem, + boolean canAccessTimeline) + throws RemoteException {} // Mostly matched with MediaController.ControllerCallback diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 11b6f37b154..9e7f201d79c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -601,6 +601,38 @@ private void dispatchRemoteControllerTaskToLegacyStub(RemoteControllerTask task) } } + private void dispatchOnPeriodicSessionPositionInfoChanged( + SessionPositionInfo sessionPositionInfo) { + ConnectedControllersManager controllersManager = + sessionStub.getConnectedControllersManager(); + List controllers = + sessionStub.getConnectedControllersManager().getConnectedControllers(); + for (int i = 0; i < controllers.size(); i++) { + ControllerInfo controller = controllers.get(i); + boolean canAccessCurrentMediaItem = + controllersManager.isPlayerCommandAvailable( + controller, Player.COMMAND_GET_CURRENT_MEDIA_ITEM); + boolean canAccessTimeline = + controllersManager.isPlayerCommandAvailable(controller, Player.COMMAND_GET_TIMELINE); + dispatchRemoteControllerTaskWithoutReturn( + controller, + (controllerCb, seq) -> + controllerCb.onPeriodicSessionPositionInfoChanged( + seq, sessionPositionInfo, canAccessCurrentMediaItem, canAccessTimeline)); + } + try { + sessionLegacyStub + .getControllerLegacyCbForBroadcast() + .onPeriodicSessionPositionInfoChanged( + /* seq= */ 0, + sessionPositionInfo, + /* canAccessCurrentMediaItem= */ true, + /* canAccessTimeline= */ true); + } catch (RemoteException e) { + Log.e(TAG, "Exception in using media1 API", e); + } + } + protected void dispatchRemoteControllerTaskWithoutReturn(RemoteControllerTask task) { List controllers = sessionStub.getConnectedControllersManager().getConnectedControllers(); @@ -719,8 +751,7 @@ private void notifyPeriodicSessionPositionInfoChangesOnHandler() { } } SessionPositionInfo sessionPositionInfo = playerWrapper.createSessionPositionInfoForBundling(); - dispatchRemoteControllerTaskWithoutReturn( - (callback, seq) -> callback.onPeriodicSessionPositionInfoChanged(seq, sessionPositionInfo)); + dispatchOnPeriodicSessionPositionInfoChanged(sessionPositionInfo); schedulePeriodicSessionPositionInfoChanges(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index e130763ea21..d49b5b16665 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -1175,7 +1175,11 @@ public void onDeviceVolumeChanged(int seq, int volume, boolean muted) { @Override public void onPeriodicSessionPositionInfoChanged( - int unusedSeq, SessionPositionInfo unusedSessionPositionInfo) throws RemoteException { + int unusedSeq, + SessionPositionInfo unusedSessionPositionInfo, + boolean unusedCanAccessCurrentMediaItem, + boolean unusedCanAccessTimeline) + throws RemoteException { sessionImpl .getSessionCompat() .setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat()); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 1cbb8106d57..3a36b803686 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -1684,9 +1684,14 @@ public void onDisconnected(int sequenceNumber) throws RemoteException { @Override public void onPeriodicSessionPositionInfoChanged( - int sequenceNumber, SessionPositionInfo sessionPositionInfo) throws RemoteException { + int sequenceNumber, + SessionPositionInfo sessionPositionInfo, + boolean canAccessCurrentMediaItem, + boolean canAccessTimeline) + throws RemoteException { iController.onPeriodicSessionPositionInfoChanged( - sequenceNumber, sessionPositionInfo.toBundle()); + sequenceNumber, + sessionPositionInfo.toBundle(canAccessCurrentMediaItem, canAccessTimeline)); } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java index f4bd254eed8..56207efa9d7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java @@ -801,38 +801,53 @@ private boolean isPlaying( public Bundle toBundle( Player.Commands availableCommands, boolean excludeTimeline, boolean excludeTracks) { Bundle bundle = new Bundle(); + boolean canAccessCurrentMediaItem = + availableCommands.contains(Player.COMMAND_GET_CURRENT_MEDIA_ITEM); + boolean canAccessTimeline = availableCommands.contains(Player.COMMAND_GET_TIMELINE); if (playerError != null) { bundle.putBundle(FIELD_PLAYBACK_ERROR, playerError.toBundle()); } bundle.putInt(FIELD_MEDIA_ITEM_TRANSITION_REASON, mediaItemTransitionReason); - bundle.putBundle(FIELD_SESSION_POSITION_INFO, sessionPositionInfo.toBundle()); - bundle.putBundle(FIELD_OLD_POSITION_INFO, oldPositionInfo.toBundle()); - bundle.putBundle(FIELD_NEW_POSITION_INFO, newPositionInfo.toBundle()); + bundle.putBundle( + FIELD_SESSION_POSITION_INFO, + sessionPositionInfo.toBundle(canAccessCurrentMediaItem, canAccessTimeline)); + bundle.putBundle( + FIELD_OLD_POSITION_INFO, + oldPositionInfo.toBundle(canAccessCurrentMediaItem, canAccessTimeline)); + bundle.putBundle( + FIELD_NEW_POSITION_INFO, + newPositionInfo.toBundle(canAccessCurrentMediaItem, canAccessTimeline)); bundle.putInt(FIELD_DISCONTINUITY_REASON, discontinuityReason); bundle.putBundle(FIELD_PLAYBACK_PARAMETERS, playbackParameters.toBundle()); bundle.putInt(FIELD_REPEAT_MODE, repeatMode); bundle.putBoolean(FIELD_SHUFFLE_MODE_ENABLED, shuffleModeEnabled); - if (!excludeTimeline && availableCommands.contains(Player.COMMAND_GET_TIMELINE)) { + if (!excludeTimeline && canAccessTimeline) { bundle.putBundle(FIELD_TIMELINE, timeline.toBundle()); } bundle.putBundle(FIELD_VIDEO_SIZE, videoSize.toBundle()); if (availableCommands.contains(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)) { bundle.putBundle(FIELD_PLAYLIST_METADATA, playlistMetadata.toBundle()); } - bundle.putFloat(FIELD_VOLUME, volume); - bundle.putBundle(FIELD_AUDIO_ATTRIBUTES, audioAttributes.toBundle()); + if (availableCommands.contains(Player.COMMAND_GET_VOLUME)) { + bundle.putFloat(FIELD_VOLUME, volume); + } + if (availableCommands.contains(Player.COMMAND_GET_AUDIO_ATTRIBUTES)) { + bundle.putBundle(FIELD_AUDIO_ATTRIBUTES, audioAttributes.toBundle()); + } if (availableCommands.contains(Player.COMMAND_GET_TEXT)) { bundle.putBundle(FIELD_CUE_GROUP, cueGroup.toBundle()); } bundle.putBundle(FIELD_DEVICE_INFO, deviceInfo.toBundle()); - bundle.putInt(FIELD_DEVICE_VOLUME, deviceVolume); - bundle.putBoolean(FIELD_DEVICE_MUTED, deviceMuted); + if (availableCommands.contains(Player.COMMAND_GET_DEVICE_VOLUME)) { + bundle.putInt(FIELD_DEVICE_VOLUME, deviceVolume); + bundle.putBoolean(FIELD_DEVICE_MUTED, deviceMuted); + } bundle.putBoolean(FIELD_PLAY_WHEN_READY, playWhenReady); bundle.putInt(FIELD_PLAYBACK_SUPPRESSION_REASON, playbackSuppressionReason); bundle.putInt(FIELD_PLAYBACK_STATE, playbackState); bundle.putBoolean(FIELD_IS_PLAYING, isPlaying); bundle.putBoolean(FIELD_IS_LOADING, isLoading); - if (availableCommands.contains(Player.COMMAND_GET_TIMELINE)) { + if (availableCommands.contains(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)) { bundle.putBundle(FIELD_MEDIA_METADATA, mediaMetadata.toBundle()); } bundle.putLong(FIELD_SEEK_BACK_INCREMENT_MS, seekBackIncrementMs); @@ -842,7 +857,6 @@ public Bundle toBundle( bundle.putBundle(FIELD_CURRENT_TRACKS, currentTracks.toBundle()); } bundle.putBundle(FIELD_TRACK_SELECTION_PARAMETERS, trackSelectionParameters.toBundle()); - return bundle; } diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index bf5f2756c91..42c391f9c4d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -1031,19 +1031,18 @@ public void onAdjustVolume(int direction) { *

        This excludes window uid and period uid that wouldn't be preserved when bundling. */ public PositionInfo createPositionInfoForBundling() { - if (!isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) { - return SessionPositionInfo.DEFAULT_POSITION_INFO; - } + boolean canAccessCurrentMediaItem = isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM); + boolean canAccessTimeline = isCommandAvailable(COMMAND_GET_TIMELINE); return new PositionInfo( /* windowUid= */ null, - getCurrentMediaItemIndex(), - getCurrentMediaItem(), + canAccessTimeline ? getCurrentMediaItemIndex() : 0, + canAccessCurrentMediaItem ? getCurrentMediaItem() : null, /* periodUid= */ null, - getCurrentPeriodIndex(), - getCurrentPosition(), - getContentPosition(), - getCurrentAdGroupIndex(), - getCurrentAdIndexInAdGroup()); + canAccessTimeline ? getCurrentPeriodIndex() : 0, + canAccessCurrentMediaItem ? getCurrentPosition() : 0, + canAccessCurrentMediaItem ? getContentPosition() : 0, + canAccessCurrentMediaItem ? getCurrentAdGroupIndex() : C.INDEX_UNSET, + canAccessCurrentMediaItem ? getCurrentAdIndexInAdGroup() : C.INDEX_UNSET); } /** @@ -1052,20 +1051,18 @@ public PositionInfo createPositionInfoForBundling() { *

        This excludes window uid and period uid that wouldn't be preserved when bundling. */ public SessionPositionInfo createSessionPositionInfoForBundling() { - if (!isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) { - return SessionPositionInfo.DEFAULT; - } + boolean canAccessCurrentMediaItem = isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM); return new SessionPositionInfo( createPositionInfoForBundling(), - isPlayingAd(), + canAccessCurrentMediaItem && isPlayingAd(), /* eventTimeMs= */ SystemClock.elapsedRealtime(), - getDuration(), - getBufferedPosition(), - getBufferedPercentage(), - getTotalBufferedDuration(), - getCurrentLiveOffset(), - getContentDuration(), - getContentBufferedPosition()); + canAccessCurrentMediaItem ? getDuration() : C.TIME_UNSET, + canAccessCurrentMediaItem ? getBufferedPosition() : 0, + canAccessCurrentMediaItem ? getBufferedPercentage() : 0, + canAccessCurrentMediaItem ? getTotalBufferedDuration() : 0, + canAccessCurrentMediaItem ? getCurrentLiveOffset() : C.TIME_UNSET, + canAccessCurrentMediaItem ? getContentDuration() : C.TIME_UNSET, + canAccessCurrentMediaItem ? getContentBufferedPosition() : 0); } public PlayerInfo createPlayerInfoForBundling() { diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java b/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java index f8960d2a876..a4d537f0cc9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionPositionInfo.java @@ -170,17 +170,28 @@ public String toString() { @Override public Bundle toBundle() { + return toBundle(/* canAccessCurrentMediaItem= */ true, /* canAccessTimeline= */ true); + } + + public Bundle toBundle(boolean canAccessCurrentMediaItem, boolean canAccessTimeline) { Bundle bundle = new Bundle(); - bundle.putBundle(FIELD_POSITION_INFO, positionInfo.toBundle()); - bundle.putBoolean(FIELD_IS_PLAYING_AD, isPlayingAd); + bundle.putBundle( + FIELD_POSITION_INFO, positionInfo.toBundle(canAccessCurrentMediaItem, canAccessTimeline)); + bundle.putBoolean(FIELD_IS_PLAYING_AD, canAccessCurrentMediaItem && isPlayingAd); bundle.putLong(FIELD_EVENT_TIME_MS, eventTimeMs); - bundle.putLong(FIELD_DURATION_MS, durationMs); - bundle.putLong(FIELD_BUFFERED_POSITION_MS, bufferedPositionMs); - bundle.putInt(FIELD_BUFFERED_PERCENTAGE, bufferedPercentage); - bundle.putLong(FIELD_TOTAL_BUFFERED_DURATION_MS, totalBufferedDurationMs); - bundle.putLong(FIELD_CURRENT_LIVE_OFFSET_MS, currentLiveOffsetMs); - bundle.putLong(FIELD_CONTENT_DURATION_MS, contentDurationMs); - bundle.putLong(FIELD_CONTENT_BUFFERED_POSITION_MS, contentBufferedPositionMs); + bundle.putLong(FIELD_DURATION_MS, canAccessCurrentMediaItem ? durationMs : C.TIME_UNSET); + bundle.putLong(FIELD_BUFFERED_POSITION_MS, canAccessCurrentMediaItem ? bufferedPositionMs : 0); + bundle.putInt(FIELD_BUFFERED_PERCENTAGE, canAccessCurrentMediaItem ? bufferedPercentage : 0); + bundle.putLong( + FIELD_TOTAL_BUFFERED_DURATION_MS, canAccessCurrentMediaItem ? totalBufferedDurationMs : 0); + bundle.putLong( + FIELD_CURRENT_LIVE_OFFSET_MS, + canAccessCurrentMediaItem ? currentLiveOffsetMs : C.TIME_UNSET); + bundle.putLong( + FIELD_CONTENT_DURATION_MS, canAccessCurrentMediaItem ? contentDurationMs : C.TIME_UNSET); + bundle.putLong( + FIELD_CONTENT_BUFFERED_POSITION_MS, + canAccessCurrentMediaItem ? contentBufferedPositionMs : 0); return bundle; } @@ -196,8 +207,7 @@ private static SessionPositionInfo fromBundle(Bundle bundle) { boolean isPlayingAd = bundle.getBoolean(FIELD_IS_PLAYING_AD, /* defaultValue= */ false); long eventTimeMs = bundle.getLong(FIELD_EVENT_TIME_MS, /* defaultValue= */ C.TIME_UNSET); long durationMs = bundle.getLong(FIELD_DURATION_MS, /* defaultValue= */ C.TIME_UNSET); - long bufferedPositionMs = - bundle.getLong(FIELD_BUFFERED_POSITION_MS, /* defaultValue= */ C.TIME_UNSET); + long bufferedPositionMs = bundle.getLong(FIELD_BUFFERED_POSITION_MS, /* defaultValue= */ 0); int bufferedPercentage = bundle.getInt(FIELD_BUFFERED_PERCENTAGE, /* defaultValue= */ 0); long totalBufferedDurationMs = bundle.getLong(FIELD_TOTAL_BUFFERED_DURATION_MS, /* defaultValue= */ 0); @@ -206,7 +216,7 @@ private static SessionPositionInfo fromBundle(Bundle bundle) { long contentDurationMs = bundle.getLong(FIELD_CONTENT_DURATION_MS, /* defaultValue= */ C.TIME_UNSET); long contentBufferedPositionMs = - bundle.getLong(FIELD_CONTENT_BUFFERED_POSITION_MS, /* defaultValue= */ C.TIME_UNSET); + bundle.getLong(FIELD_CONTENT_BUFFERED_POSITION_MS, /* defaultValue= */ 0); return new SessionPositionInfo( positionInfo, diff --git a/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java b/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java index 7e8738f9d90..fdc4e2e4c01 100644 --- a/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java @@ -15,10 +15,29 @@ */ package androidx.media3.session; +import static androidx.media3.common.MimeTypes.AUDIO_AAC; import static com.google.common.truth.Truth.assertThat; import android.os.Bundle; +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.C; +import androidx.media3.common.DeviceInfo; +import androidx.media3.common.Format; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.PlaybackParameters; +import androidx.media3.common.Player; +import androidx.media3.common.Timeline; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.Tracks; +import androidx.media3.common.VideoSize; +import androidx.media3.common.text.CueGroup; +import androidx.media3.test.utils.FakeTimeline; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,4 +69,485 @@ public void bundlingExclusionFromBundle_toBundleRoundTrip_equalInstances() { assertThat(resultingBundlingExclusions).isEqualTo(bundlingExclusions); } + + @Test + public void toBundleFromBundle_withAllCommands_restoresAllData() { + PlayerInfo playerInfo = + new PlayerInfo.Builder(PlayerInfo.DEFAULT) + .setOldPositionInfo( + new Player.PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 5, + /* mediaItem= */ new MediaItem.Builder().setMediaId("id1").build(), + /* periodUid= */ null, + /* periodIndex= */ 4, + /* positionMs= */ 4000, + /* contentPositionMs= */ 5000, + /* adGroupIndex= */ 3, + /* adIndexInAdGroup= */ 2)) + .setNewPositionInfo( + new Player.PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 6, + /* mediaItem= */ new MediaItem.Builder().setMediaId("id2").build(), + /* periodUid= */ null, + /* periodIndex= */ 7, + /* positionMs= */ 8000, + /* contentPositionMs= */ 9000, + /* adGroupIndex= */ 5, + /* adIndexInAdGroup= */ 1)) + .setSessionPositionInfo( + new SessionPositionInfo( + new Player.PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 8, + /* mediaItem= */ new MediaItem.Builder().setMediaId("id3").build(), + /* periodUid= */ null, + /* periodIndex= */ 9, + /* positionMs= */ 2000, + /* contentPositionMs= */ 7000, + /* adGroupIndex= */ 9, + /* adIndexInAdGroup= */ 1), + /* isPlayingAd= */ true, + /* eventTimeMs= */ 123456789, + /* durationMs= */ 30000, + /* bufferedPositionMs= */ 20000, + /* bufferedPercentage= */ 50, + /* totalBufferedDurationMs= */ 25000, + /* currentLiveOffsetMs= */ 3000, + /* contentDurationMs= */ 27000, + /* contentBufferedPositionMs= */ 15000)) + .setTimeline(new FakeTimeline(/* windowCount= */ 10)) + .setMediaMetadata(new MediaMetadata.Builder().setTitle("title").build()) + .setPlaylistMetadata(new MediaMetadata.Builder().setArtist("artist").build()) + .setVolume(0.5f) + .setDeviceVolume(10) + .setDeviceMuted(true) + .setAudioAttributes( + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_SPEECH).build()) + .setCues(new CueGroup(/* cues= */ ImmutableList.of(), /* presentationTimeUs= */ 1234)) + .setCurrentTracks( + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup( + new Format.Builder().setSampleMimeType(AUDIO_AAC).build()), + /* adaptiveSupported= */ false, + new int[] {C.FORMAT_EXCEEDS_CAPABILITIES}, + /* trackSelected= */ new boolean[] {true})))) + .setDeviceInfo( + new DeviceInfo( + DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 4, /* maxVolume= */ 10)) + .setDiscontinuityReason(Player.DISCONTINUITY_REASON_REMOVE) + .setIsLoading(true) + .setIsPlaying(true) + .setMaxSeekToPreviousPositionMs(5000) + .setMediaItemTransitionReason(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) + .setPlaybackParameters(new PlaybackParameters(2f)) + .setPlaybackState(Player.STATE_BUFFERING) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .setPlayerError( + new PlaybackException( + /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_TIMEOUT)) + .setPlayWhenReady(true) + .setPlayWhenReadyChangedReason(Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .setSeekBackIncrement(7000) + .setSeekForwardIncrement(6000) + .setShuffleModeEnabled(true) + .setTrackSelectionParameters( + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxAudioBitrate(5000) + .build()) + .setVideoSize(new VideoSize(/* width= */ 1024, /* height= */ 768)) + .build(); + + PlayerInfo infoAfterBundling = + PlayerInfo.CREATOR.fromBundle( + playerInfo.toBundle( + new Player.Commands.Builder().addAllCommands().build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false)); + + assertThat(infoAfterBundling.oldPositionInfo.mediaItemIndex).isEqualTo(5); + assertThat(infoAfterBundling.oldPositionInfo.periodIndex).isEqualTo(4); + assertThat(infoAfterBundling.oldPositionInfo.mediaItem.mediaId).isEqualTo("id1"); + assertThat(infoAfterBundling.oldPositionInfo.positionMs).isEqualTo(4000); + assertThat(infoAfterBundling.oldPositionInfo.contentPositionMs).isEqualTo(5000); + assertThat(infoAfterBundling.oldPositionInfo.adGroupIndex).isEqualTo(3); + assertThat(infoAfterBundling.oldPositionInfo.adIndexInAdGroup).isEqualTo(2); + assertThat(infoAfterBundling.newPositionInfo.mediaItemIndex).isEqualTo(6); + assertThat(infoAfterBundling.newPositionInfo.periodIndex).isEqualTo(7); + assertThat(infoAfterBundling.newPositionInfo.mediaItem.mediaId).isEqualTo("id2"); + assertThat(infoAfterBundling.newPositionInfo.positionMs).isEqualTo(8000); + assertThat(infoAfterBundling.newPositionInfo.contentPositionMs).isEqualTo(9000); + assertThat(infoAfterBundling.newPositionInfo.adGroupIndex).isEqualTo(5); + assertThat(infoAfterBundling.newPositionInfo.adIndexInAdGroup).isEqualTo(1); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.mediaItemIndex).isEqualTo(8); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.periodIndex).isEqualTo(9); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.mediaItem.mediaId) + .isEqualTo("id3"); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.positionMs).isEqualTo(2000); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.contentPositionMs) + .isEqualTo(7000); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.adGroupIndex).isEqualTo(9); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.adIndexInAdGroup).isEqualTo(1); + assertThat(infoAfterBundling.sessionPositionInfo.isPlayingAd).isTrue(); + assertThat(infoAfterBundling.sessionPositionInfo.eventTimeMs).isEqualTo(123456789); + assertThat(infoAfterBundling.sessionPositionInfo.durationMs).isEqualTo(30000); + assertThat(infoAfterBundling.sessionPositionInfo.bufferedPositionMs).isEqualTo(20000); + assertThat(infoAfterBundling.sessionPositionInfo.bufferedPercentage).isEqualTo(50); + assertThat(infoAfterBundling.sessionPositionInfo.totalBufferedDurationMs).isEqualTo(25000); + assertThat(infoAfterBundling.sessionPositionInfo.currentLiveOffsetMs).isEqualTo(3000); + assertThat(infoAfterBundling.sessionPositionInfo.contentDurationMs).isEqualTo(27000); + assertThat(infoAfterBundling.sessionPositionInfo.contentBufferedPositionMs).isEqualTo(15000); + assertThat(infoAfterBundling.timeline.getWindowCount()).isEqualTo(10); + assertThat(infoAfterBundling.mediaMetadata.title).isEqualTo("title"); + assertThat(infoAfterBundling.playlistMetadata.artist).isEqualTo("artist"); + assertThat(infoAfterBundling.volume).isEqualTo(0.5f); + assertThat(infoAfterBundling.deviceVolume).isEqualTo(10); + assertThat(infoAfterBundling.deviceMuted).isTrue(); + assertThat(infoAfterBundling.audioAttributes.contentType) + .isEqualTo(C.AUDIO_CONTENT_TYPE_SPEECH); + assertThat(infoAfterBundling.cueGroup.presentationTimeUs).isEqualTo(1234); + assertThat(infoAfterBundling.currentTracks.getGroups()).hasSize(1); + assertThat(infoAfterBundling.deviceInfo.maxVolume).isEqualTo(10); + assertThat(infoAfterBundling.discontinuityReason).isEqualTo(Player.DISCONTINUITY_REASON_REMOVE); + assertThat(infoAfterBundling.isLoading).isTrue(); + assertThat(infoAfterBundling.isPlaying).isTrue(); + assertThat(infoAfterBundling.maxSeekToPreviousPositionMs).isEqualTo(5000); + assertThat(infoAfterBundling.mediaItemTransitionReason) + .isEqualTo(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + assertThat(infoAfterBundling.playbackParameters.speed).isEqualTo(2f); + assertThat(infoAfterBundling.playbackState).isEqualTo(Player.STATE_BUFFERING); + assertThat(infoAfterBundling.playbackSuppressionReason) + .isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + assertThat(infoAfterBundling.playerError.errorCode) + .isEqualTo(PlaybackException.ERROR_CODE_TIMEOUT); + assertThat(infoAfterBundling.playWhenReady).isTrue(); + assertThat(infoAfterBundling.playWhenReadyChangedReason) + .isEqualTo(Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + assertThat(infoAfterBundling.repeatMode).isEqualTo(Player.REPEAT_MODE_ONE); + assertThat(infoAfterBundling.seekBackIncrementMs).isEqualTo(7000); + assertThat(infoAfterBundling.seekForwardIncrementMs).isEqualTo(6000); + assertThat(infoAfterBundling.shuffleModeEnabled).isTrue(); + assertThat(infoAfterBundling.trackSelectionParameters.maxAudioBitrate).isEqualTo(5000); + assertThat(infoAfterBundling.videoSize.width).isEqualTo(1024); + } + + @Test + public void toBundleFromBundle_withoutCommandGetCurrentMediaItem_filtersInformation() { + PlayerInfo playerInfo = + new PlayerInfo.Builder(PlayerInfo.DEFAULT) + .setOldPositionInfo( + new Player.PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 5, + /* mediaItem= */ new MediaItem.Builder().setMediaId("id1").build(), + /* periodUid= */ null, + /* periodIndex= */ 4, + /* positionMs= */ 4000, + /* contentPositionMs= */ 5000, + /* adGroupIndex= */ 3, + /* adIndexInAdGroup= */ 2)) + .setNewPositionInfo( + new Player.PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 6, + /* mediaItem= */ new MediaItem.Builder().setMediaId("id2").build(), + /* periodUid= */ null, + /* periodIndex= */ 7, + /* positionMs= */ 8000, + /* contentPositionMs= */ 9000, + /* adGroupIndex= */ 5, + /* adIndexInAdGroup= */ 1)) + .setSessionPositionInfo( + new SessionPositionInfo( + new Player.PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 8, + /* mediaItem= */ new MediaItem.Builder().setMediaId("id3").build(), + /* periodUid= */ null, + /* periodIndex= */ 9, + /* positionMs= */ 2000, + /* contentPositionMs= */ 7000, + /* adGroupIndex= */ 9, + /* adIndexInAdGroup= */ 1), + /* isPlayingAd= */ true, + /* eventTimeMs= */ 123456789, + /* durationMs= */ 30000, + /* bufferedPositionMs= */ 20000, + /* bufferedPercentage= */ 50, + /* totalBufferedDurationMs= */ 25000, + /* currentLiveOffsetMs= */ 3000, + /* contentDurationMs= */ 25000, + /* contentBufferedPositionMs= */ 15000)) + .build(); + + PlayerInfo infoAfterBundling = + PlayerInfo.CREATOR.fromBundle( + playerInfo.toBundle( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false)); + + assertThat(infoAfterBundling.oldPositionInfo.mediaItemIndex).isEqualTo(5); + assertThat(infoAfterBundling.oldPositionInfo.periodIndex).isEqualTo(4); + assertThat(infoAfterBundling.oldPositionInfo.mediaItem).isEqualTo(null); + assertThat(infoAfterBundling.oldPositionInfo.positionMs).isEqualTo(0); + assertThat(infoAfterBundling.oldPositionInfo.contentPositionMs).isEqualTo(0); + assertThat(infoAfterBundling.oldPositionInfo.adGroupIndex).isEqualTo(C.INDEX_UNSET); + assertThat(infoAfterBundling.oldPositionInfo.adIndexInAdGroup).isEqualTo(C.INDEX_UNSET); + assertThat(infoAfterBundling.newPositionInfo.mediaItemIndex).isEqualTo(6); + assertThat(infoAfterBundling.newPositionInfo.periodIndex).isEqualTo(7); + assertThat(infoAfterBundling.newPositionInfo.mediaItem).isEqualTo(null); + assertThat(infoAfterBundling.newPositionInfo.positionMs).isEqualTo(0); + assertThat(infoAfterBundling.newPositionInfo.contentPositionMs).isEqualTo(0); + assertThat(infoAfterBundling.newPositionInfo.adGroupIndex).isEqualTo(C.INDEX_UNSET); + assertThat(infoAfterBundling.newPositionInfo.adIndexInAdGroup).isEqualTo(C.INDEX_UNSET); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.mediaItemIndex).isEqualTo(8); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.periodIndex).isEqualTo(9); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.mediaItem).isEqualTo(null); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.positionMs).isEqualTo(0); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.contentPositionMs).isEqualTo(0); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.adGroupIndex) + .isEqualTo(C.INDEX_UNSET); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.adIndexInAdGroup) + .isEqualTo(C.INDEX_UNSET); + assertThat(infoAfterBundling.sessionPositionInfo.isPlayingAd).isFalse(); + assertThat(infoAfterBundling.sessionPositionInfo.eventTimeMs).isEqualTo(123456789); + assertThat(infoAfterBundling.sessionPositionInfo.durationMs).isEqualTo(C.TIME_UNSET); + assertThat(infoAfterBundling.sessionPositionInfo.bufferedPositionMs).isEqualTo(0); + assertThat(infoAfterBundling.sessionPositionInfo.bufferedPercentage).isEqualTo(0); + assertThat(infoAfterBundling.sessionPositionInfo.totalBufferedDurationMs).isEqualTo(0); + assertThat(infoAfterBundling.sessionPositionInfo.currentLiveOffsetMs).isEqualTo(C.TIME_UNSET); + assertThat(infoAfterBundling.sessionPositionInfo.contentDurationMs).isEqualTo(C.TIME_UNSET); + assertThat(infoAfterBundling.sessionPositionInfo.contentBufferedPositionMs).isEqualTo(0); + } + + @Test + public void toBundleFromBundle_withoutCommandGetTimeline_filtersInformation() { + PlayerInfo playerInfo = + new PlayerInfo.Builder(PlayerInfo.DEFAULT) + .setOldPositionInfo( + new Player.PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 5, + /* mediaItem= */ new MediaItem.Builder().setMediaId("id1").build(), + /* periodUid= */ null, + /* periodIndex= */ 4, + /* positionMs= */ 4000, + /* contentPositionMs= */ 5000, + /* adGroupIndex= */ 3, + /* adIndexInAdGroup= */ 2)) + .setNewPositionInfo( + new Player.PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 6, + /* mediaItem= */ new MediaItem.Builder().setMediaId("id2").build(), + /* periodUid= */ null, + /* periodIndex= */ 7, + /* positionMs= */ 8000, + /* contentPositionMs= */ 9000, + /* adGroupIndex= */ 5, + /* adIndexInAdGroup= */ 1)) + .setSessionPositionInfo( + new SessionPositionInfo( + new Player.PositionInfo( + /* windowUid= */ null, + /* mediaItemIndex= */ 8, + /* mediaItem= */ new MediaItem.Builder().setMediaId("id3").build(), + /* periodUid= */ null, + /* periodIndex= */ 9, + /* positionMs= */ 2000, + /* contentPositionMs= */ 7000, + /* adGroupIndex= */ 9, + /* adIndexInAdGroup= */ 1), + /* isPlayingAd= */ true, + /* eventTimeMs= */ 123456789, + /* durationMs= */ 30000, + /* bufferedPositionMs= */ 20000, + /* bufferedPercentage= */ 50, + /* totalBufferedDurationMs= */ 25000, + /* currentLiveOffsetMs= */ 3000, + /* contentDurationMs= */ 27000, + /* contentBufferedPositionMs= */ 15000)) + .setTimeline(new FakeTimeline(/* windowCount= */ 10)) + .build(); + + PlayerInfo infoAfterBundling = + PlayerInfo.CREATOR.fromBundle( + playerInfo.toBundle( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TIMELINE) + .build(), + /* excludeTimeline= */ true, + /* excludeTracks= */ false)); + + assertThat(infoAfterBundling.oldPositionInfo.mediaItemIndex).isEqualTo(0); + assertThat(infoAfterBundling.oldPositionInfo.periodIndex).isEqualTo(0); + assertThat(infoAfterBundling.oldPositionInfo.mediaItem.mediaId).isEqualTo("id1"); + assertThat(infoAfterBundling.oldPositionInfo.positionMs).isEqualTo(4000); + assertThat(infoAfterBundling.oldPositionInfo.contentPositionMs).isEqualTo(5000); + assertThat(infoAfterBundling.oldPositionInfo.adGroupIndex).isEqualTo(3); + assertThat(infoAfterBundling.oldPositionInfo.adIndexInAdGroup).isEqualTo(2); + assertThat(infoAfterBundling.newPositionInfo.mediaItemIndex).isEqualTo(0); + assertThat(infoAfterBundling.newPositionInfo.periodIndex).isEqualTo(0); + assertThat(infoAfterBundling.newPositionInfo.mediaItem.mediaId).isEqualTo("id2"); + assertThat(infoAfterBundling.newPositionInfo.positionMs).isEqualTo(8000); + assertThat(infoAfterBundling.newPositionInfo.contentPositionMs).isEqualTo(9000); + assertThat(infoAfterBundling.newPositionInfo.adGroupIndex).isEqualTo(5); + assertThat(infoAfterBundling.newPositionInfo.adIndexInAdGroup).isEqualTo(1); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.mediaItemIndex).isEqualTo(0); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.periodIndex).isEqualTo(0); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.mediaItem.mediaId) + .isEqualTo("id3"); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.positionMs).isEqualTo(2000); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.contentPositionMs) + .isEqualTo(7000); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.adGroupIndex).isEqualTo(9); + assertThat(infoAfterBundling.sessionPositionInfo.positionInfo.adIndexInAdGroup).isEqualTo(1); + assertThat(infoAfterBundling.sessionPositionInfo.isPlayingAd).isTrue(); + assertThat(infoAfterBundling.sessionPositionInfo.eventTimeMs).isEqualTo(123456789); + assertThat(infoAfterBundling.sessionPositionInfo.durationMs).isEqualTo(30000); + assertThat(infoAfterBundling.sessionPositionInfo.bufferedPositionMs).isEqualTo(20000); + assertThat(infoAfterBundling.sessionPositionInfo.bufferedPercentage).isEqualTo(50); + assertThat(infoAfterBundling.sessionPositionInfo.totalBufferedDurationMs).isEqualTo(25000); + assertThat(infoAfterBundling.sessionPositionInfo.currentLiveOffsetMs).isEqualTo(3000); + assertThat(infoAfterBundling.sessionPositionInfo.contentDurationMs).isEqualTo(27000); + assertThat(infoAfterBundling.sessionPositionInfo.contentBufferedPositionMs).isEqualTo(15000); + assertThat(infoAfterBundling.timeline).isEqualTo(Timeline.EMPTY); + } + + @Test + public void toBundleFromBundle_withoutCommandGetMediaItemsMetadata_filtersInformation() { + PlayerInfo playerInfo = + new PlayerInfo.Builder(PlayerInfo.DEFAULT) + .setMediaMetadata(new MediaMetadata.Builder().setTitle("title").build()) + .setPlaylistMetadata(new MediaMetadata.Builder().setArtist("artist").build()) + .build(); + + PlayerInfo infoAfterBundling = + PlayerInfo.CREATOR.fromBundle( + playerInfo.toBundle( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_MEDIA_ITEMS_METADATA) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false)); + + assertThat(infoAfterBundling.mediaMetadata).isEqualTo(MediaMetadata.EMPTY); + assertThat(infoAfterBundling.playlistMetadata).isEqualTo(MediaMetadata.EMPTY); + } + + @Test + public void toBundleFromBundle_withoutCommandGetVolume_filtersInformation() { + PlayerInfo playerInfo = new PlayerInfo.Builder(PlayerInfo.DEFAULT).setVolume(0.5f).build(); + + PlayerInfo infoAfterBundling = + PlayerInfo.CREATOR.fromBundle( + playerInfo.toBundle( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_VOLUME) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false)); + + assertThat(infoAfterBundling.volume).isEqualTo(1f); + } + + @Test + public void toBundleFromBundle_withoutCommandGetDeviceVolume_filtersInformation() { + PlayerInfo playerInfo = + new PlayerInfo.Builder(PlayerInfo.DEFAULT).setDeviceVolume(10).setDeviceMuted(true).build(); + + PlayerInfo infoAfterBundling = + PlayerInfo.CREATOR.fromBundle( + playerInfo.toBundle( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_DEVICE_VOLUME) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false)); + + assertThat(infoAfterBundling.deviceVolume).isEqualTo(0); + assertThat(infoAfterBundling.deviceMuted).isFalse(); + } + + @Test + public void toBundleFromBundle_withoutCommandGetAudioAttributes_filtersInformation() { + PlayerInfo playerInfo = + new PlayerInfo.Builder(PlayerInfo.DEFAULT) + .setAudioAttributes( + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_SPEECH).build()) + .build(); + + PlayerInfo infoAfterBundling = + PlayerInfo.CREATOR.fromBundle( + playerInfo.toBundle( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_AUDIO_ATTRIBUTES) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false)); + + assertThat(infoAfterBundling.audioAttributes).isEqualTo(AudioAttributes.DEFAULT); + } + + @Test + public void toBundleFromBundle_withoutCommandGetText_filtersInformation() { + PlayerInfo playerInfo = + new PlayerInfo.Builder(PlayerInfo.DEFAULT) + .setCues(new CueGroup(/* cues= */ ImmutableList.of(), /* presentationTimeUs= */ 1234)) + .build(); + + PlayerInfo infoAfterBundling = + PlayerInfo.CREATOR.fromBundle( + playerInfo.toBundle( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TEXT) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ false)); + + assertThat(infoAfterBundling.cueGroup).isEqualTo(CueGroup.EMPTY_TIME_ZERO); + } + + @Test + public void toBundleFromBundle_withoutCommandGetTracks_filtersInformation() { + PlayerInfo playerInfo = + new PlayerInfo.Builder(PlayerInfo.DEFAULT) + .setCurrentTracks( + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup( + new Format.Builder().setSampleMimeType(AUDIO_AAC).build()), + /* adaptiveSupported= */ false, + new int[] {C.FORMAT_EXCEEDS_CAPABILITIES}, + /* trackSelected= */ new boolean[] {true})))) + .build(); + + PlayerInfo infoAfterBundling = + PlayerInfo.CREATOR.fromBundle( + playerInfo.toBundle( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TRACKS) + .build(), + /* excludeTimeline= */ false, + /* excludeTracks= */ true)); + + assertThat(infoAfterBundling.currentTracks).isEqualTo(Tracks.EMPTY); + } } From 5b18c2d89f9ec7814747815b9df81a11c6a3eacf Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 19 Jan 2023 09:50:52 +0000 Subject: [PATCH 115/141] Extend command GET_CURRENT_MEDIA_ITEM to more methods. We currently only document it for the getCurrentMediaItem(), but the command was always meant to cover all information about the current media item and the position therein. To correctly hide information for controllers, we need to filter the Timeline when bundling the PlayerInfo class if only this command is available. PiperOrigin-RevId: 503098124 (cherry picked from commit f15b7525436b45694b5e1971dac922adff48b5ae) --- .../java/androidx/media3/common/Player.java | 70 ++++- .../java/androidx/media3/common/Timeline.java | 33 +++ .../media3/session/MediaSessionImpl.java | 2 - .../media3/session/MediaSessionStub.java | 83 ++++-- .../androidx/media3/session/PlayerInfo.java | 4 + .../media3/session/PlayerWrapper.java | 79 +++++- .../media3/session/PlayerInfoTest.java | 43 ++- .../session/MediaControllerListenerTest.java | 37 +-- .../session/MediaSessionPlayerTest.java | 263 ++++++++++++++++++ .../androidx/media3/session/MockPlayer.java | 12 +- 10 files changed, 564 insertions(+), 62 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index 17183a7f963..9015699e320 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -1625,10 +1625,28 @@ default void onMetadata(Metadata metadata) {} int COMMAND_SET_REPEAT_MODE = 15; /** - * Command to get the currently playing {@link MediaItem}. + * Command to get information about the currently playing {@link MediaItem}. * - *

        The {@link #getCurrentMediaItem()} method must only be called if this command is {@linkplain - * #isCommandAvailable(int) available}. + *

        The following methods must only be called if this command is {@linkplain + * #isCommandAvailable(int) available}: + * + *

          + *
        • {@link #getCurrentMediaItem()} + *
        • {@link #isCurrentMediaItemDynamic()} + *
        • {@link #isCurrentMediaItemLive()} + *
        • {@link #isCurrentMediaItemSeekable()} + *
        • {@link #getCurrentLiveOffset()} + *
        • {@link #getDuration()} + *
        • {@link #getCurrentPosition()} + *
        • {@link #getBufferedPosition()} + *
        • {@link #getContentDuration()} + *
        • {@link #getContentPosition()} + *
        • {@link #getContentBufferedPosition()} + *
        • {@link #getTotalBufferedDuration()} + *
        • {@link #isPlayingAd()} + *
        • {@link #getCurrentAdGroupIndex()} + *
        • {@link #getCurrentAdIndexInAdGroup()} + *
        */ int COMMAND_GET_CURRENT_MEDIA_ITEM = 16; @@ -1648,8 +1666,6 @@ default void onMetadata(Metadata metadata) {} *
      • {@link #getPreviousMediaItemIndex()} *
      • {@link #hasPreviousMediaItem()} *
      • {@link #hasNextMediaItem()} - *
      • {@link #getCurrentAdGroupIndex()} - *
      • {@link #getCurrentAdIndexInAdGroup()} *
      */ int COMMAND_GET_TIMELINE = 17; @@ -2692,18 +2708,27 @@ default void onMetadata(Metadata metadata) {} /** * Returns the duration of the current content or ad in milliseconds, or {@link C#TIME_UNSET} if * the duration is not known. + * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. */ long getDuration(); /** * Returns the playback position in the current content or ad, in milliseconds, or the prospective * position in milliseconds if the {@link #getCurrentTimeline() current timeline} is empty. + * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. */ long getCurrentPosition(); /** * Returns an estimate of the position in the current content or ad up to which data is buffered, * in milliseconds. + * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. */ long getBufferedPosition(); @@ -2717,6 +2742,9 @@ default void onMetadata(Metadata metadata) {} /** * Returns an estimate of the total buffered duration from the current position, in milliseconds. * This includes pre-buffered data for subsequent ads and {@linkplain MediaItem media items}. + * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. */ long getTotalBufferedDuration(); @@ -2731,6 +2759,9 @@ default void onMetadata(Metadata metadata) {} * Returns whether the current {@link MediaItem} is dynamic (may change when the {@link Timeline} * is updated), or {@code false} if the {@link Timeline} is empty. * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. + * * @see Timeline.Window#isDynamic */ boolean isCurrentMediaItemDynamic(); @@ -2746,6 +2777,9 @@ default void onMetadata(Metadata metadata) {} * Returns whether the current {@link MediaItem} is live, or {@code false} if the {@link Timeline} * is empty. * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. + * * @see Timeline.Window#isLive() */ boolean isCurrentMediaItemLive(); @@ -2760,6 +2794,9 @@ default void onMetadata(Metadata metadata) {} * *

      Note that this offset may rely on an accurate local time, so this method may return an * incorrect value if the difference between system clock and server clock is unknown. + * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. */ long getCurrentLiveOffset(); @@ -2774,18 +2811,26 @@ default void onMetadata(Metadata metadata) {} * Returns whether the current {@link MediaItem} is seekable, or {@code false} if the {@link * Timeline} is empty. * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. + * * @see Timeline.Window#isSeekable */ boolean isCurrentMediaItemSeekable(); - /** Returns whether the player is currently playing an ad. */ + /** + * Returns whether the player is currently playing an ad. + * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. + */ boolean isPlayingAd(); /** * If {@link #isPlayingAd()} returns true, returns the index of the ad group in the period * currently being played. Returns {@link C#INDEX_UNSET} otherwise. * - *

      This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain * #getAvailableCommands() available}. */ int getCurrentAdGroupIndex(); @@ -2794,7 +2839,7 @@ default void onMetadata(Metadata metadata) {} * If {@link #isPlayingAd()} returns true, returns the index of the ad in its ad group. Returns * {@link C#INDEX_UNSET} otherwise. * - *

      This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain * #getAvailableCommands() available}. */ int getCurrentAdIndexInAdGroup(); @@ -2803,6 +2848,9 @@ default void onMetadata(Metadata metadata) {} * If {@link #isPlayingAd()} returns {@code true}, returns the duration of the current content in * milliseconds, or {@link C#TIME_UNSET} if the duration is not known. If there is no ad playing, * the returned duration is the same as that returned by {@link #getDuration()}. + * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. */ long getContentDuration(); @@ -2810,6 +2858,9 @@ default void onMetadata(Metadata metadata) {} * If {@link #isPlayingAd()} returns {@code true}, returns the content position that will be * played once all ads in the ad group have finished playing, in milliseconds. If there is no ad * playing, the returned position is the same as that returned by {@link #getCurrentPosition()}. + * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. */ long getContentPosition(); @@ -2817,6 +2868,9 @@ default void onMetadata(Metadata metadata) {} * If {@link #isPlayingAd()} returns {@code true}, returns an estimate of the content position in * the current content up to which data is buffered, in milliseconds. If there is no ad playing, * the returned position is the same as that returned by {@link #getBufferedPosition()}. + * + *

      This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain + * #getAvailableCommands() available}. */ long getContentBufferedPosition(); diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java index 1d7706f907c..d470b37b52f 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java +++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java @@ -1416,6 +1416,39 @@ public final Bundle toBundle() { return bundle; } + /** + * Returns a {@link Bundle} containing just the specified {@link Window}. + * + *

      The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of + * an instance restored by {@link #CREATOR} may have missing fields as described in {@link + * Window#toBundle()} and {@link Period#toBundle()}. + * + * @param windowIndex The index of the {@link Window} to include in the {@link Bundle}. + */ + @UnstableApi + public final Bundle toBundleWithOneWindowOnly(int windowIndex) { + Window window = getWindow(windowIndex, new Window(), /* defaultPositionProjectionUs= */ 0); + + List periodBundles = new ArrayList<>(); + Period period = new Period(); + for (int i = window.firstPeriodIndex; i <= window.lastPeriodIndex; i++) { + getPeriod(i, period, /* setIds= */ false); + period.windowIndex = 0; + periodBundles.add(period.toBundle()); + } + + window.lastPeriodIndex = window.lastPeriodIndex - window.firstPeriodIndex; + window.firstPeriodIndex = 0; + Bundle windowBundle = window.toBundle(); + + Bundle bundle = new Bundle(); + BundleUtil.putBinder( + bundle, FIELD_WINDOWS, new BundleListRetriever(ImmutableList.of(windowBundle))); + BundleUtil.putBinder(bundle, FIELD_PERIODS, new BundleListRetriever(periodBundles)); + bundle.putIntArray(FIELD_SHUFFLED_WINDOW_INDICES, new int[] {0}); + return bundle; + } + /** * Object that can restore a {@link Timeline} from a {@link Bundle}. * diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 9e7f201d79c..180030adf14 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -810,8 +810,6 @@ public void onMediaItemTransition( if (player == null) { return; } - // Note: OK to omit mediaItem here, because PlayerInfo changed message will copy playerInfo - // with sessionPositionInfo, which includes current window index. session.playerInfo = session.playerInfo.copyWithMediaItemTransitionReason(reason); session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( /* excludeTimeline= */ true, /* excludeTracks= */ true); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 3a36b803686..6ae74ccbbfc 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -136,11 +136,16 @@ private static void sendSessionResult( private static SessionTask, K> sendSessionResultSuccess( Consumer task) { + return sendSessionResultSuccess((player, controller) -> task.accept(player)); + } + + private static + SessionTask, K> sendSessionResultSuccess(ControllerPlayerTask task) { return (sessionImpl, controller, sequenceNumber) -> { if (sessionImpl.isReleased()) { return Futures.immediateVoidFuture(); } - task.accept(sessionImpl.getPlayerWrapper()); + task.run(sessionImpl.getPlayerWrapper(), controller); sendSessionResult( controller, sequenceNumber, new SessionResult(SessionResult.RESULT_SUCCESS)); return Futures.immediateVoidFuture(); @@ -189,7 +194,8 @@ SessionTask, K> handleMediaItemsWhenReady( sessionImpl.getApplicationHandler(), () -> { if (!sessionImpl.isReleased()) { - mediaItemPlayerTask.run(sessionImpl.getPlayerWrapper(), mediaItems); + mediaItemPlayerTask.run( + sessionImpl.getPlayerWrapper(), controller, mediaItems); } }, new SessionResult(SessionResult.RESULT_SUCCESS))); @@ -370,6 +376,20 @@ private static ListenableFuture handleSess return outputFuture; } + private int maybeCorrectMediaItemIndex( + ControllerInfo controllerInfo, PlayerWrapper player, int mediaItemIndex) { + if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE) + && !connectedControllersManager.isPlayerCommandAvailable( + controllerInfo, Player.COMMAND_GET_TIMELINE) + && connectedControllersManager.isPlayerCommandAvailable( + controllerInfo, Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) { + // COMMAND_GET_TIMELINE was filtered out for this controller, so all indices are relative to + // the current one. + return mediaItemIndex + player.getCurrentMediaItemIndex(); + } + return mediaItemIndex; + } + public void connect( IMediaController caller, int controllerVersion, @@ -555,7 +575,7 @@ public void stop(@Nullable IMediaController caller, int sequenceNumber) throws R return; } queueSessionTaskWithPlayerCommand( - caller, sequenceNumber, COMMAND_STOP, sendSessionResultSuccess(Player::stop)); + caller, sequenceNumber, COMMAND_STOP, sendSessionResultSuccess(player -> player.stop())); } @Override @@ -655,7 +675,7 @@ public void seekToDefaultPosition(IMediaController caller, int sequenceNumber) { caller, sequenceNumber, COMMAND_SEEK_TO_DEFAULT_POSITION, - sendSessionResultSuccess(Player::seekToDefaultPosition)); + sendSessionResultSuccess(player -> player.seekToDefaultPosition())); } @Override @@ -668,7 +688,10 @@ public void seekToDefaultPositionWithMediaItemIndex( caller, sequenceNumber, COMMAND_SEEK_TO_MEDIA_ITEM, - sendSessionResultSuccess(player -> player.seekToDefaultPosition(mediaItemIndex))); + sendSessionResultSuccess( + (player, controller) -> + player.seekToDefaultPosition( + maybeCorrectMediaItemIndex(controller, player, mediaItemIndex)))); } @Override @@ -695,7 +718,10 @@ public void seekToWithMediaItemIndex( caller, sequenceNumber, COMMAND_SEEK_TO_MEDIA_ITEM, - sendSessionResultSuccess(player -> player.seekTo(mediaItemIndex, positionMs))); + sendSessionResultSuccess( + (player, controller) -> + player.seekTo( + maybeCorrectMediaItemIndex(controller, player, mediaItemIndex), positionMs))); } @Override @@ -843,7 +869,8 @@ public void setMediaItem( handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), - Player::setMediaItems))); + (playerWrapper, controller, mediaItems) -> + playerWrapper.setMediaItems(mediaItems)))); } @Override @@ -870,7 +897,7 @@ public void setMediaItemWithStartPosition( handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), - (player, mediaItems) -> + (player, controller, mediaItems) -> player.setMediaItems(mediaItems, /* startIndex= */ 0, startPositionMs)))); } @@ -898,7 +925,8 @@ public void setMediaItemWithResetPosition( handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), - (player, mediaItems) -> player.setMediaItems(mediaItems, resetPosition)))); + (player, controller, mediaItems) -> + player.setMediaItems(mediaItems, resetPosition)))); } @Override @@ -927,7 +955,8 @@ public void setMediaItems( handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList), - Player::setMediaItems))); + (playerWrapper, controller, mediaItems) -> + playerWrapper.setMediaItems(mediaItems)))); } @Override @@ -956,7 +985,8 @@ public void setMediaItemsWithResetPosition( handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList), - (player, mediaItems) -> player.setMediaItems(mediaItems, resetPosition)))); + (player, controller, mediaItems) -> + player.setMediaItems(mediaItems, resetPosition)))); } @Override @@ -986,7 +1016,7 @@ public void setMediaItemsWithStartIndex( handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList), - (player, mediaItems) -> + (player, controller, mediaItems) -> player.setMediaItems(mediaItems, startIndex, startPositionMs)))); } @@ -1033,7 +1063,8 @@ public void addMediaItem( handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), - Player::addMediaItems))); + (playerWrapper, controller, mediaItems) -> + playerWrapper.addMediaItems(mediaItems)))); } @Override @@ -1057,7 +1088,9 @@ public void addMediaItemWithIndex( handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), - (player, mediaItems) -> player.addMediaItems(index, mediaItems)))); + (player, controller, mediaItems) -> + player.addMediaItems( + maybeCorrectMediaItemIndex(controller, player, index), mediaItems)))); } @Override @@ -1085,7 +1118,7 @@ public void addMediaItems( handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, mediaItems), - Player::addMediaItems))); + (playerWrapper, controller, items) -> playerWrapper.addMediaItems(items)))); } @Override @@ -1114,7 +1147,9 @@ public void addMediaItemsWithIndex( handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, mediaItems), - (player, items) -> player.addMediaItems(index, items)))); + (player, controller, items) -> + player.addMediaItems( + maybeCorrectMediaItemIndex(controller, player, index), items)))); } @Override @@ -1126,7 +1161,9 @@ public void removeMediaItem(@Nullable IMediaController caller, int sequenceNumbe caller, sequenceNumber, COMMAND_CHANGE_MEDIA_ITEMS, - sendSessionResultSuccess(player -> player.removeMediaItem(index))); + sendSessionResultSuccess( + (player, controller) -> + player.removeMediaItem(maybeCorrectMediaItemIndex(controller, player, index)))); } @Override @@ -1139,7 +1176,11 @@ public void removeMediaItems( caller, sequenceNumber, COMMAND_CHANGE_MEDIA_ITEMS, - sendSessionResultSuccess(player -> player.removeMediaItems(fromIndex, toIndex))); + sendSessionResultSuccess( + (player, controller) -> + player.removeMediaItems( + maybeCorrectMediaItemIndex(controller, player, fromIndex), + maybeCorrectMediaItemIndex(controller, player, toIndex)))); } @Override @@ -1576,7 +1617,11 @@ private interface SessionTask { } private interface MediaItemPlayerTask { - void run(PlayerWrapper player, List mediaItems); + void run(PlayerWrapper player, ControllerInfo controller, List mediaItems); + } + + private interface ControllerPlayerTask { + void run(PlayerWrapper player, ControllerInfo controller); } /* package */ static final class Controller2Cb implements ControllerCb { diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java index 56207efa9d7..ddc212cd53a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java @@ -823,6 +823,10 @@ public Bundle toBundle( bundle.putBoolean(FIELD_SHUFFLE_MODE_ENABLED, shuffleModeEnabled); if (!excludeTimeline && canAccessTimeline) { bundle.putBundle(FIELD_TIMELINE, timeline.toBundle()); + } else if (!canAccessTimeline && canAccessCurrentMediaItem && !timeline.isEmpty()) { + bundle.putBundle( + FIELD_TIMELINE, + timeline.toBundleWithOneWindowOnly(sessionPositionInfo.positionInfo.mediaItemIndex)); } bundle.putBundle(FIELD_VIDEO_SIZE, videoSize.toBundle()); if (availableCommands.contains(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)) { diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index 42c391f9c4d..c4922f4b4fb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -17,6 +17,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.common.util.Util.postOrRun; import static androidx.media3.session.MediaConstants.EXTRAS_KEY_MEDIA_ID_COMPAT; import static androidx.media3.session.MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT; @@ -588,7 +589,12 @@ public Timeline getCurrentTimeline() { } public Timeline getCurrentTimelineWithCommandCheck() { - return isCommandAvailable(COMMAND_GET_TIMELINE) ? getCurrentTimeline() : Timeline.EMPTY; + if (isCommandAvailable(COMMAND_GET_TIMELINE)) { + return getCurrentTimeline(); + } else if (isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) { + return new CurrentMediaItemOnlyTimeline(this); + } + return Timeline.EMPTY; } @Override @@ -1165,4 +1171,75 @@ private static long convertCommandToPlaybackStateActions(@Command int command) { return 0; } } + + private static final class CurrentMediaItemOnlyTimeline extends Timeline { + + private static final Object UID = new Object(); + + @Nullable private final MediaItem mediaItem; + private final boolean isSeekable; + private final boolean isDynamic; + @Nullable private final MediaItem.LiveConfiguration liveConfiguration; + private final long durationUs; + + public CurrentMediaItemOnlyTimeline(PlayerWrapper player) { + mediaItem = player.getCurrentMediaItem(); + isSeekable = player.isCurrentMediaItemSeekable(); + isDynamic = player.isCurrentMediaItemDynamic(); + liveConfiguration = + player.isCurrentMediaItemLive() ? MediaItem.LiveConfiguration.UNSET : null; + durationUs = msToUs(player.getContentDuration()); + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + window.set( + UID, + mediaItem, + /* manifest= */ null, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, + isSeekable, + isDynamic, + liveConfiguration, + /* defaultPositionUs= */ 0, + durationUs, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ 0, + /* positionInFirstPeriodUs= */ 0); + return window; + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + period.set( + /* id= */ UID, + /* uid= */ UID, + /* windowIndex= */ 0, + durationUs, + /* positionInWindowUs= */ 0); + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return UID.equals(uid) ? 0 : C.INDEX_UNSET; + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + return UID; + } + } } diff --git a/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java b/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java index fdc4e2e4c01..32ea6e18a58 100644 --- a/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java @@ -376,7 +376,32 @@ public void toBundleFromBundle_withoutCommandGetTimeline_filtersInformation() { /* currentLiveOffsetMs= */ 3000, /* contentDurationMs= */ 27000, /* contentBufferedPositionMs= */ 15000)) - .setTimeline(new FakeTimeline(/* windowCount= */ 10)) + .setTimeline( + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000), + new FakeTimeline.TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000), + new FakeTimeline.TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000), + new FakeTimeline.TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000), + new FakeTimeline.TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000), + new FakeTimeline.TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000), + new FakeTimeline.TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000), + new FakeTimeline.TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000), + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ 2, + /* id= */ new Object(), + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationUs= */ 5000), + new FakeTimeline.TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000))) .build(); PlayerInfo infoAfterBundling = @@ -421,7 +446,21 @@ public void toBundleFromBundle_withoutCommandGetTimeline_filtersInformation() { assertThat(infoAfterBundling.sessionPositionInfo.currentLiveOffsetMs).isEqualTo(3000); assertThat(infoAfterBundling.sessionPositionInfo.contentDurationMs).isEqualTo(27000); assertThat(infoAfterBundling.sessionPositionInfo.contentBufferedPositionMs).isEqualTo(15000); - assertThat(infoAfterBundling.timeline).isEqualTo(Timeline.EMPTY); + assertThat(infoAfterBundling.timeline.getWindowCount()).isEqualTo(1); + Timeline.Window window = + infoAfterBundling.timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()); + assertThat(window.durationUs).isEqualTo(5000); + assertThat(window.firstPeriodIndex).isEqualTo(0); + assertThat(window.lastPeriodIndex).isEqualTo(1); + Timeline.Period period = + infoAfterBundling.timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period()); + assertThat(period.durationUs) + .isEqualTo( + 2500 + FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US); + assertThat(period.windowIndex).isEqualTo(0); + infoAfterBundling.timeline.getPeriod(/* periodIndex= */ 1, period); + assertThat(period.durationUs).isEqualTo(2500); + assertThat(period.windowIndex).isEqualTo(0); } @Test diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java index 13f7d64d4e7..0beb95bef44 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java @@ -2215,7 +2215,7 @@ public void onEvents(Player player, Player.Events events) { } @Test - public void onTimelineChanged_playerCommandUnavailable_emptyTimelineMediaItemAndMetadata() + public void onTimelineChanged_playerCommandUnavailable_reducesTimelineToOneItem() throws Exception { int testMediaItemsSize = 2; List testMediaItemList = MediaTestUtils.createMediaItems(testMediaItemsSize); @@ -2227,8 +2227,6 @@ public void onTimelineChanged_playerCommandUnavailable_emptyTimelineMediaItemAnd CountDownLatch latch = new CountDownLatch(3); AtomicReference timelineFromParamRef = new AtomicReference<>(); AtomicReference timelineFromGetterRef = new AtomicReference<>(); - List onEventsTimelines = new ArrayList<>(); - AtomicReference metadataFromGetterRef = new AtomicReference<>(); AtomicReference isCurrentMediaItemNullRef = new AtomicReference<>(); List eventsList = new ArrayList<>(); Player.Listener listener = @@ -2237,7 +2235,6 @@ public void onTimelineChanged_playerCommandUnavailable_emptyTimelineMediaItemAnd public void onTimelineChanged(Timeline timeline, int reason) { timelineFromParamRef.set(timeline); timelineFromGetterRef.set(controller.getCurrentTimeline()); - metadataFromGetterRef.set(controller.getMediaMetadata()); isCurrentMediaItemNullRef.set(controller.getCurrentMediaItem() == null); latch.countDown(); } @@ -2245,7 +2242,6 @@ public void onTimelineChanged(Timeline timeline, int reason) { @Override public void onEvents(Player player, Player.Events events) { // onEvents is called twice. - onEventsTimelines.add(player.getCurrentTimeline()); eventsList.add(events); latch.countDown(); } @@ -2256,27 +2252,17 @@ public void onEvents(Player player, Player.Events events) { remoteSession.getMockPlayer().notifyAvailableCommandsChanged(commandsWithoutGetTimeline); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(timelineFromParamRef.get()).isEqualTo(Timeline.EMPTY); - assertThat(onEventsTimelines).hasSize(2); - for (int i = 0; i < onEventsTimelines.get(1).getWindowCount(); i++) { - assertThat( - onEventsTimelines - .get(1) - .getWindow(/* windowIndex= */ i, new Timeline.Window()) - .mediaItem) - .isEqualTo(MediaItem.EMPTY); - } - assertThat(metadataFromGetterRef.get()).isEqualTo(MediaMetadata.EMPTY); - assertThat(isCurrentMediaItemNullRef.get()).isTrue(); + assertThat(timelineFromParamRef.get().getWindowCount()).isEqualTo(1); + assertThat(timelineFromGetterRef.get().getWindowCount()).isEqualTo(1); + assertThat(isCurrentMediaItemNullRef.get()).isFalse(); assertThat(eventsList).hasSize(2); assertThat(getEventsAsList(eventsList.get(0))) .containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED); - assertThat(getEventsAsList(eventsList.get(1))) - .containsExactly(Player.EVENT_TIMELINE_CHANGED, Player.EVENT_MEDIA_ITEM_TRANSITION); + assertThat(getEventsAsList(eventsList.get(1))).containsExactly(Player.EVENT_TIMELINE_CHANGED); } @Test - public void onTimelineChanged_sessionCommandUnavailable_emptyTimelineMediaItemAndMetadata() + public void onTimelineChanged_sessionCommandUnavailable_reducesTimelineToOneItem() throws Exception { int testMediaItemsSize = 2; List testMediaItemList = MediaTestUtils.createMediaItems(testMediaItemsSize); @@ -2288,7 +2274,6 @@ public void onTimelineChanged_sessionCommandUnavailable_emptyTimelineMediaItemAn CountDownLatch latch = new CountDownLatch(3); AtomicReference timelineFromParamRef = new AtomicReference<>(); AtomicReference timelineFromGetterRef = new AtomicReference<>(); - AtomicReference metadataFromGetterRef = new AtomicReference<>(); AtomicReference isCurrentMediaItemNullRef = new AtomicReference<>(); List eventsList = new ArrayList<>(); Player.Listener listener = @@ -2297,7 +2282,6 @@ public void onTimelineChanged_sessionCommandUnavailable_emptyTimelineMediaItemAn public void onTimelineChanged(Timeline timeline, int reason) { timelineFromParamRef.set(timeline); timelineFromGetterRef.set(controller.getCurrentTimeline()); - metadataFromGetterRef.set(controller.getMediaMetadata()); isCurrentMediaItemNullRef.set(controller.getCurrentMediaItem() == null); latch.countDown(); } @@ -2315,14 +2299,13 @@ public void onEvents(Player player, Player.Events events) { remoteSession.setAvailableCommands(SessionCommands.EMPTY, commandsWithoutGetTimeline); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(timelineFromParamRef.get()).isEqualTo(Timeline.EMPTY); - assertThat(metadataFromGetterRef.get()).isEqualTo(MediaMetadata.EMPTY); - assertThat(isCurrentMediaItemNullRef.get()).isTrue(); + assertThat(timelineFromParamRef.get().getWindowCount()).isEqualTo(1); + assertThat(timelineFromGetterRef.get().getWindowCount()).isEqualTo(1); + assertThat(isCurrentMediaItemNullRef.get()).isFalse(); assertThat(eventsList).hasSize(2); assertThat(getEventsAsList(eventsList.get(0))) .containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED); - assertThat(getEventsAsList(eventsList.get(1))) - .containsExactly(Player.EVENT_TIMELINE_CHANGED, Player.EVENT_MEDIA_ITEM_TRANSITION); + assertThat(getEventsAsList(eventsList.get(1))).containsExactly(Player.EVENT_TIMELINE_CHANGED); } /** This also tests {@link MediaController#getAvailableCommands()}. */ diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java index 076643c2a28..2098f6ca291 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java @@ -34,6 +34,8 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.util.List; import org.junit.After; @@ -164,6 +166,47 @@ public void seekToDefaultPosition_withMediaItemIndex() throws Exception { assertThat(player.seekMediaItemIndex).isEqualTo(mediaItemIndex); } + @Test + public void seekToDefaultPosition_withMediaItemIndexWithoutGetTimelineCommand() throws Exception { + MockPlayer player = + new MockPlayer.Builder() + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .setMediaItems(/* itemCount= */ 5) + .build(); + player.currentMediaItemIndex = 3; + MediaSession session = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) + .setCallback( + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + SessionCommands sessionCommands = + new SessionCommands.Builder().addAllSessionCommands().build(); + Player.Commands playerCommands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TIMELINE) + .build(); + return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands); + } + }) + .setId("seekToDefaultPosition_withMediaItemIndexWithoutGetTimelineCommand") + .build(); + RemoteMediaController controller = + remoteControllerTestRule.createRemoteController(session.getToken()); + + // The controller should only be able to see the current item without Timeline access. + controller.seekToDefaultPosition(/* mediaItemIndex= */ 0); + player.awaitMethodCalled( + MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS); + controller.release(); + session.release(); + player.release(); + + assertThat(player.seekMediaItemIndex).isEqualTo(3); + } + @Test public void seekTo() throws Exception { long seekPositionMs = 12125L; @@ -185,6 +228,47 @@ public void seekTo_withMediaItemIndex() throws Exception { assertThat(player.seekPositionMs).isEqualTo(seekPositionMs); } + @Test + public void seekTo_withMediaItemIndexWithoutGetTimelineCommand() throws Exception { + MockPlayer player = + new MockPlayer.Builder() + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .setMediaItems(/* itemCount= */ 5) + .build(); + player.currentMediaItemIndex = 3; + MediaSession session = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) + .setCallback( + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + SessionCommands sessionCommands = + new SessionCommands.Builder().addAllSessionCommands().build(); + Player.Commands playerCommands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TIMELINE) + .build(); + return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands); + } + }) + .setId("seekTo_withMediaItemIndexWithoutGetTimelineCommand") + .build(); + RemoteMediaController controller = + remoteControllerTestRule.createRemoteController(session.getToken()); + + // The controller should only be able to see the current item without Timeline access. + controller.seekTo(/* mediaItemIndex= */ 0, /* seekPositionMs= */ 2000); + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS); + controller.release(); + session.release(); + player.release(); + + assertThat(player.seekMediaItemIndex).isEqualTo(3); + assertThat(player.seekPositionMs).isEqualTo(2000); + } + @Test public void setPlaybackSpeed() throws Exception { float testSpeed = 1.5f; @@ -352,6 +436,55 @@ public void addMediaItem_withIndex() throws Exception { assertThat(player.mediaItems).hasSize(6); } + @Test + public void addMediaItem_withIndexWithoutGetTimelineCommand() throws Exception { + MockPlayer player = + new MockPlayer.Builder() + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .setMediaItems(/* itemCount= */ 5) + .build(); + player.currentMediaItemIndex = 3; + MediaSession session = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) + .setCallback( + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + SessionCommands sessionCommands = + new SessionCommands.Builder().addAllSessionCommands().build(); + Player.Commands playerCommands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TIMELINE) + .build(); + return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands); + } + + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, + MediaSession.ControllerInfo controller, + List mediaItems) { + return Futures.immediateFuture(mediaItems); + } + }) + .setId("addMediaItem_withIndexWithoutGetTimelineCommand") + .build(); + RemoteMediaController controller = + remoteControllerTestRule.createRemoteController(session.getToken()); + MediaItem mediaItem = MediaTestUtils.createMediaItem("addMediaItem_withIndex"); + + // The controller should only be able to see the current item without Timeline access. + controller.addMediaItem(/* index= */ 1, mediaItem); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, TIMEOUT_MS); + controller.release(); + session.release(); + player.release(); + + assertThat(player.index).isEqualTo(4); + } + @Test public void addMediaItems() throws Exception { int size = 2; @@ -376,6 +509,55 @@ public void addMediaItems_withIndex() throws Exception { assertThat(player.mediaItems).hasSize(7); } + @Test + public void addMediaItems_withIndexWithoutGetTimelineCommand() throws Exception { + MockPlayer player = + new MockPlayer.Builder() + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .setMediaItems(/* itemCount= */ 5) + .build(); + player.currentMediaItemIndex = 3; + MediaSession session = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) + .setCallback( + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + SessionCommands sessionCommands = + new SessionCommands.Builder().addAllSessionCommands().build(); + Player.Commands playerCommands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TIMELINE) + .build(); + return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands); + } + + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, + MediaSession.ControllerInfo controller, + List mediaItems) { + return Futures.immediateFuture(mediaItems); + } + }) + .setId("addMediaItems_withIndexWithoutGetTimelineCommand") + .build(); + RemoteMediaController controller = + remoteControllerTestRule.createRemoteController(session.getToken()); + MediaItem mediaItem = MediaTestUtils.createMediaItem("addMediaItem_withIndex"); + + // The controller should only be able to see the current item without Timeline access. + controller.addMediaItems(/* index= */ 1, ImmutableList.of(mediaItem, mediaItem)); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, TIMEOUT_MS); + controller.release(); + session.release(); + player.release(); + + assertThat(player.index).isEqualTo(4); + } + @Test public void removeMediaItem() throws Exception { int index = 3; @@ -386,6 +568,46 @@ public void removeMediaItem() throws Exception { assertThat(player.index).isEqualTo(index); } + @Test + public void removeMediaItem_withoutGetTimelineCommand() throws Exception { + MockPlayer player = + new MockPlayer.Builder() + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .setMediaItems(/* itemCount= */ 5) + .build(); + player.currentMediaItemIndex = 3; + MediaSession session = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) + .setCallback( + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + SessionCommands sessionCommands = + new SessionCommands.Builder().addAllSessionCommands().build(); + Player.Commands playerCommands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TIMELINE) + .build(); + return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands); + } + }) + .setId("removeMediaItem_withoutGetTimelineCommand") + .build(); + RemoteMediaController controller = + remoteControllerTestRule.createRemoteController(session.getToken()); + + // The controller should only be able to see the current item without Timeline access. + controller.removeMediaItem(/* index= */ 0); + player.awaitMethodCalled(MockPlayer.METHOD_REMOVE_MEDIA_ITEM, TIMEOUT_MS); + controller.release(); + session.release(); + player.release(); + + assertThat(player.index).isEqualTo(3); + } + @Test public void removeMediaItems() throws Exception { int fromIndex = 0; @@ -398,6 +620,47 @@ public void removeMediaItems() throws Exception { assertThat(player.toIndex).isEqualTo(toIndex); } + @Test + public void removeMediaItems_withoutGetTimelineCommand() throws Exception { + MockPlayer player = + new MockPlayer.Builder() + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .setMediaItems(/* itemCount= */ 5) + .build(); + player.currentMediaItemIndex = 3; + MediaSession session = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) + .setCallback( + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + SessionCommands sessionCommands = + new SessionCommands.Builder().addAllSessionCommands().build(); + Player.Commands playerCommands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_TIMELINE) + .build(); + return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands); + } + }) + .setId("removeMediaItems_withoutGetTimelineCommand") + .build(); + RemoteMediaController controller = + remoteControllerTestRule.createRemoteController(session.getToken()); + + // The controller should only be able to see the current item without Timeline access. + controller.removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ 0); + player.awaitMethodCalled(MockPlayer.METHOD_REMOVE_MEDIA_ITEMS, TIMEOUT_MS); + controller.release(); + session.release(); + player.release(); + + assertThat(player.fromIndex).isEqualTo(3); + assertThat(player.toIndex).isEqualTo(3); + } + @Test public void clearMediaItems() throws Exception { controller.clearMediaItems(); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java index 23e30bf4b8e..4d33c9efe30 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java @@ -795,7 +795,9 @@ public boolean isCurrentWindowDynamic() { @Override public boolean isCurrentMediaItemDynamic() { - throw new UnsupportedOperationException(); + Timeline timeline = getCurrentTimeline(); + return !timeline.isEmpty() + && timeline.getWindow(getCurrentMediaItemIndex(), new Timeline.Window()).isDynamic; } /** @@ -809,7 +811,9 @@ public boolean isCurrentWindowLive() { @Override public boolean isCurrentMediaItemLive() { - throw new UnsupportedOperationException(); + Timeline timeline = getCurrentTimeline(); + return !timeline.isEmpty() + && timeline.getWindow(getCurrentMediaItemIndex(), new Timeline.Window()).isLive(); } /** @@ -823,7 +827,9 @@ public boolean isCurrentWindowSeekable() { @Override public boolean isCurrentMediaItemSeekable() { - throw new UnsupportedOperationException(); + Timeline timeline = getCurrentTimeline(); + return !timeline.isEmpty() + && timeline.getWindow(getCurrentMediaItemIndex(), new Timeline.Window()).isSeekable; } @Override From 28e37808ed638494522ea71851fb7b511262045c Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 19 Jan 2023 15:00:49 +0000 Subject: [PATCH 116/141] Update media controller position before pausing. We stop estimating new position when pausing until we receive a new position from the player. However, this means that we will continue to return a possible stale previous position. Updating the current position before pausing solves this issue. PiperOrigin-RevId: 503153982 (cherry picked from commit e961c1b5e9bb4a6f63458b1bdcb49e97f415fabf) --- .../session/MediaControllerImplBase.java | 63 ++++++++++--------- .../media3/session/MediaControllerTest.java | 31 +++++++++ 2 files changed, 65 insertions(+), 29 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index 8cca5c4d874..8f224cb1eec 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -129,7 +129,7 @@ @Nullable private TextureView videoTextureView; private Size surfaceSize; @Nullable private IMediaSession iSession; - private long lastReturnedCurrentPositionMs; + private long currentPositionMs; private long lastSetPlayWhenReadyCalledTimeMs; @Nullable private PlayerInfo pendingPlayerInfo; @Nullable private BundlingExclusions pendingBundlingExclusions; @@ -175,7 +175,7 @@ public MediaControllerImplBase( ? null : new SessionServiceConnection(connectionHints); flushCommandQueueHandler = new FlushCommandQueueHandler(applicationLooper); - lastReturnedCurrentPositionMs = C.TIME_UNSET; + currentPositionMs = C.TIME_UNSET; lastSetPlayWhenReadyCalledTimeMs = C.TIME_UNSET; } @@ -582,32 +582,8 @@ public long getDuration() { @Override public long getCurrentPosition() { - boolean receivedUpdatedPositionInfo = - lastSetPlayWhenReadyCalledTimeMs < playerInfo.sessionPositionInfo.eventTimeMs; - if (!playerInfo.isPlaying) { - if (receivedUpdatedPositionInfo || lastReturnedCurrentPositionMs == C.TIME_UNSET) { - lastReturnedCurrentPositionMs = playerInfo.sessionPositionInfo.positionInfo.positionMs; - } - return lastReturnedCurrentPositionMs; - } - - if (!receivedUpdatedPositionInfo && lastReturnedCurrentPositionMs != C.TIME_UNSET) { - // Need an updated current position in order to make a new position estimation - return lastReturnedCurrentPositionMs; - } - - long elapsedTimeMs = - (getInstance().getTimeDiffMs() != C.TIME_UNSET) - ? getInstance().getTimeDiffMs() - : SystemClock.elapsedRealtime() - playerInfo.sessionPositionInfo.eventTimeMs; - long estimatedPositionMs = - playerInfo.sessionPositionInfo.positionInfo.positionMs - + (long) (elapsedTimeMs * playerInfo.playbackParameters.speed); - if (playerInfo.sessionPositionInfo.durationMs != C.TIME_UNSET) { - estimatedPositionMs = min(estimatedPositionMs, playerInfo.sessionPositionInfo.durationMs); - } - lastReturnedCurrentPositionMs = estimatedPositionMs; - return lastReturnedCurrentPositionMs; + maybeUpdateCurrentPositionMs(); + return currentPositionMs; } @Override @@ -1966,7 +1942,8 @@ private void setPlayWhenReady( return; } - // Stop estimating content position until a new positionInfo arrives from the player + // Update position and then stop estimating until a new positionInfo arrives from the player. + maybeUpdateCurrentPositionMs(); lastSetPlayWhenReadyCalledTimeMs = SystemClock.elapsedRealtime(); PlayerInfo playerInfo = this.playerInfo.copyWithPlayWhenReady( @@ -2726,6 +2703,34 @@ private PlayerInfo maskTimelineAndPositionInfo( return playerInfo; } + private void maybeUpdateCurrentPositionMs() { + boolean receivedUpdatedPositionInfo = + lastSetPlayWhenReadyCalledTimeMs < playerInfo.sessionPositionInfo.eventTimeMs; + if (!playerInfo.isPlaying) { + if (receivedUpdatedPositionInfo || currentPositionMs == C.TIME_UNSET) { + currentPositionMs = playerInfo.sessionPositionInfo.positionInfo.positionMs; + } + return; + } + + if (!receivedUpdatedPositionInfo && currentPositionMs != C.TIME_UNSET) { + // Need an updated current position in order to make a new position estimation + return; + } + + long elapsedTimeMs = + (getInstance().getTimeDiffMs() != C.TIME_UNSET) + ? getInstance().getTimeDiffMs() + : SystemClock.elapsedRealtime() - playerInfo.sessionPositionInfo.eventTimeMs; + long estimatedPositionMs = + playerInfo.sessionPositionInfo.positionInfo.positionMs + + (long) (elapsedTimeMs * playerInfo.playbackParameters.speed); + if (playerInfo.sessionPositionInfo.durationMs != C.TIME_UNSET) { + estimatedPositionMs = min(estimatedPositionMs, playerInfo.sessionPositionInfo.durationMs); + } + currentPositionMs = estimatedPositionMs; + } + private Period getPeriodWithNewWindowIndex(Timeline timeline, int periodIndex, int windowIndex) { Period period = new Period(); timeline.getPeriod(periodIndex, period); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java index a7b5dbfc6c3..0bf910b6864 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java @@ -887,6 +887,37 @@ public void getCurrentPosition_whenPlaying_advances() throws Exception { assertThat(currentPositionMs).isEqualTo(expectedCurrentPositionMs); } + @Test + public void getCurrentPosition_afterPause_returnsCorrectPosition() throws Exception { + long testCurrentPosition = 100L; + PlaybackParameters testPlaybackParameters = new PlaybackParameters(/* speed= */ 2.0f); + long testTimeDiff = 50L; + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setPlaybackState(Player.STATE_READY) + .setPlayWhenReady(true) + .setCurrentPosition(testCurrentPosition) + .setDuration(10_000L) + .setPlaybackParameters(testPlaybackParameters) + .build(); + remoteSession.setPlayer(playerConfig); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + + long currentPositionMs = + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setTimeDiffMs(testTimeDiff); + controller.pause(); + return controller.getCurrentPosition(); + }); + + long expectedCurrentPositionMs = + testCurrentPosition + (long) (testTimeDiff * testPlaybackParameters.speed); + assertThat(currentPositionMs).isEqualTo(expectedCurrentPositionMs); + } + @Test public void getContentPosition_whenPlayingAd_doesNotAdvance() throws Exception { long testContentPosition = 100L; From 43677b95eb5d3c5df04d3a130d0f87f673ac59b3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 19 Jan 2023 16:32:14 +0000 Subject: [PATCH 117/141] Add command check for metadata in DefaultMediaNotificationProvider PiperOrigin-RevId: 503172986 (cherry picked from commit 052c4b3c1a6b72efd7fcbf433c646fed9ea91748) --- .../DefaultMediaNotificationProvider.java | 54 ++++++++++--------- .../DefaultMediaNotificationProviderTest.java | 45 +++++++++++++++- 2 files changed, 71 insertions(+), 28 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index c3acb2a83d4..6b2368df7dd 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -319,33 +319,35 @@ public final MediaNotification createNotification( mediaStyle.setShowActionsInCompactView(compactViewIndices); // Set metadata info in the notification. - MediaMetadata metadata = player.getMediaMetadata(); - builder - .setContentTitle(getNotificationContentTitle(metadata)) - .setContentText(getNotificationContentText(metadata)); - @Nullable - ListenableFuture bitmapFuture = - mediaSession.getBitmapLoader().loadBitmapFromMetadata(metadata); - if (bitmapFuture != null) { - if (pendingOnBitmapLoadedFutureCallback != null) { - pendingOnBitmapLoadedFutureCallback.discardIfPending(); - } - if (bitmapFuture.isDone()) { - try { - builder.setLargeIcon(Futures.getDone(bitmapFuture)); - } catch (ExecutionException e) { - Log.w(TAG, getBitmapLoadErrorMessage(e)); + if (player.isCommandAvailable(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)) { + MediaMetadata metadata = player.getMediaMetadata(); + builder + .setContentTitle(getNotificationContentTitle(metadata)) + .setContentText(getNotificationContentText(metadata)); + @Nullable + ListenableFuture bitmapFuture = + mediaSession.getBitmapLoader().loadBitmapFromMetadata(metadata); + if (bitmapFuture != null) { + if (pendingOnBitmapLoadedFutureCallback != null) { + pendingOnBitmapLoadedFutureCallback.discardIfPending(); + } + if (bitmapFuture.isDone()) { + try { + builder.setLargeIcon(Futures.getDone(bitmapFuture)); + } catch (ExecutionException e) { + Log.w(TAG, getBitmapLoadErrorMessage(e)); + } + } else { + pendingOnBitmapLoadedFutureCallback = + new OnBitmapLoadedFutureCallback( + notificationId, builder, onNotificationChangedCallback); + Futures.addCallback( + bitmapFuture, + pendingOnBitmapLoadedFutureCallback, + // This callback must be executed on the next looper iteration, after this method has + // returned a media notification. + mainHandler::post); } - } else { - pendingOnBitmapLoadedFutureCallback = - new OnBitmapLoadedFutureCallback( - notificationId, builder, onNotificationChangedCallback); - Futures.addCallback( - bitmapFuture, - pendingOnBitmapLoadedFutureCallback, - // This callback must be executed on the next looper iteration, after this method has - // returned a media notification. - mainHandler::post); } } diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java index fe7616bce3d..e696979c6cf 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java @@ -20,6 +20,7 @@ import static androidx.media3.session.DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -628,6 +629,33 @@ public void setMediaMetadataArtist_notificationUsesItAsContentText() { assertThat(isMediaMetadataArtistEqualToNotificationContentText).isTrue(); } + @Test + public void + setMediaMetadata_withoutAvailableCommandToGetMetadata_doesNotUseMetadataForNotification() { + Context context = ApplicationProvider.getApplicationContext(); + DefaultMediaNotificationProvider defaultMediaNotificationProvider = + new DefaultMediaNotificationProvider.Builder(context).build(); + DefaultActionFactory defaultActionFactory = + new DefaultActionFactory(Robolectric.setupService(TestService.class)); + MediaSession mockMediaSession = + createMockMediaSessionForNotification( + new MediaMetadata.Builder().setArtist("artist").setTitle("title").build(), + /* getMetadataCommandAvailable= */ false); + BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); + when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); + when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); + + MediaNotification notification = + defaultMediaNotificationProvider.createNotification( + mockMediaSession, + ImmutableList.of(), + defaultActionFactory, + mock(MediaNotification.Provider.Callback.class)); + + assertThat(NotificationCompat.getContentText(notification.notification)).isNull(); + assertThat(NotificationCompat.getContentTitle(notification.notification)).isNull(); + } + /** * {@link DefaultMediaNotificationProvider} is designed to be extendable. Public constructor * should not be removed. @@ -720,9 +748,22 @@ private static void assertHasNotificationChannel( } private static MediaSession createMockMediaSessionForNotification(MediaMetadata mediaMetadata) { + return createMockMediaSessionForNotification( + mediaMetadata, /* getMetadataCommandAvailable= */ true); + } + + private static MediaSession createMockMediaSessionForNotification( + MediaMetadata mediaMetadata, boolean getMetadataCommandAvailable) { Player mockPlayer = mock(Player.class); - when(mockPlayer.getAvailableCommands()).thenReturn(Commands.EMPTY); - when(mockPlayer.getMediaMetadata()).thenReturn(mediaMetadata); + when(mockPlayer.isCommandAvailable(anyInt())).thenReturn(false); + if (getMetadataCommandAvailable) { + when(mockPlayer.getAvailableCommands()) + .thenReturn(new Commands.Builder().add(Player.COMMAND_GET_MEDIA_ITEMS_METADATA).build()); + when(mockPlayer.isCommandAvailable(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)).thenReturn(true); + when(mockPlayer.getMediaMetadata()).thenReturn(mediaMetadata); + } else { + when(mockPlayer.getAvailableCommands()).thenReturn(Commands.EMPTY); + } MediaSession mockMediaSession = mock(MediaSession.class); when(mockMediaSession.getPlayer()).thenReturn(mockPlayer); MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); From 967224c1aac6a3f165359acba3637f2053691361 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 19 Jan 2023 16:56:32 +0000 Subject: [PATCH 118/141] Explicitly document most Player.Listener methods in terms of getters This makes it implicitly clear that if the value of a getter changes due to a change in command availability then the listener will be invoked, without needing to explicitly document every command on every listener method. #minor-release PiperOrigin-RevId: 503178383 (cherry picked from commit 280889bc4a5b7ddc1b1c9fe15e222cad7f2e548a) --- .../java/androidx/media3/common/Player.java | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index 9015699e320..a33775d5222 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -605,9 +605,15 @@ private static Commands fromBundle(Bundle bundle) { } /** - * Listener of all changes in the Player. + * Listener for changes in a {@link Player}. * *

      All methods have no-op default implementations to allow selective overrides. + * + *

      If the return value of a {@link Player} getter changes due to a change in {@linkplain + * #onAvailableCommandsChanged(Commands) command availability}, the corresponding listener + * method(s) will be invoked. If the return value of a {@link Player} getter does not change + * because the corresponding command is {@linkplain #onAvailableCommandsChanged(Commands) not + * available}, the corresponding listener method will not be invoked. */ interface Listener { @@ -617,9 +623,6 @@ interface Listener { *

      State changes and events that happen within one {@link Looper} message queue iteration are * reported together and only after all individual callbacks were triggered. * - *

      Only state changes represented by {@linkplain Event events} are reported through this - * method. - * *

      Listeners should prefer this method over individual callbacks in the following cases: * *

        @@ -645,7 +648,7 @@ interface Listener { default void onEvents(Player player, Events events) {} /** - * Called when the timeline has been refreshed. + * Called when the value of {@link Player#getCurrentTimeline()} changes. * *

        Note that the current {@link MediaItem} or playback position may change as a result of a * timeline change. If playback can't continue smoothly because of this timeline change, a @@ -664,9 +667,8 @@ default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reas * Called when playback transitions to a media item or starts repeating a media item according * to the current {@link #getRepeatMode() repeat mode}. * - *

        Note that this callback is also called when the playlist becomes non-empty or empty as a - * consequence of a playlist change or {@linkplain #onAvailableCommandsChanged(Commands) a - * change in available commands}. + *

        Note that this callback is also called when the value of {@link #getCurrentTimeline()} + * becomes non-empty or empty. * *

        {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. @@ -678,7 +680,7 @@ default void onMediaItemTransition( @Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {} /** - * Called when the tracks change. + * Called when the value of {@link Player#getCurrentTracks()} changes. * *

        {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. @@ -688,14 +690,7 @@ default void onMediaItemTransition( default void onTracksChanged(Tracks tracks) {} /** - * Called when the combined {@link MediaMetadata} changes. - * - *

        The provided {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata - * MediaItem metadata}, the static metadata in the media's {@link Format#metadata Format}, and - * any timed metadata that has been parsed from the media and output via {@link - * Listener#onMetadata(Metadata)}. If a field is populated in the {@link - * MediaItem#mediaMetadata}, it will be prioritised above the same field coming from static or - * timed metadata. + * Called when the value of {@link Player#getMediaMetadata()} changes. * *

        This method may be called multiple times in quick succession. * @@ -707,7 +702,7 @@ default void onTracksChanged(Tracks tracks) {} default void onMediaMetadataChanged(MediaMetadata mediaMetadata) {} /** - * Called when the playlist {@link MediaMetadata} changes. + * Called when the value of {@link Player#getPlaylistMetadata()} changes. * *

        {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. @@ -876,10 +871,10 @@ default void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) {} /** - * Called when the current playback parameters change. The playback parameters may change due to - * a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change - * them (for example, if audio playback switches to passthrough or offload mode, where speed - * adjustment is no longer possible). + * Called when the value of {@link #getPlaybackParameters()} changes. The playback parameters + * may change due to a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player + * itself may change them (for example, if audio playback switches to passthrough or offload + * mode, where speed adjustment is no longer possible). * *

        {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. @@ -940,7 +935,7 @@ default void onSeekProcessed() {} default void onAudioSessionIdChanged(int audioSessionId) {} /** - * Called when the audio attributes change. + * Called when the value of {@link #getAudioAttributes()} changes. * *

        {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. @@ -950,7 +945,7 @@ default void onAudioSessionIdChanged(int audioSessionId) {} default void onAudioAttributesChanged(AudioAttributes audioAttributes) {} /** - * Called when the volume changes. + * Called when the value of {@link #getVolume()} changes. * *

        {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. @@ -980,7 +975,7 @@ default void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {} default void onDeviceInfoChanged(DeviceInfo deviceInfo) {} /** - * Called when the device volume or mute state changes. + * Called when the value of {@link #getDeviceVolume()} or {@link #isDeviceMuted()} changes. * *

        {@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. @@ -1024,7 +1019,7 @@ default void onSurfaceSizeChanged(int width, int height) {} default void onRenderedFirstFrame() {} /** - * Called when there is a change in the {@linkplain Cue cues}. + * Called when the value of {@link #getCurrentCues()} changes. * *

        Both this method and {@link #onCues(CueGroup)} are called when there is a change in the * cues. You should only implement one or the other. @@ -1039,7 +1034,7 @@ default void onRenderedFirstFrame() {} default void onCues(List cues) {} /** - * Called when there is a change in the {@link CueGroup}. + * Called when the value of {@link #getCurrentCues()} changes. * *

        Both this method and {@link #onCues(List)} are called when there is a change in the cues. * You should only implement one or the other. @@ -1390,7 +1385,7 @@ default void onMetadata(Metadata metadata) {} /** * Commands that indicate which method calls are currently permitted on a particular {@code - * Player} instance, and which corresponding {@link Player.Listener} methods will be invoked. + * Player} instance. * *

        The currently available commands can be inspected with {@link #getAvailableCommands()} and * {@link #isCommandAvailable(int)}. From 107a481356ce80bbb439b6ee44f1f37f8cc9543a Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 20 Jan 2023 12:03:58 +0000 Subject: [PATCH 119/141] Add the MediaSession as an argument to `getMediaButtons()` Issue: androidx/media#216 #minor-release PiperOrigin-RevId: 503406474 (cherry picked from commit e690802e9ecf96dfbb972864819a45ae92c47c90) --- RELEASENOTES.md | 3 ++ .../DefaultMediaNotificationProvider.java | 18 ++++++---- .../DefaultMediaNotificationProviderTest.java | 33 ++++++++++++++----- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 69a5f177a37..47170725bef 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -43,6 +43,9 @@ `SessionToken` ([#171](https://github.com/androidx/media/issues/171)). * Use `onMediaMetadataChanged` to trigger updates of the platform media session ([#219](https://github.com/androidx/media/issues/219)). + * Add the media session as an argument of `getMediaButtons()` of the + `DefaultMediaNotificationProvider` and use immutable lists for clarity + ([#216](https://github.com/androidx/media/issues/216)). * Metadata: * Parse multiple null-separated values from ID3 frames, as permitted by ID3 v2.4. diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index 6b2368df7dd..b6c487fcd22 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -54,7 +54,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; @@ -310,6 +309,7 @@ public final MediaNotification createNotification( addNotificationActions( mediaSession, getMediaButtons( + mediaSession, player.getAvailableCommands(), customLayout, /* showPauseButton= */ player.getPlayWhenReady() @@ -418,6 +418,7 @@ public final void setSmallIcon(@DrawableRes int smallIconResourceId) { * need the custom command set in {@link MediaSession.Callback#onPostConnect(MediaSession, * MediaSession.ControllerInfo)} also. * + * @param session The media session. * @param playerCommands The available player commands. * @param customLayout The {@linkplain MediaSession#setCustomLayout(List) custom layout of * commands}. @@ -425,10 +426,13 @@ public final void setSmallIcon(@DrawableRes int smallIconResourceId) { * player is currently playing content), otherwise show a play button to start playback. * @return The ordered list of command buttons to be placed on the notification. */ - protected List getMediaButtons( - Player.Commands playerCommands, List customLayout, boolean showPauseButton) { + protected ImmutableList getMediaButtons( + MediaSession session, + Player.Commands playerCommands, + ImmutableList customLayout, + boolean showPauseButton) { // Skip to previous action. - List commandButtons = new ArrayList<>(); + ImmutableList.Builder commandButtons = new ImmutableList.Builder<>(); if (playerCommands.containsAny(COMMAND_SEEK_TO_PREVIOUS, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)) { Bundle commandButtonExtras = new Bundle(); commandButtonExtras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, INDEX_UNSET); @@ -477,14 +481,14 @@ protected List getMediaButtons( commandButtons.add(button); } } - return commandButtons; + return commandButtons.build(); } /** * Adds the media buttons to the notification builder for the given action factory. * *

        The list of {@code mediaButtons} is the list resulting from {@link #getMediaButtons( - * Player.Commands, List, boolean)}. + * MediaSession, Player.Commands, ImmutableList, boolean)}. * *

        Override this method to customize how the media buttons {@linkplain * NotificationCompat.Builder#addAction(NotificationCompat.Action) are added} to the notification @@ -505,7 +509,7 @@ protected List getMediaButtons( */ protected int[] addNotificationActions( MediaSession mediaSession, - List mediaButtons, + ImmutableList mediaButtons, NotificationCompat.Builder builder, MediaNotification.ActionFactory actionFactory) { int[] compactViewIndices = new int[3]; diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java index e696979c6cf..cd159ea0be7 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java @@ -67,13 +67,20 @@ public void getMediaButtons_playWhenReadyTrueOrFalse_correctPlayPauseResources() new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext()) .build(); Commands commands = new Commands.Builder().addAllCommands().build(); + MediaSession mockMediaSession = mock(MediaSession.class); List mediaButtonsWhenPlaying = defaultMediaNotificationProvider.getMediaButtons( - commands, /* customLayout= */ ImmutableList.of(), /* showPauseButton= */ true); + mockMediaSession, + commands, + /* customLayout= */ ImmutableList.of(), + /* showPauseButton= */ true); List mediaButtonWhenPaused = defaultMediaNotificationProvider.getMediaButtons( - commands, /* customLayout= */ ImmutableList.of(), /* showPauseButton= */ false); + mockMediaSession, + commands, + /* customLayout= */ ImmutableList.of(), + /* showPauseButton= */ false); assertThat(mediaButtonsWhenPlaying).hasSize(3); assertThat(mediaButtonsWhenPlaying.get(1).playerCommand).isEqualTo(Player.COMMAND_PLAY_PAUSE); @@ -92,6 +99,7 @@ public void getMediaButtons_allCommandsAvailable_createsPauseSkipNextSkipPreviou DefaultMediaNotificationProvider defaultMediaNotificationProvider = new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext()) .build(); + MediaSession mockMediaSession = mock(MediaSession.class); Commands commands = new Commands.Builder().addAllCommands().build(); SessionCommand customSessionCommand = new SessionCommand("", Bundle.EMPTY); CommandButton customCommandButton = @@ -103,7 +111,10 @@ public void getMediaButtons_allCommandsAvailable_createsPauseSkipNextSkipPreviou List mediaButtons = defaultMediaNotificationProvider.getMediaButtons( - commands, ImmutableList.of(customCommandButton), /* showPauseButton= */ true); + mockMediaSession, + commands, + ImmutableList.of(customCommandButton), + /* showPauseButton= */ true); assertThat(mediaButtons).hasSize(4); assertThat(mediaButtons.get(0).playerCommand) @@ -118,6 +129,7 @@ public void getMediaButtons_noPlayerCommandsAvailable_onlyCustomLayoutButtons() DefaultMediaNotificationProvider defaultMediaNotificationProvider = new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext()) .build(); + MediaSession mockMediaSession = mock(MediaSession.class); Commands commands = new Commands.Builder().build(); SessionCommand customSessionCommand = new SessionCommand("action1", Bundle.EMPTY); CommandButton customCommandButton = @@ -129,7 +141,10 @@ public void getMediaButtons_noPlayerCommandsAvailable_onlyCustomLayoutButtons() List mediaButtons = defaultMediaNotificationProvider.getMediaButtons( - commands, ImmutableList.of(customCommandButton), /* showPauseButton= */ true); + mockMediaSession, + commands, + ImmutableList.of(customCommandButton), + /* showPauseButton= */ true); assertThat(mediaButtons).containsExactly(customCommandButton); } @@ -702,17 +717,19 @@ public void overridesProviderDefinition_compilesSuccessfully() { DefaultMediaNotificationProvider unused = new DefaultMediaNotificationProvider(context) { @Override - public List getMediaButtons( + public ImmutableList getMediaButtons( + MediaSession mediaSession, Player.Commands playerCommands, - List customLayout, + ImmutableList customLayout, boolean showPauseButton) { - return super.getMediaButtons(playerCommands, customLayout, showPauseButton); + return super.getMediaButtons( + mediaSession, playerCommands, customLayout, showPauseButton); } @Override public int[] addNotificationActions( MediaSession mediaSession, - List mediaButtons, + ImmutableList mediaButtons, NotificationCompat.Builder builder, MediaNotification.ActionFactory actionFactory) { return super.addNotificationActions(mediaSession, mediaButtons, builder, actionFactory); From e266051fbef7c35528de7890de5f50127432cd4c Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Fri, 20 Jan 2023 14:25:09 +0000 Subject: [PATCH 120/141] Add onSetMediaItems listener with access to start index and position Added onSetMediaItems callback listener to allow the session to modify/set MediaItem list, starting index and position before call to Player.setMediaItem(s). Added conditional check in MediaSessionStub.setMediaItem methods to only call player.setMediaItem rather than setMediaItems if player does not support COMMAND_CHANGE_MEDIA_ITEMS PiperOrigin-RevId: 503427927 (cherry picked from commit bb11e0286eaa49b4178dfa29ebaea5dafba8fc39) --- RELEASENOTES.md | 3 + .../androidx/media3/session/MediaSession.java | 168 +++++++++++++++- .../media3/session/MediaSessionImpl.java | 8 + .../session/MediaSessionLegacyStub.java | 19 +- .../media3/session/MediaSessionStub.java | 188 ++++++++++++------ .../androidx/media3/session/MediaUtils.java | 27 +++ .../session/MediaSessionCallbackTest.java | 173 +++++++++++++++- ...CallbackWithMediaControllerCompatTest.java | 118 +++++++++-- .../session/MediaSessionPlayerTest.java | 16 +- 9 files changed, 621 insertions(+), 99 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 47170725bef..5ceebf14d9f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -46,6 +46,9 @@ * Add the media session as an argument of `getMediaButtons()` of the `DefaultMediaNotificationProvider` and use immutable lists for clarity ([#216](https://github.com/androidx/media/issues/216)). + * Add `onSetMediaItems` callback listener to provide means to modify/set + `MediaItem` list, starting index and position by session before setting + onto Player ([#156](https://github.com/androidx/media/issues/156)). * Metadata: * Parse multiple null-separated values from ID3 frames, as permitted by ID3 v2.4. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 7f09c4280ba..9037f9aae82 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -58,6 +58,8 @@ import androidx.media3.common.util.Util; import androidx.media3.session.MediaLibraryService.LibraryParams; import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Longs; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.util.HashMap; @@ -1055,13 +1057,13 @@ default ListenableFuture onCustomCommand( /** * Called when a controller requested to add new {@linkplain MediaItem media items} to the - * playlist via one of the {@code Player.addMediaItem(s)} or {@code Player.setMediaItem(s)} - * methods. + * playlist via one of the {@code Player.addMediaItem(s)} methods. Unless overriden, {@link + * Callback#onSetMediaItems} will direct {@code Player.setMediaItem(s)} to this method as well. * - *

        This callback is also called when an app is using a legacy {@link - * MediaControllerCompat.TransportControls} to prepare or play media (for instance when browsing - * the catalogue and then selecting an item for preparation from Android Auto that is using the - * legacy Media1 library). + *

        In addition, unless {@link Callback#onSetMediaItems} is overridden, this callback is also + * called when an app is using a legacy {@link MediaControllerCompat.TransportControls} to + * prepare or play media (for instance when browsing the catalogue and then selecting an item + * for preparation from Android Auto that is using the legacy Media1 library). * *

        Note that the requested {@linkplain MediaItem media items} don't have a {@link * MediaItem.LocalConfiguration} (for example, a URI) and need to be updated to make them @@ -1074,8 +1076,8 @@ default ListenableFuture onCustomCommand( * the {@link MediaItem media items} have been resolved, the session will call {@link * Player#setMediaItems} or {@link Player#addMediaItems} as requested. * - *

        Interoperability: This method will be called in response to the following {@link - * MediaControllerCompat} methods: + *

        Interoperability: This method will be called, unless {@link Callback#onSetMediaItems} is + * overridden, in response to the following {@link MediaControllerCompat} methods: * *

          *
        • {@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri} @@ -1103,6 +1105,156 @@ default ListenableFuture> onAddMediaItems( MediaSession mediaSession, ControllerInfo controller, List mediaItems) { return Futures.immediateFailedFuture(new UnsupportedOperationException()); } + + /** + * Called when a controller requested to set {@linkplain MediaItem media items} to the playlist + * via one of the {@code Player.setMediaItem(s)} methods. The default implementation calls + * {@link Callback#onAddMediaItems}. Override this method if you want to modify/set the starting + * index/position for the {@code Player.setMediaItem(s)} methods. + * + *

          This callback is also called when an app is using a legacy {@link + * MediaControllerCompat.TransportControls} to prepare or play media (for instance when browsing + * the catalogue and then selecting an item for preparation from Android Auto that is using the + * legacy Media1 library). + * + *

          Note that the requested {@linkplain MediaItem media items} in the + * MediaItemsWithStartPosition don't have a {@link MediaItem.LocalConfiguration} (for example, a + * URI) and need to be updated to make them playable by the underlying {@link Player}. + * Typically, this implementation should be able to identify the correct item by its {@link + * MediaItem#mediaId} and/or the {@link MediaItem#requestMetadata}. + * + *

          Return a {@link ListenableFuture} with the resolved {@linkplain + * MediaItemsWithStartPosition media items and starting index and position}. You can also return + * the items directly by using Guava's {@link Futures#immediateFuture(Object)}. Once the {@link + * MediaItemsWithStartPosition} has been resolved, the session will call {@link + * Player#setMediaItems} as requested. If the resolved {@link + * MediaItemsWithStartPosition#startIndex startIndex} is {@link + * androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} and {@link + * MediaItemsWithStartPosition#startPositionMs startPositionMs} is {@link + * androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} then the session will call {@link + * Player#setMediaItem(MediaItem, boolean)} with {@code resetPosition} set to {@code true}. + * + *

          Interoperability: This method will be called in response to the following {@link + * MediaControllerCompat} methods: + * + *

            + *
          • {@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri} + *
          • {@link MediaControllerCompat.TransportControls#playFromUri playFromUri} + *
          • {@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId} + *
          • {@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId} + *
          • {@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch} + *
          • {@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch} + *
          • {@link MediaControllerCompat.TransportControls#addQueueItem addQueueItem} + *
          + * + * The values of {@link MediaItem#mediaId}, {@link MediaItem.RequestMetadata#mediaUri}, {@link + * MediaItem.RequestMetadata#searchQuery} and {@link MediaItem.RequestMetadata#extras} will be + * set to match the legacy method call. The session will call {@link Player#setMediaItems} or + * {@link Player#addMediaItems}, followed by {@link Player#prepare()} and {@link Player#play()} + * as appropriate once the {@link MediaItem} has been resolved. + * + * @param mediaSession The session for this event. + * @param controller The controller information. + * @param mediaItems The list of requested {@linkplain MediaItem media items}. + * @param startIndex The start index in the {@link MediaItem} list from which to start playing. + * If startIndex is {@link androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} and + * startPositionMs is {@link androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} then caller + * is requesting to set media items with default index and position. + * @param startPositionMs The starting position in the media item from where to start playing. + * If startIndex is {@link androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} and + * startPositionMs is {@link androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} then caller + * is requesting to set media items with default index and position. + * @return A {@link ListenableFuture} with a {@link MediaItemsWithStartPosition} containing a + * list of resolved {@linkplain MediaItem media items}, and a starting index and position + * that are playable by the underlying {@link Player}. If returned {@link + * MediaItemsWithStartPosition#startIndex} is {@link androidx.media3.common.C#INDEX_UNSET + * C.INDEX_UNSET} and {@link MediaItemsWithStartPosition#startPositionMs} is {@link + * androidx.media3.common.C#TIME_UNSET C.TIME_UNSET}, then {@linkplain + * Player#setMediaItems(List, boolean) Player#setMediaItems(List, true)} will be called to + * set media items with default index and position. + */ + @UnstableApi + default ListenableFuture onSetMediaItems( + MediaSession mediaSession, + ControllerInfo controller, + List mediaItems, + int startIndex, + long startPositionMs) { + return Util.transformFutureAsync( + onAddMediaItems(mediaSession, controller, mediaItems), + (mediaItemList) -> + Futures.immediateFuture( + new MediaItemsWithStartPosition(mediaItemList, startIndex, startPositionMs))); + } + } + + /** Representation of list of media items and where to start playing */ + @UnstableApi + public static final class MediaItemsWithStartPosition { + /** List of {@link MediaItem media items}. */ + public final ImmutableList mediaItems; + /** + * Index to start playing at in {@link MediaItem} list. + * + *

          If startIndex is {@link androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} and + * startPositionMs is {@link androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} then the + * requested start is the default index and position. If only startIndex is {@link + * androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET}, then the requested start is the + * {@linkplain Player#getCurrentMediaItemIndex() current index} and {@linkplain + * Player#getContentPosition() position}. + */ + public final int startIndex; + /** + * Position to start playing from in starting media item. + * + *

          If startIndex is {@link androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} and + * startPositionMs is {@link androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} then the + * requested start is the default start index that takes into account whether {@link + * Player#getShuffleModeEnabled() shuffling is enabled} and the {@linkplain + * Timeline.Window#defaultPositionUs} default position}. If only startIndex is {@link + * androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET}, then the requested start is the + * {@linkplain Player#getCurrentMediaItemIndex() current index} and {@linkplain + * Player#getContentPosition() position}. + */ + public final long startPositionMs; + + /** + * Create an instance. + * + * @param mediaItems List of {@link MediaItem media items}. + * @param startIndex Index to start playing at in {@link MediaItem} list. + * @param startPositionMs Position to start playing from in starting media item. + */ + public MediaItemsWithStartPosition( + List mediaItems, int startIndex, long startPositionMs) { + this.mediaItems = ImmutableList.copyOf(mediaItems); + this.startIndex = startIndex; + this.startPositionMs = startPositionMs; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof MediaItemsWithStartPosition)) { + return false; + } + + MediaItemsWithStartPosition other = (MediaItemsWithStartPosition) obj; + + return mediaItems.equals(other.mediaItems) + && Util.areEqual(startIndex, other.startIndex) + && Util.areEqual(startPositionMs, other.startPositionMs); + } + + @Override + public int hashCode() { + int result = mediaItems.hashCode(); + result = 31 * result + startIndex; + result = 31 * result + Longs.hashCode(startPositionMs); + return result; + } } /** diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 180030adf14..9d32853015e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -69,6 +69,7 @@ import androidx.media3.common.util.Util; import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; +import androidx.media3.session.MediaSession.MediaItemsWithStartPosition; import androidx.media3.session.SequencedFutureManager.SequencedFuture; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; @@ -524,6 +525,13 @@ protected ListenableFuture> onAddMediaItemsOnHandler( "onAddMediaItems must return a non-null future"); } + protected ListenableFuture onSetMediaItemsOnHandler( + ControllerInfo controller, List mediaItems, int startIndex, long startPositionMs) { + return checkNotNull( + callback.onSetMediaItems(instance, controller, mediaItems, startIndex, startPositionMs), + "onSetMediaItems must return a non-null future"); + } + protected boolean isReleased() { synchronized (lock) { return closed; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index d49b5b16665..0c467ac81e8 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -85,6 +85,7 @@ import androidx.media3.common.util.Util; import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; +import androidx.media3.session.MediaSession.MediaItemsWithStartPosition; import androidx.media3.session.SessionCommand.CommandCode; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; @@ -711,18 +712,26 @@ private void handleMediaRequest(MediaItem mediaItem, boolean play) { dispatchSessionTaskWithPlayerCommand( COMMAND_SET_MEDIA_ITEM, controller -> { - ListenableFuture> mediaItemsFuture = - sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)); + ListenableFuture mediaItemsFuture = + sessionImpl.onSetMediaItemsOnHandler( + controller, ImmutableList.of(mediaItem), C.INDEX_UNSET, C.TIME_UNSET); Futures.addCallback( mediaItemsFuture, - new FutureCallback>() { + new FutureCallback() { @Override - public void onSuccess(List mediaItems) { + public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) { postOrRun( sessionImpl.getApplicationHandler(), () -> { PlayerWrapper player = sessionImpl.getPlayerWrapper(); - player.setMediaItems(mediaItems); + if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET + && mediaItemsWithStartPosition.startPositionMs == C.TIME_UNSET) { + MediaUtils.setMediaItemsWithDefaultStartIndexAndPosition( + player, mediaItemsWithStartPosition); + } else { + MediaUtils.setMediaItemsWithSpecifiedStartIndexAndPosition( + player, mediaItemsWithStartPosition); + } @Player.State int playbackState = player.getPlaybackState(); if (playbackState == Player.STATE_IDLE) { player.prepareIfCommandAvailable(); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 6ae74ccbbfc..ffd6e786214 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -63,6 +63,7 @@ import androidx.core.util.ObjectsCompat; import androidx.media.MediaSessionManager; import androidx.media3.common.BundleListRetriever; +import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.MediaMetadata; @@ -79,6 +80,7 @@ import androidx.media3.session.MediaLibraryService.MediaLibrarySession; import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; +import androidx.media3.session.MediaSession.MediaItemsWithStartPosition; import androidx.media3.session.SessionCommand.CommandCode; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; @@ -202,6 +204,30 @@ SessionTask, K> handleMediaItemsWhenReady( }; } + private static + SessionTask, K> handleMediaItemsWithStartPositionWhenReady( + SessionTask, K> mediaItemsTask, + MediaItemsWithStartPositionPlayerTask mediaItemPlayerTask) { + return (sessionImpl, controller, sequenceNumber) -> { + if (sessionImpl.isReleased()) { + return Futures.immediateFuture( + new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED)); + } + return transformFutureAsync( + mediaItemsTask.run(sessionImpl, controller, sequenceNumber), + mediaItemsWithStartPosition -> + postOrRunWithCompletion( + sessionImpl.getApplicationHandler(), + () -> { + if (!sessionImpl.isReleased()) { + mediaItemPlayerTask.run( + sessionImpl.getPlayerWrapper(), controller, mediaItemsWithStartPosition); + } + }, + new SessionResult(SessionResult.RESULT_SUCCESS))); + }; + } + private static void sendLibraryResult( ControllerInfo controller, int sequenceNumber, LibraryResult result) { try { @@ -851,26 +877,8 @@ public void setPlaybackParameters( @Override public void setMediaItem( @Nullable IMediaController caller, int sequenceNumber, @Nullable Bundle mediaItemBundle) { - if (caller == null || mediaItemBundle == null) { - return; - } - MediaItem mediaItem; - try { - mediaItem = MediaItem.CREATOR.fromBundle(mediaItemBundle); - } catch (RuntimeException e) { - Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e); - return; - } - queueSessionTaskWithPlayerCommand( - caller, - sequenceNumber, - COMMAND_SET_MEDIA_ITEM, - sendSessionResultWhenReady( - handleMediaItemsWhenReady( - (sessionImpl, controller, sequenceNum) -> - sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), - (playerWrapper, controller, mediaItems) -> - playerWrapper.setMediaItems(mediaItems)))); + setMediaItemWithResetPosition( + caller, sequenceNumber, mediaItemBundle, /* resetPosition= */ true); } @Override @@ -894,11 +902,35 @@ public void setMediaItemWithStartPosition( sequenceNumber, COMMAND_SET_MEDIA_ITEM, sendSessionResultWhenReady( - handleMediaItemsWhenReady( + handleMediaItemsWithStartPositionWhenReady( (sessionImpl, controller, sequenceNum) -> - sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), - (player, controller, mediaItems) -> - player.setMediaItems(mediaItems, /* startIndex= */ 0, startPositionMs)))); + sessionImpl.onSetMediaItemsOnHandler( + controller, + ImmutableList.of(mediaItem), + /* startIndex= */ 0, + startPositionMs), + (player, controller, mediaItemsWithStartPosition) -> { + if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { + if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET + && mediaItemsWithStartPosition.startPositionMs == C.TIME_UNSET) { + player.setMediaItems( + mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true); + } else { + player.setMediaItems( + mediaItemsWithStartPosition.mediaItems, + mediaItemsWithStartPosition.startIndex, + mediaItemsWithStartPosition.startPositionMs); + } + } else { + if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) { + player.setMediaItem( + mediaItemsWithStartPosition.mediaItems.get(0), + mediaItemsWithStartPosition.startPositionMs); + } else { + player.clearMediaItems(); + } + } + }))); } @Override @@ -922,11 +954,27 @@ public void setMediaItemWithResetPosition( sequenceNumber, COMMAND_SET_MEDIA_ITEM, sendSessionResultWhenReady( - handleMediaItemsWhenReady( + handleMediaItemsWithStartPositionWhenReady( (sessionImpl, controller, sequenceNum) -> - sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), - (player, controller, mediaItems) -> - player.setMediaItems(mediaItems, resetPosition)))); + sessionImpl.onSetMediaItemsOnHandler( + controller, + ImmutableList.of(mediaItem), + resetPosition + ? C.INDEX_UNSET + : sessionImpl.getPlayerWrapper().getCurrentMediaItemIndex(), + resetPosition + ? C.TIME_UNSET + : sessionImpl.getPlayerWrapper().getCurrentPosition()), + (player, controller, mediaItemsWithStartPosition) -> { + if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET + && mediaItemsWithStartPosition.startPositionMs == C.TIME_UNSET) { + MediaUtils.setMediaItemsWithDefaultStartIndexAndPosition( + player, mediaItemsWithStartPosition); + } else { + MediaUtils.setMediaItemsWithSpecifiedStartIndexAndPosition( + player, mediaItemsWithStartPosition); + } + }))); } @Override @@ -934,29 +982,8 @@ public void setMediaItems( @Nullable IMediaController caller, int sequenceNumber, @Nullable IBinder mediaItemsRetriever) { - if (caller == null || mediaItemsRetriever == null) { - return; - } - List mediaItemList; - try { - mediaItemList = - BundleableUtil.fromBundleList( - MediaItem.CREATOR, BundleListRetriever.getList(mediaItemsRetriever)); - } catch (RuntimeException e) { - Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e); - return; - } - - queueSessionTaskWithPlayerCommand( - caller, - sequenceNumber, - COMMAND_CHANGE_MEDIA_ITEMS, - sendSessionResultWhenReady( - handleMediaItemsWhenReady( - (sessionImpl, controller, sequenceNum) -> - sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList), - (playerWrapper, controller, mediaItems) -> - playerWrapper.setMediaItems(mediaItems)))); + setMediaItemsWithResetPosition( + caller, sequenceNumber, mediaItemsRetriever, /* resetPosition= */ true); } @Override @@ -982,11 +1009,29 @@ public void setMediaItemsWithResetPosition( sequenceNumber, COMMAND_CHANGE_MEDIA_ITEMS, sendSessionResultWhenReady( - handleMediaItemsWhenReady( + handleMediaItemsWithStartPositionWhenReady( (sessionImpl, controller, sequenceNum) -> - sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList), - (player, controller, mediaItems) -> - player.setMediaItems(mediaItems, resetPosition)))); + sessionImpl.onSetMediaItemsOnHandler( + controller, + mediaItemList, + resetPosition + ? C.INDEX_UNSET + : sessionImpl.getPlayerWrapper().getCurrentMediaItemIndex(), + resetPosition + ? C.TIME_UNSET + : sessionImpl.getPlayerWrapper().getCurrentPosition()), + (player, controller, mediaItemsWithStartPosition) -> { + if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET + && mediaItemsWithStartPosition.startPositionMs == C.TIME_UNSET) { + player.setMediaItems( + mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true); + } else { + player.setMediaItems( + mediaItemsWithStartPosition.mediaItems, + mediaItemsWithStartPosition.startIndex, + mediaItemsWithStartPosition.startPositionMs); + } + }))); } @Override @@ -1013,11 +1058,29 @@ public void setMediaItemsWithStartIndex( sequenceNumber, COMMAND_CHANGE_MEDIA_ITEMS, sendSessionResultWhenReady( - handleMediaItemsWhenReady( + handleMediaItemsWithStartPositionWhenReady( (sessionImpl, controller, sequenceNum) -> - sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList), - (player, controller, mediaItems) -> - player.setMediaItems(mediaItems, startIndex, startPositionMs)))); + sessionImpl.onSetMediaItemsOnHandler( + controller, + mediaItemList, + (startIndex == C.INDEX_UNSET) + ? sessionImpl.getPlayerWrapper().getCurrentMediaItemIndex() + : startIndex, + (startIndex == C.INDEX_UNSET) + ? sessionImpl.getPlayerWrapper().getCurrentPosition() + : startPositionMs), + (player, controller, mediaItemsWithStartPosition) -> { + if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET + && mediaItemsWithStartPosition.startPositionMs == C.TIME_UNSET) { + player.setMediaItems( + mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true); + } else { + player.setMediaItems( + mediaItemsWithStartPosition.mediaItems, + mediaItemsWithStartPosition.startIndex, + mediaItemsWithStartPosition.startPositionMs); + } + }))); } @Override @@ -1624,6 +1687,13 @@ private interface ControllerPlayerTask { void run(PlayerWrapper player, ControllerInfo controller); } + private interface MediaItemsWithStartPositionPlayerTask { + void run( + PlayerWrapper player, + ControllerInfo controller, + MediaItemsWithStartPosition mediaItemsWithStartPosition); + } + /* package */ static final class Controller2Cb implements ControllerCb { private final IMediaController iController; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 0d61696904f..6f3233b5205 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -1368,5 +1368,32 @@ public static int calculateBufferedPercentage(long bufferedPositionMs, long dura : Util.constrainValue((int) ((bufferedPositionMs * 100) / durationMs), 0, 100); } + public static void setMediaItemsWithDefaultStartIndexAndPosition( + PlayerWrapper player, MediaSession.MediaItemsWithStartPosition mediaItemsWithStartPosition) { + if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { + player.setMediaItems(mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true); + } else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) { + player.setMediaItem(mediaItemsWithStartPosition.mediaItems.get(0), /* resetPosition= */ true); + } else { + player.clearMediaItems(); + } + } + + public static void setMediaItemsWithSpecifiedStartIndexAndPosition( + PlayerWrapper player, MediaSession.MediaItemsWithStartPosition mediaItemsWithStartPosition) { + if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { + player.setMediaItems( + mediaItemsWithStartPosition.mediaItems, + mediaItemsWithStartPosition.startIndex, + mediaItemsWithStartPosition.startPositionMs); + } else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) { + player.setMediaItem( + mediaItemsWithStartPosition.mediaItems.get(0), + mediaItemsWithStartPosition.startPositionMs); + } else { + player.clearMediaItems(); + } + } + private MediaUtils() {} } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java index 1891ee001db..37ea204459d 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java @@ -29,6 +29,7 @@ import android.content.Context; import android.os.Bundle; import android.text.TextUtils; +import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.Player; @@ -368,14 +369,14 @@ public ListenableFuture> onAddMediaItems( controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItem(mediaItem); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(requestedMediaItems.get()).containsExactly(mediaItem); assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem)); } @Test - public void onAddMediaItems_withSetMediaItemWithIndex() throws Exception { + public void onAddMediaItems_withSetMediaItemWithStartPosition() throws Exception { MediaItem mediaItem = createMediaItem("mediaId"); AtomicReference> requestedMediaItems = new AtomicReference<>(); MediaSession.Callback callback = @@ -452,7 +453,7 @@ public ListenableFuture> onAddMediaItems( controllerTestRule.createRemoteController(session.getToken()); controller.setMediaItems(mediaItems); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder(); assertThat(player.mediaItems) @@ -461,7 +462,7 @@ public ListenableFuture> onAddMediaItems( } @Test - public void onAddMediaItems_withSetMediaItemsWithStartPosition() throws Exception { + public void onAddMediaItems_withSetMediaItemsWithStartIndex() throws Exception { List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); AtomicReference> requestedMediaItems = new AtomicReference<>(); MediaSession.Callback callback = @@ -645,6 +646,170 @@ public ListenableFuture> onAddMediaItems( assertThat(player.index).isEqualTo(1); } + @Test + public void onSetMediaItems_withSetMediaItemWithStartPosition_callsPlayerWithStartIndex() + throws Exception { + MediaItem mediaItem = createMediaItem("mediaId"); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture onSetMediaItems( + MediaSession mediaSession, + ControllerInfo controller, + List mediaItems, + int startIndex, + long startPositionMs) { + requestedMediaItems.set(mediaItems); + + return Futures.immediateFuture( + new MediaSession.MediaItemsWithStartPosition( + updateMediaItemsWithLocalConfiguration(mediaItems), + startIndex, + /* startPosition = testStartPosition * 2 */ 200)); + } + }; + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player).setCallback(callback).build()); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + + controller.setMediaItem(mediaItem, /* startPositionMs= */ 100); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); + + assertThat(requestedMediaItems.get()).containsExactly(mediaItem); + assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem)); + assertThat(player.startMediaItemIndex).isEqualTo(0); + assertThat(player.startPositionMs).isEqualTo(200); + } + + @Test + public void onSetMediaItems_withSetMediaItemsWithStartIndex_callsPlayerWithStartIndex() + throws Exception { + List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture onSetMediaItems( + MediaSession mediaSession, + ControllerInfo controller, + List mediaItems, + int startIndex, + long startPositionMs) { + requestedMediaItems.set(mediaItems); + + return Futures.immediateFuture( + new MediaSession.MediaItemsWithStartPosition( + updateMediaItemsWithLocalConfiguration(mediaItems), + startIndex, + /* startPosition= */ 200)); + } + }; + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player).setCallback(callback).build()); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + + controller.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 100); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); + + assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder(); + assertThat(player.mediaItems) + .containsExactlyElementsIn(updateMediaItemsWithLocalConfiguration(mediaItems)) + .inOrder(); + assertThat(player.startMediaItemIndex).isEqualTo(1); + assertThat(player.startPositionMs).isEqualTo(200); + } + + @Test + public void onSetMediaItems_withIndexPositionUnset_callsPlayerWithResetPosition() + throws Exception { + List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture onSetMediaItems( + MediaSession mediaSession, + ControllerInfo controller, + List mediaItems, + int startIndex, + long startPositionMs) { + requestedMediaItems.set(mediaItems); + + return Futures.immediateFuture( + new MediaSession.MediaItemsWithStartPosition( + updateMediaItemsWithLocalConfiguration(mediaItems), + C.INDEX_UNSET, + C.TIME_UNSET)); + } + }; + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player).setCallback(callback).build()); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + + controller.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 100); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); + + assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder(); + assertThat(player.mediaItems) + .containsExactlyElementsIn(updateMediaItemsWithLocalConfiguration(mediaItems)) + .inOrder(); + assertThat(player.resetPosition).isEqualTo(true); + } + + @Test + public void onSetMediaItems_withStartIndexUnset_callsPlayerWithCurrentIndexAndPosition() + throws Exception { + List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture onSetMediaItems( + MediaSession mediaSession, + ControllerInfo controller, + List mediaItems, + int startIndex, + long startPositionMs) { + requestedMediaItems.set(mediaItems); + + return Futures.immediateFuture( + new MediaSession.MediaItemsWithStartPosition( + updateMediaItemsWithLocalConfiguration(mediaItems), + startIndex, + startPositionMs)); + } + }; + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player).setCallback(callback).build()); + RemoteMediaController controller = + controllerTestRule.createRemoteController(session.getToken()); + controller.setMediaItems(mediaItems, true); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); + + // Model that player played to next item. Current media item index and position have changed + player.currentMediaItemIndex = 1; + player.currentPosition = 200; + + // Re-set media items with start index and position as current index and position + controller.setMediaItems(mediaItems, C.INDEX_UNSET, /* startPosition= */ 0); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); + + assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder(); + assertThat(player.mediaItems) + .containsExactlyElementsIn(updateMediaItemsWithLocalConfiguration(mediaItems)) + .inOrder(); + assertThat(player.startMediaItemIndex).isEqualTo(1); + assertThat(player.startPositionMs).isEqualTo(200); + } + @Test public void onConnect() throws Exception { AtomicReference connectionHints = new AtomicReference<>(); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java index 504c48ed639..fcdbe1ec5ac 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java @@ -961,7 +961,7 @@ public MediaSession.ConnectionResult onConnect( } @Test - public void prepareFromMediaUri() throws Exception { + public void prepareFromMediaUri_withOnAddMediaItems() throws Exception { Uri mediaUri = Uri.parse("foo://bar"); Bundle bundle = new Bundle(); bundle.putString("key", "value"); @@ -988,7 +988,7 @@ public ListenableFuture> onAddMediaItems( controller.getTransportControls().prepareFromUri(mediaUri, bundle); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); assertThat(requestedMediaItems.get()).hasSize(1); assertThat(requestedMediaItems.get().get(0).requestMetadata.mediaUri).isEqualTo(mediaUri); @@ -997,7 +997,7 @@ public ListenableFuture> onAddMediaItems( } @Test - public void playFromMediaUri() throws Exception { + public void playFromMediaUri_withOnAddMediaItems() throws Exception { Uri request = Uri.parse("foo://bar"); Bundle bundle = new Bundle(); bundle.putString("key", "value"); @@ -1024,7 +1024,7 @@ public ListenableFuture> onAddMediaItems( controller.getTransportControls().playFromUri(request, bundle); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); assertThat(requestedMediaItems.get()).hasSize(1); @@ -1034,7 +1034,7 @@ public ListenableFuture> onAddMediaItems( } @Test - public void prepareFromMediaId() throws Exception { + public void prepareFromMediaId_withOnAddMediaItems() throws Exception { String request = "media_id"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); @@ -1061,7 +1061,7 @@ public ListenableFuture> onAddMediaItems( controller.getTransportControls().prepareFromMediaId(request, bundle); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); assertThat(requestedMediaItems.get()).hasSize(1); assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(request); @@ -1070,7 +1070,53 @@ public ListenableFuture> onAddMediaItems( } @Test - public void playFromMediaId() throws Exception { + public void prepareFromMediaId_withOnSetMediaItems_callsPlayerWithStartIndex() throws Exception { + String request = "media_id"; + Bundle bundle = new Bundle(); + bundle.putString("key", "value"); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture onSetMediaItems( + MediaSession mediaSession, + ControllerInfo controller, + List mediaItems, + int startIndex, + long startPositionMs) { + requestedMediaItems.set(mediaItems); + return executorService.submit( + () -> + new MediaSession.MediaItemsWithStartPosition( + ImmutableList.of(resolvedMediaItem), + /* startIndex= */ 2, + /* startPositionMs= */ 100)); + } + }; + session = + new MediaSession.Builder(context, player) + .setId("prepareFromMediaId") + .setCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().prepareFromMediaId(request, bundle); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(request); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); + assertThat(player.startMediaItemIndex).isEqualTo(2); + assertThat(player.startPositionMs).isEqualTo(100); + } + + @Test + public void playFromMediaId_withOnAddMediaItems() throws Exception { String mediaId = "media_id"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); @@ -1097,7 +1143,7 @@ public ListenableFuture> onAddMediaItems( controller.getTransportControls().playFromMediaId(mediaId, bundle); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); assertThat(requestedMediaItems.get()).hasSize(1); @@ -1107,7 +1153,49 @@ public ListenableFuture> onAddMediaItems( } @Test - public void prepareFromSearch() throws Exception { + public void playFromMediaId_withOnSetMediaItems_callsPlayerWithStartIndex() throws Exception { + String mediaId = "media_id"; + Bundle bundle = new Bundle(); + bundle.putString("key", "value"); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture onSetMediaItems( + MediaSession mediaSession, + ControllerInfo controller, + List mediaItems, + int startIndex, + long startPositionMs) { + return executorService.submit( + () -> + new MediaSession.MediaItemsWithStartPosition( + ImmutableList.of(resolvedMediaItem), + /* startIndex= */ 2, + /* startPositionMs= */ 100)); + } + }; + session = + new MediaSession.Builder(context, player) + .setId("playFromMediaId") + .setCallback(callback) + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + + controller.getTransportControls().playFromMediaId(mediaId, bundle); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); + assertThat(player.startMediaItemIndex).isEqualTo(2); + assertThat(player.startPositionMs).isEqualTo(100); + } + + @Test + public void prepareFromSearch_withOnAddMediaItems() throws Exception { String query = "test_query"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); @@ -1134,7 +1222,7 @@ public ListenableFuture> onAddMediaItems( controller.getTransportControls().prepareFromSearch(query, bundle); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); assertThat(requestedMediaItems.get()).hasSize(1); assertThat(requestedMediaItems.get().get(0).requestMetadata.searchQuery).isEqualTo(query); @@ -1143,7 +1231,7 @@ public ListenableFuture> onAddMediaItems( } @Test - public void playFromSearch() throws Exception { + public void playFromSearch_withOnAddMediaItems() throws Exception { String query = "test_query"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); @@ -1170,7 +1258,7 @@ public ListenableFuture> onAddMediaItems( controller.getTransportControls().playFromSearch(query, bundle); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); assertThat(requestedMediaItems.get()).hasSize(1); @@ -1204,7 +1292,7 @@ public ListenableFuture> onAddMediaItems( controller.getTransportControls().prepareFromUri(Uri.parse("foo://bar"), Bundle.EMPTY); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @@ -1234,7 +1322,7 @@ public ListenableFuture> onAddMediaItems( controller.getTransportControls().playFromUri(Uri.parse("foo://bar"), Bundle.EMPTY); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); assertThat(player.mediaItems).containsExactly(resolvedMediaItem); @@ -1268,7 +1356,7 @@ public ListenableFuture> onAddMediaItems( controller.getTransportControls().playFromUri(Uri.parse("foo://bar"), Bundle.EMPTY); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse(); assertThat(player.mediaItems).containsExactly(resolvedMediaItem); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java index 2098f6ca291..6ebf428658b 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java @@ -300,7 +300,7 @@ public void setMediaItem() throws Exception { controller.setMediaItem(item); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(player.mediaItems).containsExactly(item); assertThat(player.startPositionMs).isEqualTo(startPositionMs); assertThat(player.resetPosition).isEqualTo(resetPosition); @@ -316,7 +316,7 @@ public void setMediaItem_withStartPosition() throws Exception { controller.setMediaItem(item); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(player.mediaItems).containsExactly(item); assertThat(player.startPositionMs).isEqualTo(startPositionMs); assertThat(player.resetPosition).isEqualTo(resetPosition); @@ -332,7 +332,7 @@ public void setMediaItem_withResetPosition() throws Exception { controller.setMediaItem(item); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(player.mediaItems).containsExactly(item); assertThat(player.startPositionMs).isEqualTo(startPositionMs); assertThat(player.resetPosition).isEqualTo(resetPosition); @@ -344,9 +344,9 @@ public void setMediaItems() throws Exception { controller.setMediaItems(items); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(player.mediaItems).isEqualTo(items); - assertThat(player.resetPosition).isFalse(); + assertThat(player.resetPosition).isTrue(); } @Test @@ -382,7 +382,7 @@ public void setMediaItems_withDuplicatedItems() throws Exception { controller.setMediaItems(list); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(player.mediaItems.size()).isEqualTo(listSize); for (int i = 0; i < listSize; i++) { assertThat(player.mediaItems.get(i).mediaId).isEqualTo(list.get(i).mediaId); @@ -395,7 +395,7 @@ public void setMediaItems_withLongPlaylist() throws Exception { // Make client app to generate a long list, and call setMediaItems() with it. controller.createAndSetFakeMediaItems(listSize); - player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(player.mediaItems).isNotNull(); assertThat(player.mediaItems.size()).isEqualTo(listSize); for (int i = 0; i < listSize; i++) { @@ -824,7 +824,7 @@ public void mixedAsyncAndSyncCommands_calledInCorrectOrder() throws Exception { controller.setMediaItemsPreparePlayAddItemsSeek(initialItems, addedItems, /* seekIndex= */ 3); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); boolean setMediaItemsCalledBeforePrepare = - player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS); + player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION); player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS); boolean addMediaItemsCalledBeforeSeek = player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS); From 2adcfd9b15850bd1185105284f5077c1b969232d Mon Sep 17 00:00:00 2001 From: christosts Date: Mon, 23 Jan 2023 17:58:28 +0000 Subject: [PATCH 121/141] Add missing # in release notes PiperOrigin-RevId: 504013985 (cherry picked from commit 5147011772286778e84410012a24e329fde12040) From 846258b69c12fb66fe947b910ab27d28fcab5514 Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Tue, 24 Jan 2023 15:45:44 +0000 Subject: [PATCH 122/141] Deduplicate onSetMediaItem handler logic Created unified MediaUtils method to handle various logic for calling Player.setMediaItems from MediaSessionStub and MediaSessionLegacyStub PiperOrigin-RevId: 504271877 (cherry picked from commit 7fbdbeb6cafe075f04b6a4321ef826643b3482e1) --- .../androidx/media3/session/MediaSession.java | 38 ++++------- .../session/MediaSessionLegacyStub.java | 10 +-- .../media3/session/MediaSessionStub.java | 67 ++----------------- .../androidx/media3/session/MediaUtils.java | 40 +++++------ 4 files changed, 39 insertions(+), 116 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 9037f9aae82..f3045d3cda1 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -1129,9 +1129,7 @@ default ListenableFuture> onAddMediaItems( * MediaItemsWithStartPosition} has been resolved, the session will call {@link * Player#setMediaItems} as requested. If the resolved {@link * MediaItemsWithStartPosition#startIndex startIndex} is {@link - * androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} and {@link - * MediaItemsWithStartPosition#startPositionMs startPositionMs} is {@link - * androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} then the session will call {@link + * androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} then the session will call {@link * Player#setMediaItem(MediaItem, boolean)} with {@code resetPosition} set to {@code true}. * *

          Interoperability: This method will be called in response to the following {@link @@ -1156,14 +1154,12 @@ default ListenableFuture> onAddMediaItems( * @param mediaSession The session for this event. * @param controller The controller information. * @param mediaItems The list of requested {@linkplain MediaItem media items}. - * @param startIndex The start index in the {@link MediaItem} list from which to start playing. - * If startIndex is {@link androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} and - * startPositionMs is {@link androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} then caller - * is requesting to set media items with default index and position. - * @param startPositionMs The starting position in the media item from where to start playing. - * If startIndex is {@link androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} and - * startPositionMs is {@link androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} then caller - * is requesting to set media items with default index and position. + * @param startIndex The start index in the {@link MediaItem} list from which to start playing, + * or {@link androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} to start playing from the + * default index in the playlist. + * @param startPositionMs The starting position in the media item from where to start playing, + * or {@link androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} to start playing from the + * default position in the media item. This value is ignored if startIndex is C.INDEX_UNSET * @return A {@link ListenableFuture} with a {@link MediaItemsWithStartPosition} containing a * list of resolved {@linkplain MediaItem media items}, and a starting index and position * that are playable by the underlying {@link Player}. If returned {@link @@ -1196,25 +1192,17 @@ public static final class MediaItemsWithStartPosition { /** * Index to start playing at in {@link MediaItem} list. * - *

          If startIndex is {@link androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} and - * startPositionMs is {@link androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} then the - * requested start is the default index and position. If only startIndex is {@link - * androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET}, then the requested start is the - * {@linkplain Player#getCurrentMediaItemIndex() current index} and {@linkplain - * Player#getContentPosition() position}. + *

          The start index in the {@link MediaItem} list from which to start playing, or {@link + * androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} to start playing from the default index + * in the playlist. */ public final int startIndex; /** * Position to start playing from in starting media item. * - *

          If startIndex is {@link androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} and - * startPositionMs is {@link androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} then the - * requested start is the default start index that takes into account whether {@link - * Player#getShuffleModeEnabled() shuffling is enabled} and the {@linkplain - * Timeline.Window#defaultPositionUs} default position}. If only startIndex is {@link - * androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET}, then the requested start is the - * {@linkplain Player#getCurrentMediaItemIndex() current index} and {@linkplain - * Player#getContentPosition() position}. + *

          The starting position in the media item from where to start playing, or {@link + * androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} to start playing from the default position + * in the media item. This value is ignored if startIndex is C.INDEX_UNSET */ public final long startPositionMs; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 0c467ac81e8..1e9caba13bf 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -724,14 +724,8 @@ public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) { sessionImpl.getApplicationHandler(), () -> { PlayerWrapper player = sessionImpl.getPlayerWrapper(); - if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET - && mediaItemsWithStartPosition.startPositionMs == C.TIME_UNSET) { - MediaUtils.setMediaItemsWithDefaultStartIndexAndPosition( - player, mediaItemsWithStartPosition); - } else { - MediaUtils.setMediaItemsWithSpecifiedStartIndexAndPosition( - player, mediaItemsWithStartPosition); - } + MediaUtils.setMediaItemsWithStartIndexAndPosition( + player, mediaItemsWithStartPosition); @Player.State int playbackState = player.getPlaybackState(); if (playbackState == Player.STATE_IDLE) { player.prepareIfCommandAvailable(); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index ffd6e786214..3c0b27a775d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -221,7 +221,7 @@ SessionTask, K> handleMediaItemsWithStartPositio () -> { if (!sessionImpl.isReleased()) { mediaItemPlayerTask.run( - sessionImpl.getPlayerWrapper(), controller, mediaItemsWithStartPosition); + sessionImpl.getPlayerWrapper(), mediaItemsWithStartPosition); } }, new SessionResult(SessionResult.RESULT_SUCCESS))); @@ -909,28 +909,7 @@ public void setMediaItemWithStartPosition( ImmutableList.of(mediaItem), /* startIndex= */ 0, startPositionMs), - (player, controller, mediaItemsWithStartPosition) -> { - if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { - if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET - && mediaItemsWithStartPosition.startPositionMs == C.TIME_UNSET) { - player.setMediaItems( - mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true); - } else { - player.setMediaItems( - mediaItemsWithStartPosition.mediaItems, - mediaItemsWithStartPosition.startIndex, - mediaItemsWithStartPosition.startPositionMs); - } - } else { - if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) { - player.setMediaItem( - mediaItemsWithStartPosition.mediaItems.get(0), - mediaItemsWithStartPosition.startPositionMs); - } else { - player.clearMediaItems(); - } - } - }))); + MediaUtils::setMediaItemsWithStartIndexAndPosition))); } @Override @@ -965,16 +944,7 @@ public void setMediaItemWithResetPosition( resetPosition ? C.TIME_UNSET : sessionImpl.getPlayerWrapper().getCurrentPosition()), - (player, controller, mediaItemsWithStartPosition) -> { - if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET - && mediaItemsWithStartPosition.startPositionMs == C.TIME_UNSET) { - MediaUtils.setMediaItemsWithDefaultStartIndexAndPosition( - player, mediaItemsWithStartPosition); - } else { - MediaUtils.setMediaItemsWithSpecifiedStartIndexAndPosition( - player, mediaItemsWithStartPosition); - } - }))); + MediaUtils::setMediaItemsWithStartIndexAndPosition))); } @Override @@ -1020,18 +990,7 @@ public void setMediaItemsWithResetPosition( resetPosition ? C.TIME_UNSET : sessionImpl.getPlayerWrapper().getCurrentPosition()), - (player, controller, mediaItemsWithStartPosition) -> { - if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET - && mediaItemsWithStartPosition.startPositionMs == C.TIME_UNSET) { - player.setMediaItems( - mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true); - } else { - player.setMediaItems( - mediaItemsWithStartPosition.mediaItems, - mediaItemsWithStartPosition.startIndex, - mediaItemsWithStartPosition.startPositionMs); - } - }))); + MediaUtils::setMediaItemsWithStartIndexAndPosition))); } @Override @@ -1069,18 +1028,7 @@ public void setMediaItemsWithStartIndex( (startIndex == C.INDEX_UNSET) ? sessionImpl.getPlayerWrapper().getCurrentPosition() : startPositionMs), - (player, controller, mediaItemsWithStartPosition) -> { - if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET - && mediaItemsWithStartPosition.startPositionMs == C.TIME_UNSET) { - player.setMediaItems( - mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true); - } else { - player.setMediaItems( - mediaItemsWithStartPosition.mediaItems, - mediaItemsWithStartPosition.startIndex, - mediaItemsWithStartPosition.startPositionMs); - } - }))); + MediaUtils::setMediaItemsWithStartIndexAndPosition))); } @Override @@ -1688,10 +1636,7 @@ private interface ControllerPlayerTask { } private interface MediaItemsWithStartPositionPlayerTask { - void run( - PlayerWrapper player, - ControllerInfo controller, - MediaItemsWithStartPosition mediaItemsWithStartPosition); + void run(PlayerWrapper player, MediaItemsWithStartPosition mediaItemsWithStartPosition); } /* package */ static final class Controller2Cb implements ControllerCb { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 6f3233b5205..fc983aa537e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -1368,30 +1368,26 @@ public static int calculateBufferedPercentage(long bufferedPositionMs, long dura : Util.constrainValue((int) ((bufferedPositionMs * 100) / durationMs), 0, 100); } - public static void setMediaItemsWithDefaultStartIndexAndPosition( + public static void setMediaItemsWithStartIndexAndPosition( PlayerWrapper player, MediaSession.MediaItemsWithStartPosition mediaItemsWithStartPosition) { - if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { - player.setMediaItems(mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true); - } else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) { - player.setMediaItem(mediaItemsWithStartPosition.mediaItems.get(0), /* resetPosition= */ true); - } else { - player.clearMediaItems(); - } - } - - public static void setMediaItemsWithSpecifiedStartIndexAndPosition( - PlayerWrapper player, MediaSession.MediaItemsWithStartPosition mediaItemsWithStartPosition) { - if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { - player.setMediaItems( - mediaItemsWithStartPosition.mediaItems, - mediaItemsWithStartPosition.startIndex, - mediaItemsWithStartPosition.startPositionMs); - } else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) { - player.setMediaItem( - mediaItemsWithStartPosition.mediaItems.get(0), - mediaItemsWithStartPosition.startPositionMs); + if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET) { + if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { + player.setMediaItems(mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true); + } else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) { + player.setMediaItem( + mediaItemsWithStartPosition.mediaItems.get(0), /* resetPosition= */ true); + } } else { - player.clearMediaItems(); + if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { + player.setMediaItems( + mediaItemsWithStartPosition.mediaItems, + mediaItemsWithStartPosition.startIndex, + mediaItemsWithStartPosition.startPositionMs); + } else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) { + player.setMediaItem( + mediaItemsWithStartPosition.mediaItems.get(0), + mediaItemsWithStartPosition.startPositionMs); + } } } From 207d67b7af676c6a5a11e9835d053f737e6cb7b5 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 24 Jan 2023 18:04:27 +0000 Subject: [PATCH 123/141] Suppress warnings in ImaUtil ImaUtil calls VideoProgressUpdate.equals() which is annotated as hidden, which causes lint errors with gradle. #minor-release PiperOrigin-RevId: 504306210 (cherry picked from commit 5f6e172c8fce652adf2c05e8f2d041c793e900ea) --- .../src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java index 2e900e7c6de..bd19af60f2b 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java @@ -270,6 +270,7 @@ public static Looper getImaLooper() { } /** Returns a human-readable representation of a video progress update. */ + @SuppressWarnings("RestrictedApi") // VideoProgressUpdate.equals() is annotated as hidden. public static String getStringForVideoProgressUpdate(VideoProgressUpdate videoProgressUpdate) { if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) { return "not ready"; From c357e67dd14f3db1c4cda96f34b2a418ffeaa41a Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 24 Jan 2023 18:06:24 +0000 Subject: [PATCH 124/141] Filter available commands based on PlaybackStateCompat actions This allows a MediaController to understand which methods calls are available on a legacy session. PiperOrigin-RevId: 504306806 (cherry picked from commit 067340cb0a03dede0f51425de00643fe3789baf2) --- .../session/MediaControllerImplLegacy.java | 11 +- .../androidx/media3/session/MediaUtils.java | 104 ++++- .../media3/test/session/common/TestUtils.java | 14 + .../media3/session/MediaUtilsTest.java | 399 +++++++++++++++++- 4 files changed, 499 insertions(+), 29 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index 489997beb8b..fc58fbcbc50 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -46,6 +46,7 @@ import android.view.TextureView; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; +import androidx.media.VolumeProviderCompat; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; @@ -1877,9 +1878,17 @@ private static ControllerInfo buildNewControllerInfo( // Note: Sets the available player command here although it can be obtained before session is // ready. It's to follow the decision on MediaController to disallow any commands before // connection is made. + int volumeControlType = + newLegacyPlayerInfo.playbackInfoCompat != null + ? newLegacyPlayerInfo.playbackInfoCompat.getVolumeControl() + : VolumeProviderCompat.VOLUME_CONTROL_FIXED; availablePlayerCommands = (oldControllerInfo.availablePlayerCommands == Commands.EMPTY) - ? MediaUtils.convertToPlayerCommands(sessionFlags, isSessionReady) + ? MediaUtils.convertToPlayerCommands( + newLegacyPlayerInfo.playbackStateCompat, + volumeControlType, + sessionFlags, + isSessionReady) : oldControllerInfo.availablePlayerCommands; PlaybackException playerError = diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index fc983aa537e..95f3e3bbf5a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -19,13 +19,17 @@ import static androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS; import static androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME; import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS; +import static androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES; import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME; import static androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA; import static androidx.media3.common.Player.COMMAND_GET_TIMELINE; import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE; import static androidx.media3.common.Player.COMMAND_PREPARE; +import static androidx.media3.common.Player.COMMAND_SEEK_BACK; +import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD; import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM; +import static androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION; import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT; import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; @@ -65,6 +69,7 @@ import androidx.annotation.Nullable; import androidx.media.AudioAttributesCompat; import androidx.media.MediaBrowserServiceCompat.BrowserRoot; +import androidx.media.VolumeProviderCompat; import androidx.media3.common.AdPlaybackState; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; @@ -1070,42 +1075,103 @@ public static List removeNullElements(List<@NullableType T> list) { } /** - * Converts {@link MediaControllerCompat#getFlags() session flags} and {@link - * MediaControllerCompat#isSessionReady whether session is ready} to {@link Player.Commands}. + * Converts {@link PlaybackStateCompat}, {@link + * MediaControllerCompat.PlaybackInfo#getVolumeControl() volume control type}, {@link + * MediaControllerCompat#getFlags() session flags} and {@link MediaControllerCompat#isSessionReady + * whether the session is ready} to {@link Player.Commands}. * - * @param sessionFlags The session flag. + * @param playbackStateCompat The {@link PlaybackStateCompat}. + * @param volumeControlType The {@link MediaControllerCompat.PlaybackInfo#getVolumeControl() + * volume control type}. + * @param sessionFlags The session flags. * @param isSessionReady Whether the session compat is ready. * @return The converted player commands. */ - public static Player.Commands convertToPlayerCommands(long sessionFlags, boolean isSessionReady) { + public static Player.Commands convertToPlayerCommands( + @Nullable PlaybackStateCompat playbackStateCompat, + int volumeControlType, + long sessionFlags, + boolean isSessionReady) { Commands.Builder playerCommandsBuilder = new Commands.Builder(); + long actions = playbackStateCompat == null ? 0 : playbackStateCompat.getActions(); + if ((hasAction(actions, PlaybackStateCompat.ACTION_PLAY) + && hasAction(actions, PlaybackStateCompat.ACTION_PAUSE)) + || hasAction(actions, PlaybackStateCompat.ACTION_PLAY_PAUSE)) { + playerCommandsBuilder.add(COMMAND_PLAY_PAUSE); + } + if (hasAction(actions, PlaybackStateCompat.ACTION_PREPARE)) { + playerCommandsBuilder.add(COMMAND_PREPARE); + } + if ((hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID) + && hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) + || (hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH) + && hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) + || (hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_URI) + && hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_URI))) { + // Require both PREPARE and PLAY actions as we have no logic to handle having just one action. + playerCommandsBuilder.addAll(COMMAND_SET_MEDIA_ITEM, COMMAND_PREPARE); + } + if (hasAction(actions, PlaybackStateCompat.ACTION_REWIND)) { + playerCommandsBuilder.add(COMMAND_SEEK_BACK); + } + if (hasAction(actions, PlaybackStateCompat.ACTION_FAST_FORWARD)) { + playerCommandsBuilder.add(COMMAND_SEEK_FORWARD); + } + if (hasAction(actions, PlaybackStateCompat.ACTION_SEEK_TO)) { + playerCommandsBuilder.addAll( + COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_TO_DEFAULT_POSITION); + } + if (hasAction(actions, PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) { + playerCommandsBuilder.addAll(COMMAND_SEEK_TO_NEXT, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM); + } + if (hasAction(actions, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) { + playerCommandsBuilder.addAll(COMMAND_SEEK_TO_PREVIOUS, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM); + } + if (hasAction(actions, PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED)) { + playerCommandsBuilder.add(COMMAND_SET_SPEED_AND_PITCH); + } + if (hasAction(actions, PlaybackStateCompat.ACTION_STOP)) { + playerCommandsBuilder.add(COMMAND_STOP); + } + if (volumeControlType == VolumeProviderCompat.VOLUME_CONTROL_RELATIVE) { + playerCommandsBuilder.add(COMMAND_ADJUST_DEVICE_VOLUME); + } else if (volumeControlType == VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE) { + playerCommandsBuilder.addAll(COMMAND_ADJUST_DEVICE_VOLUME, COMMAND_SET_DEVICE_VOLUME); + } playerCommandsBuilder.addAll( - COMMAND_PLAY_PAUSE, - COMMAND_PREPARE, - COMMAND_STOP, - COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, - COMMAND_SET_SPEED_AND_PITCH, COMMAND_GET_DEVICE_VOLUME, - COMMAND_SET_DEVICE_VOLUME, - COMMAND_ADJUST_DEVICE_VOLUME, COMMAND_GET_TIMELINE, - COMMAND_SEEK_TO_PREVIOUS, - COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, - COMMAND_SEEK_TO_NEXT, - COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_GET_CURRENT_MEDIA_ITEM, - COMMAND_SET_MEDIA_ITEM); - boolean includePlaylistCommands = (sessionFlags & FLAG_HANDLES_QUEUE_COMMANDS) != 0; - if (includePlaylistCommands) { + COMMAND_GET_AUDIO_ATTRIBUTES); + if ((sessionFlags & FLAG_HANDLES_QUEUE_COMMANDS) != 0) { playerCommandsBuilder.add(COMMAND_CHANGE_MEDIA_ITEMS); + if (hasAction(actions, PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) { + playerCommandsBuilder.add(Player.COMMAND_SEEK_TO_MEDIA_ITEM); + } } if (isSessionReady) { - playerCommandsBuilder.addAll(COMMAND_SET_SHUFFLE_MODE, COMMAND_SET_REPEAT_MODE); + if (hasAction(actions, PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) { + playerCommandsBuilder.add(COMMAND_SET_REPEAT_MODE); + } + if (hasAction(actions, PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) { + playerCommandsBuilder.add(COMMAND_SET_SHUFFLE_MODE); + } } return playerCommandsBuilder.build(); } + /** + * Checks if the set of actions contains the specified action. + * + * @param actions A bit set of actions. + * @param action The action to check. + * @return Whether the action is contained in the set. + */ + private static boolean hasAction(long actions, @PlaybackStateCompat.Actions long action) { + return (actions & action) != 0; + } + /** * Converts {@link PlaybackStateCompat} to {@link SessionCommands}. * diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/TestUtils.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/TestUtils.java index fe90b5bd04b..5a6e05df290 100644 --- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/TestUtils.java +++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/TestUtils.java @@ -153,6 +153,20 @@ static void setKeepScreenOn(Activity activity) { return list.build(); } + /** + * Returns an {@link ImmutableList} with the {@linkplain Player.Command Commands} contained in + * {@code commands}. The contents of the list are in matching order with the {@linkplain + * Player.Command Commands} returned by {@link Player.Commands#get(int)}. + */ + // TODO(b/254265256): Move this method off test-session-common. + public static ImmutableList<@Player.Command Integer> getCommandsAsList(Player.Commands commands) { + ImmutableList.Builder<@Player.Command Integer> list = new ImmutableList.Builder<>(); + for (int i = 0; i < commands.size(); i++) { + list.add(commands.get(i)); + } + return list.build(); + } + /** Returns the bytes of a scaled asset file. */ public static byte[] getByteArrayForScaledBitmap(Context context, String fileName) throws IOException { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java index 9511124a5b6..9b0864857a5 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java @@ -18,12 +18,12 @@ import static android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_BROWSABLE; import static android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABLE; import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION; -import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; import static androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS; import static androidx.media3.common.MimeTypes.AUDIO_AAC; import static androidx.media3.common.MimeTypes.VIDEO_H264; import static androidx.media3.common.MimeTypes.VIDEO_H265; import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; +import static androidx.media3.test.session.common.TestUtils.getCommandsAsList; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; @@ -43,6 +43,7 @@ import android.util.Pair; import androidx.annotation.Nullable; import androidx.media.AudioAttributesCompat; +import androidx.media.VolumeProviderCompat; import androidx.media.utils.MediaConstants; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; @@ -479,19 +480,399 @@ public void convertToSessionCommands_whenSessionIsNotReadyOnSdk21_disallowsRatin } @Test - public void convertToPlayerCommands() { - long sessionFlags = FLAG_HANDLES_QUEUE_COMMANDS; + public void convertToPlayerCommands_withNoActions_onlyDefaultCommandsAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder().setActions(/* capabilities= */ 0).build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)) + .containsExactly( + Player.COMMAND_GET_TIMELINE, + Player.COMMAND_GET_CURRENT_MEDIA_ITEM, + Player.COMMAND_GET_DEVICE_VOLUME, + Player.COMMAND_GET_MEDIA_ITEMS_METADATA, + Player.COMMAND_GET_AUDIO_ATTRIBUTES); + } + + @Test + public void convertToPlayerCommands_withJustPlayAction_playPauseCommandNotAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder().setActions(PlaybackStateCompat.ACTION_PLAY).build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).doesNotContain(Player.COMMAND_PLAY_PAUSE); + } + + @Test + public void convertToPlayerCommands_withJustPauseAction_playPauseCommandNotAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder().setActions(PlaybackStateCompat.ACTION_PAUSE).build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).doesNotContain(Player.COMMAND_PLAY_PAUSE); + } + + @Test + public void convertToPlayerCommands_withPlayAndPauseAction_playPauseCommandAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE) + .build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).contains(Player.COMMAND_PLAY_PAUSE); + } + + @Test + public void convertToPlayerCommands_withPlayPauseAction_playPauseCommandAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder().setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE).build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).contains(Player.COMMAND_PLAY_PAUSE); + } + + @Test + public void convertToPlayerCommands_withPrepareAction_prepareCommandAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder().setActions(PlaybackStateCompat.ACTION_PREPARE).build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).contains(Player.COMMAND_PREPARE); + } + + @Test + public void convertToPlayerCommands_withRewindAction_seekBackCommandAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder().setActions(PlaybackStateCompat.ACTION_REWIND).build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).contains(Player.COMMAND_SEEK_BACK); + } + + @Test + public void convertToPlayerCommands_withFastForwardAction_seekForwardCommandAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions(PlaybackStateCompat.ACTION_FAST_FORWARD) + .build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).contains(Player.COMMAND_SEEK_FORWARD); + } + + @Test + public void convertToPlayerCommands_withSeekToAction_seekInCurrentMediaItemCommandAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder().setActions(PlaybackStateCompat.ACTION_SEEK_TO).build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)) + .contains(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM); + } + + @Test + public void convertToPlayerCommands_withSkipToNextAction_seekToNextCommandsAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT) + .build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)) + .containsAtLeast(Player.COMMAND_SEEK_TO_NEXT, Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM); + } + + @Test + public void convertToPlayerCommands_withSkipToPreviousAction_seekToPreviousCommandsAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) + .build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)) + .containsAtLeast( + Player.COMMAND_SEEK_TO_PREVIOUS, Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM); + } + + @Test + public void + convertToPlayerCommands_withPlayFromActionsWithoutPrepareFromAction_setMediaItemCommandNotAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH + | PlaybackStateCompat.ACTION_PLAY_FROM_URI) + .build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)) + .containsNoneOf(Player.COMMAND_SET_MEDIA_ITEM, Player.COMMAND_PREPARE); + } + + @Test + public void + convertToPlayerCommands_withPrepareFromActionsWithoutPlayFromAction_setMediaItemCommandNotAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH + | PlaybackStateCompat.ACTION_PREPARE_FROM_URI) + .build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)) + .containsNoneOf(Player.COMMAND_SET_MEDIA_ITEM, Player.COMMAND_PREPARE); + } + + @Test + public void + convertToPlayerCommands_withPlayFromAndPrepareFromMediaId_setMediaItemPrepareAndPlayAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID) + .build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)) + .containsAtLeast(Player.COMMAND_SET_MEDIA_ITEM, Player.COMMAND_PREPARE); + } + + @Test + public void + convertToPlayerCommands_withPlayFromAndPrepareFromSearch_setMediaItemPrepareAndPlayAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH + | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH) + .build(); + Player.Commands playerCommands = - MediaUtils.convertToPlayerCommands(sessionFlags, /* isSessionReady= */ true); - assertThat(playerCommands.contains(Player.COMMAND_GET_TIMELINE)).isTrue(); + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)) + .containsAtLeast(Player.COMMAND_SET_MEDIA_ITEM, Player.COMMAND_PREPARE); } @Test - public void convertToPlayerCommands_whenSessionIsNotReady_disallowsShuffle() { - long sessionFlags = FLAG_HANDLES_QUEUE_COMMANDS; + public void + convertToPlayerCommands_withPlayFromAndPrepareFromUri_setMediaItemPrepareAndPlayAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY_FROM_URI + | PlaybackStateCompat.ACTION_PREPARE_FROM_URI) + .build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)) + .containsAtLeast(Player.COMMAND_SET_MEDIA_ITEM, Player.COMMAND_PREPARE); + } + + @Test + public void convertToPlayerCommands_withSetPlaybackSpeedAction_setSpeedCommandAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions(PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED) + .build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).contains(Player.COMMAND_SET_SPEED_AND_PITCH); + } + + @Test + public void convertToPlayerCommands_withStopAction_stopCommandAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder().setActions(PlaybackStateCompat.ACTION_STOP).build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).contains(Player.COMMAND_STOP); + } + + @Test + public void convertToPlayerCommands_withRelativeVolumeControl_adjustVolumeCommandAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder().setActions(/* capabilities= */ 0).build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_RELATIVE, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).contains(Player.COMMAND_ADJUST_DEVICE_VOLUME); + assertThat(getCommandsAsList(playerCommands)).doesNotContain(Player.COMMAND_SET_DEVICE_VOLUME); + } + + @Test + public void convertToPlayerCommands_withAbsoluteVolumeControl_adjustVolumeCommandAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder().setActions(/* capabilities= */ 0).build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)) + .containsAtLeast(Player.COMMAND_ADJUST_DEVICE_VOLUME, Player.COMMAND_SET_DEVICE_VOLUME); + } + + @Test + public void + convertToPlayerCommands_withShuffleRepeatActionsAndSessionReady_shuffleAndRepeatCommandsAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_SET_REPEAT_MODE + | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE) + .build(); + + Player.Commands playerCommands = + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)) + .containsAtLeast(Player.COMMAND_SET_REPEAT_MODE, Player.COMMAND_SET_SHUFFLE_MODE); + } + + @Test + public void + convertToPlayerCommands_withShuffleRepeatActionsAndSessionNotReady_shuffleAndRepeatCommandsNotAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_SET_REPEAT_MODE + | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE) + .build(); + Player.Commands playerCommands = - MediaUtils.convertToPlayerCommands(sessionFlags, /* isSessionReady= */ false); - assertThat(playerCommands.contains(Player.COMMAND_SET_SHUFFLE_MODE)).isFalse(); + MediaUtils.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ false); + + assertThat(getCommandsAsList(playerCommands)) + .containsNoneOf(Player.COMMAND_SET_REPEAT_MODE, Player.COMMAND_SET_SHUFFLE_MODE); } @Test From 3708e7529aba96a8258aaf29b0965eb7f033f440 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 25 Jan 2023 10:16:08 +0000 Subject: [PATCH 125/141] Publish gradle attributes for AndroidX compatibility These attributes are required when importing our artifacts into androidx-main in order to generate reference documentation (JavaDoc and KDoc). #minor-release PiperOrigin-RevId: 504502555 (cherry picked from commit 47349b8c4bd69415da8895061be71ef748c4a2d3) --- publish.gradle | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/publish.gradle b/publish.gradle index 6b2b0fcd76a..f7c9b01f5f9 100644 --- a/publish.gradle +++ b/publish.gradle @@ -26,10 +26,26 @@ afterEvaluate { publications { release(MavenPublication) { from components.release - artifact androidSourcesJar groupId = 'androidx.media3' artifactId = findProperty('releaseArtifactId') ?: '' version = findProperty('releaseVersion') ?: '' + configurations.create("sourcesElement") { variant -> + variant.visible = false + variant.canBeResolved = false + variant.attributes.attribute( + Usage.USAGE_ATTRIBUTE, + project.objects.named(Usage, Usage.JAVA_RUNTIME)) + variant.attributes.attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category, Category.DOCUMENTATION)) + variant.attributes.attribute( + Bundling.BUNDLING_ATTRIBUTE, + project.objects.named(Bundling, Bundling.EXTERNAL)) + variant.attributes.attribute( + DocsType.DOCS_TYPE_ATTRIBUTE, + project.objects.named(DocsType, DocsType.SOURCES)) + variant.outgoing.artifact(androidSourcesJar) + components.release.addVariantsFromConfiguration(variant) {} pom { name = findProperty('releaseName') From d6c9fdb2109831179380c3e0728197b4945c7e4c Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 25 Jan 2023 14:45:41 +0000 Subject: [PATCH 126/141] Add missing } to publish.gradle This was missed in https://github.com/androidx/media/commit/47349b8c4bd69415da8895061be71ef748c4a2d3 #minor-release PiperOrigin-RevId: 504548659 (cherry picked from commit 50beec56f4188e46f67e561ac4bb4ace5bb95089) --- publish.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/publish.gradle b/publish.gradle index f7c9b01f5f9..4b93f3806b6 100644 --- a/publish.gradle +++ b/publish.gradle @@ -46,6 +46,7 @@ afterEvaluate { project.objects.named(DocsType, DocsType.SOURCES)) variant.outgoing.artifact(androidSourcesJar) components.release.addVariantsFromConfiguration(variant) {} + } pom { name = findProperty('releaseName') From 55312e1257262f255e600d2040cb8a0ebfc93f1f Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 25 Jan 2023 17:56:13 +0000 Subject: [PATCH 127/141] Add missing command checks in UI module The commands are partly checked already before enabling features or calling player methods, but the checks were still missing in many places. #minor-release PiperOrigin-RevId: 504589888 (cherry picked from commit e2ece2f5bcda0cea436d782d58fa6f1d9a4d1f99) --- .../ui/DefaultMediaDescriptionAdapter.java | 11 ++ .../androidx/media3/ui/PlayerControlView.java | 137 ++++++++++++------ .../media3/ui/PlayerNotificationManager.java | 51 +++++-- .../java/androidx/media3/ui/PlayerView.java | 46 ++++-- .../ui/TrackSelectionDialogBuilder.java | 10 +- .../DefaultMediaDescriptionAdapterTest.java | 21 ++- 6 files changed, 208 insertions(+), 68 deletions(-) diff --git a/libraries/ui/src/main/java/androidx/media3/ui/DefaultMediaDescriptionAdapter.java b/libraries/ui/src/main/java/androidx/media3/ui/DefaultMediaDescriptionAdapter.java index 5c83902d048..85b5905d67c 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/DefaultMediaDescriptionAdapter.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/DefaultMediaDescriptionAdapter.java @@ -15,6 +15,8 @@ */ package androidx.media3.ui; +import static androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA; + import android.app.PendingIntent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -48,6 +50,9 @@ public DefaultMediaDescriptionAdapter(@Nullable PendingIntent pendingIntent) { @Override public CharSequence getCurrentContentTitle(Player player) { + if (!player.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)) { + return ""; + } @Nullable CharSequence displayTitle = player.getMediaMetadata().displayTitle; if (!TextUtils.isEmpty(displayTitle)) { return displayTitle; @@ -66,6 +71,9 @@ public PendingIntent createCurrentContentIntent(Player player) { @Nullable @Override public CharSequence getCurrentContentText(Player player) { + if (!player.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)) { + return null; + } @Nullable CharSequence artist = player.getMediaMetadata().artist; if (!TextUtils.isEmpty(artist)) { return artist; @@ -77,6 +85,9 @@ public CharSequence getCurrentContentText(Player player) { @Nullable @Override public Bitmap getCurrentLargeIcon(Player player, BitmapCallback callback) { + if (!player.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)) { + return null; + } @Nullable byte[] data = player.getMediaMetadata().artworkData; if (data == null) { return null; diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java index f5aa0dca5d5..ec24bba35d3 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java @@ -15,11 +15,22 @@ */ package androidx.media3.ui; +import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM; +import static androidx.media3.common.Player.COMMAND_GET_TIMELINE; +import static androidx.media3.common.Player.COMMAND_GET_TRACKS; +import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE; +import static androidx.media3.common.Player.COMMAND_PREPARE; import static androidx.media3.common.Player.COMMAND_SEEK_BACK; import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD; import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM; +import static androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION; +import static androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; +import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; +import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; +import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH; +import static androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS; import static androidx.media3.common.Player.EVENT_AVAILABLE_COMMANDS_CHANGED; import static androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED; import static androidx.media3.common.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED; @@ -35,6 +46,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.getDrawable; +import static androidx.media3.common.util.Util.msToUs; import android.annotation.SuppressLint; import android.content.Context; @@ -798,7 +810,7 @@ public void setShowTimeoutMs(int showTimeoutMs) { */ public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { this.repeatToggleModes = repeatToggleModes; - if (player != null) { + if (player != null && player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) { @Player.RepeatMode int currentMode = player.getRepeatMode(); if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE && currentMode != Player.REPEAT_MODE_OFF) { @@ -1062,7 +1074,7 @@ private void updateRepeatModeButton() { } @Nullable Player player = this.player; - if (player == null) { + if (player == null || !player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) { updateButton(/* enabled= */ false, repeatToggleButton); repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); @@ -1096,7 +1108,7 @@ private void updateShuffleButton() { @Nullable Player player = this.player; if (!controlViewLayoutManager.getShowButton(shuffleButton)) { updateButton(/* enabled= */ false, shuffleButton); - } else if (player == null) { + } else if (player == null || !player.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)) { updateButton(/* enabled= */ false, shuffleButton); shuffleButton.setImageDrawable(shuffleOffButtonDrawable); shuffleButton.setContentDescription(shuffleOffContentDescription); @@ -1120,8 +1132,8 @@ private void initTrackSelectionAdapter() { textTrackSelectionAdapter.clear(); audioTrackSelectionAdapter.clear(); if (player == null - || !player.isCommandAvailable(Player.COMMAND_GET_TRACKS) - || !player.isCommandAvailable(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { + || !player.isCommandAvailable(COMMAND_GET_TRACKS) + || !player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { return; } Tracks tracks = player.getCurrentTracks(); @@ -1162,12 +1174,14 @@ private void updateTimeline() { if (player == null) { return; } - multiWindowTimeBar = - showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); + multiWindowTimeBar = showMultiWindowTimeBar && canShowMultiWindowTimeBar(player, window); currentWindowOffset = 0; long durationUs = 0; int adGroupCount = 0; - Timeline timeline = player.getCurrentTimeline(); + Timeline timeline = + player.isCommandAvailable(COMMAND_GET_TIMELINE) + ? player.getCurrentTimeline() + : Timeline.EMPTY; if (!timeline.isEmpty()) { int currentWindowIndex = player.getCurrentMediaItemIndex(); int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex; @@ -1209,6 +1223,11 @@ private void updateTimeline() { } durationUs += window.durationUs; } + } else if (player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) { + long playerDurationMs = player.getContentDuration(); + if (playerDurationMs != C.TIME_UNSET) { + durationUs = msToUs(playerDurationMs); + } } long durationMs = Util.usToMs(durationUs); if (durationView != null) { @@ -1236,7 +1255,7 @@ private void updateProgress() { @Nullable Player player = this.player; long position = 0; long bufferedPosition = 0; - if (player != null) { + if (player != null && player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) { position = currentWindowOffset + player.getContentPosition(); bufferedPosition = currentWindowOffset + player.getContentBufferedPosition(); } @@ -1314,7 +1333,7 @@ private void displaySettingsWindow(RecyclerView.Adapter adapter, View anchorV } private void setPlaybackSpeed(float speed) { - if (player == null) { + if (player == null || !player.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)) { return; } player.setPlaybackParameters(player.getPlaybackParameters().withSpeed(speed)); @@ -1335,11 +1354,12 @@ private void updateButton(boolean enabled, @Nullable View view) { } private void seekToTimeBarPosition(Player player, long positionMs) { - int windowIndex; - Timeline timeline = player.getCurrentTimeline(); - if (multiWindowTimeBar && !timeline.isEmpty()) { + if (multiWindowTimeBar + && player.isCommandAvailable(COMMAND_GET_TIMELINE) + && player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)) { + Timeline timeline = player.getCurrentTimeline(); int windowCount = timeline.getWindowCount(); - windowIndex = 0; + int windowIndex = 0; while (true) { long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs(); if (positionMs < windowDurationMs) { @@ -1352,17 +1372,13 @@ private void seekToTimeBarPosition(Player player, long positionMs) { positionMs -= windowDurationMs; windowIndex++; } - } else { - windowIndex = player.getCurrentMediaItemIndex(); + player.seekTo(windowIndex, positionMs); + } else if (player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)) { + player.seekTo(positionMs); } - seekTo(player, windowIndex, positionMs); updateProgress(); } - private void seekTo(Player player, int windowIndex, long positionMs) { - player.seekTo(windowIndex, positionMs); - } - private void onFullScreenButtonClicked(View v) { if (onFullScreenModeChangedListener == null) { return; @@ -1440,10 +1456,12 @@ public boolean dispatchMediaKeyEvent(KeyEvent event) { } if (event.getAction() == KeyEvent.ACTION_DOWN) { if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - if (player.getPlaybackState() != Player.STATE_ENDED) { + if (player.getPlaybackState() != Player.STATE_ENDED + && player.isCommandAvailable(COMMAND_SEEK_FORWARD)) { player.seekForward(); } - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND + && player.isCommandAvailable(COMMAND_SEEK_BACK)) { player.seekBack(); } else if (event.getRepeatCount() == 0) { switch (keyCode) { @@ -1458,10 +1476,14 @@ public boolean dispatchMediaKeyEvent(KeyEvent event) { dispatchPause(player); break; case KeyEvent.KEYCODE_MEDIA_NEXT: - player.seekToNext(); + if (player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)) { + player.seekToNext(); + } break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - player.seekToPrevious(); + if (player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) { + player.seekToPrevious(); + } break; default: break; @@ -1501,7 +1523,10 @@ private void onLayoutChange( } private boolean shouldEnablePlayPauseButton() { - return player != null && !player.getCurrentTimeline().isEmpty(); + return player != null + && player.isCommandAvailable(COMMAND_PLAY_PAUSE) + && (!player.isCommandAvailable(COMMAND_GET_TIMELINE) + || !player.getCurrentTimeline().isEmpty()); } private boolean shouldShowPauseButton() { @@ -1522,16 +1547,21 @@ private void dispatchPlayPause(Player player) { private void dispatchPlay(Player player) { @State int state = player.getPlaybackState(); - if (state == Player.STATE_IDLE) { + if (state == Player.STATE_IDLE && player.isCommandAvailable(COMMAND_PREPARE)) { player.prepare(); - } else if (state == Player.STATE_ENDED) { - seekTo(player, player.getCurrentMediaItemIndex(), C.TIME_UNSET); + } else if (state == Player.STATE_ENDED + && player.isCommandAvailable(COMMAND_SEEK_TO_DEFAULT_POSITION)) { + player.seekToDefaultPosition(); + } + if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) { + player.play(); } - player.play(); } private void dispatchPause(Player player) { - player.pause(); + if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) { + player.pause(); + } } @SuppressLint("InlinedApi") @@ -1547,13 +1577,18 @@ private static boolean isHandledMediaKey(int keyCode) { } /** - * Returns whether the specified {@code timeline} can be shown on a multi-window time bar. + * Returns whether the specified {@code player} can be shown on a multi-window time bar. * - * @param timeline The {@link Timeline} to check. + * @param player The {@link Player} to check. * @param window A scratch {@link Timeline.Window} instance. * @return Whether the specified timeline can be shown on a multi-window time bar. */ - private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window) { + private static boolean canShowMultiWindowTimeBar(Player player, Timeline.Window window) { + if (!player.isCommandAvailable(COMMAND_GET_TIMELINE) + || !player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)) { + return false; + } + Timeline timeline = player.getCurrentTimeline(); if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { return false; } @@ -1674,22 +1709,33 @@ public void onClick(View view) { } controlViewLayoutManager.resetHideCallbacks(); if (nextButton == view) { - player.seekToNext(); + if (player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)) { + player.seekToNext(); + } } else if (previousButton == view) { - player.seekToPrevious(); + if (player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) { + player.seekToPrevious(); + } } else if (fastForwardButton == view) { - if (player.getPlaybackState() != Player.STATE_ENDED) { + if (player.getPlaybackState() != Player.STATE_ENDED + && player.isCommandAvailable(COMMAND_SEEK_FORWARD)) { player.seekForward(); } } else if (rewindButton == view) { - player.seekBack(); + if (player.isCommandAvailable(COMMAND_SEEK_BACK)) { + player.seekBack(); + } } else if (playPauseButton == view) { dispatchPlayPause(player); } else if (repeatToggleButton == view) { - player.setRepeatMode( - RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); + if (player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) { + player.setRepeatMode( + RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); + } } else if (shuffleButton == view) { - player.setShuffleModeEnabled(!player.getShuffleModeEnabled()); + if (player.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)) { + player.setShuffleModeEnabled(!player.getShuffleModeEnabled()); + } } else if (settingsButton == view) { controlViewLayoutManager.removeHideCallbacks(); displaySettingsWindow(settingsAdapter, settingsButton); @@ -1892,7 +1938,8 @@ public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) { holder.checkView.setVisibility(isTrackSelectionOff ? VISIBLE : INVISIBLE); holder.itemView.setOnClickListener( v -> { - if (player != null) { + if (player != null + && player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { TrackSelectionParameters trackSelectionParameters = player.getTrackSelectionParameters(); player.setTrackSelectionParameters( @@ -1933,7 +1980,8 @@ public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) { holder.checkView.setVisibility(hasSelectionOverride ? INVISIBLE : VISIBLE); holder.itemView.setOnClickListener( v -> { - if (player == null) { + if (player == null + || !player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { return; } TrackSelectionParameters trackSelectionParameters = @@ -2036,6 +2084,9 @@ public void onBindViewHolder(SubSettingViewHolder holder, int position) { holder.checkView.setVisibility(explicitlySelected ? VISIBLE : INVISIBLE); holder.itemView.setOnClickListener( v -> { + if (!player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { + return; + } TrackSelectionParameters trackSelectionParameters = player.getTrackSelectionParameters(); player.setTrackSelectionParameters( diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java index 30e610ed14f..f420489d61e 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java @@ -15,10 +15,17 @@ */ package androidx.media3.ui; +import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS; +import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM; +import static androidx.media3.common.Player.COMMAND_GET_TIMELINE; +import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE; +import static androidx.media3.common.Player.COMMAND_PREPARE; import static androidx.media3.common.Player.COMMAND_SEEK_BACK; import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD; +import static androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION; import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; +import static androidx.media3.common.Player.COMMAND_STOP; import static androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED; import static androidx.media3.common.Player.EVENT_MEDIA_METADATA_CHANGED; import static androidx.media3.common.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED; @@ -1205,7 +1212,9 @@ protected NotificationCompat.Builder createNotification( @Nullable NotificationCompat.Builder builder, boolean ongoing, @Nullable Bitmap largeIcon) { - if (player.getPlaybackState() == Player.STATE_IDLE && player.getCurrentTimeline().isEmpty()) { + if (player.getPlaybackState() == Player.STATE_IDLE + && player.isCommandAvailable(COMMAND_GET_TIMELINE) + && player.getCurrentTimeline().isEmpty()) { builderActions = null; return null; } @@ -1259,6 +1268,7 @@ protected NotificationCompat.Builder createNotification( // Changing "showWhen" causes notification flicker if SDK_INT < 21. if (Util.SDK_INT >= 21 && useChronometer + && player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM) && player.isPlaying() && !player.isPlayingAd() && !player.isCurrentMediaItemDynamic() @@ -1537,24 +1547,43 @@ public void onReceive(Context context, Intent intent) { } String action = intent.getAction(); if (ACTION_PLAY.equals(action)) { - if (player.getPlaybackState() == Player.STATE_IDLE) { + if (player.getPlaybackState() == Player.STATE_IDLE + && player.isCommandAvailable(COMMAND_PREPARE)) { player.prepare(); - } else if (player.getPlaybackState() == Player.STATE_ENDED) { - player.seekToDefaultPosition(player.getCurrentMediaItemIndex()); + } else if (player.getPlaybackState() == Player.STATE_ENDED + && player.isCommandAvailable(COMMAND_SEEK_TO_DEFAULT_POSITION)) { + player.seekToDefaultPosition(); + } + if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) { + player.play(); } - player.play(); } else if (ACTION_PAUSE.equals(action)) { - player.pause(); + if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) { + player.pause(); + } } else if (ACTION_PREVIOUS.equals(action)) { - player.seekToPrevious(); + if (player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) { + player.seekToPrevious(); + } } else if (ACTION_REWIND.equals(action)) { - player.seekBack(); + if (player.isCommandAvailable(COMMAND_SEEK_BACK)) { + player.seekBack(); + } } else if (ACTION_FAST_FORWARD.equals(action)) { - player.seekForward(); + if (player.isCommandAvailable(COMMAND_SEEK_FORWARD)) { + player.seekForward(); + } } else if (ACTION_NEXT.equals(action)) { - player.seekToNext(); + if (player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)) { + player.seekToNext(); + } } else if (ACTION_STOP.equals(action)) { - player.stop(/* reset= */ true); + if (player.isCommandAvailable(COMMAND_STOP)) { + player.stop(); + } + if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { + player.clearMediaItems(); + } } else if (ACTION_DISMISS.equals(action)) { stopNotification(/* dismissedByUser= */ true); } else if (action != null diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java index b91d14cb5fc..702ec7efdb0 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java @@ -15,7 +15,11 @@ */ package androidx.media3.ui; +import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM; +import static androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA; import static androidx.media3.common.Player.COMMAND_GET_TEXT; +import static androidx.media3.common.Player.COMMAND_GET_TIMELINE; +import static androidx.media3.common.Player.COMMAND_GET_TRACKS; import static androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.getDrawable; @@ -527,10 +531,12 @@ public void setPlayer(@Nullable Player player) { @Nullable Player oldPlayer = this.player; if (oldPlayer != null) { oldPlayer.removeListener(componentListener); - if (surfaceView instanceof TextureView) { - oldPlayer.clearVideoTextureView((TextureView) surfaceView); - } else if (surfaceView instanceof SurfaceView) { - oldPlayer.clearVideoSurfaceView((SurfaceView) surfaceView); + if (oldPlayer.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)) { + if (surfaceView instanceof TextureView) { + oldPlayer.clearVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SurfaceView) { + oldPlayer.clearVideoSurfaceView((SurfaceView) surfaceView); + } } } if (subtitleView != null) { @@ -743,7 +749,9 @@ public void setCustomErrorMessage(@Nullable CharSequence message) { @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (player != null && player.isPlayingAd()) { + if (player != null + && player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM) + && player.isPlayingAd()) { return super.dispatchKeyEvent(event); } @@ -1274,7 +1282,8 @@ private boolean shouldShowControllerIndefinitely() { } int playbackState = player.getPlaybackState(); return controllerAutoShow - && !player.getCurrentTimeline().isEmpty() + && (!player.isCommandAvailable(COMMAND_GET_TIMELINE) + || !player.getCurrentTimeline().isEmpty()) && (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED || !checkNotNull(player).getPlayWhenReady()); @@ -1289,12 +1298,17 @@ private void showController(boolean showIndefinitely) { } private boolean isPlayingAd() { - return player != null && player.isPlayingAd() && player.getPlayWhenReady(); + return player != null + && player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM) + && player.isPlayingAd() + && player.getPlayWhenReady(); } private void updateForCurrentTrackSelections(boolean isNewPlayer) { @Nullable Player player = this.player; - if (player == null || player.getCurrentTracks().isEmpty()) { + if (player == null + || !player.isCommandAvailable(COMMAND_GET_TRACKS) + || player.getCurrentTracks().isEmpty()) { if (!keepContentOnPlayerReset) { hideArtwork(); closeShutter(); @@ -1318,7 +1332,7 @@ private void updateForCurrentTrackSelections(boolean isNewPlayer) { closeShutter(); // Display artwork if enabled and available, else hide it. if (useArtwork()) { - if (setArtworkFromMediaMetadata(player.getMediaMetadata())) { + if (setArtworkFromMediaMetadata(player)) { return; } if (setDrawableArtwork(defaultArtwork)) { @@ -1330,7 +1344,11 @@ private void updateForCurrentTrackSelections(boolean isNewPlayer) { } @RequiresNonNull("artworkView") - private boolean setArtworkFromMediaMetadata(MediaMetadata mediaMetadata) { + private boolean setArtworkFromMediaMetadata(Player player) { + if (!player.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)) { + return false; + } + MediaMetadata mediaMetadata = player.getMediaMetadata(); if (mediaMetadata.artworkData == null) { return false; } @@ -1549,10 +1567,14 @@ public void onTracksChanged(Tracks tracks) { // is necessary to avoid closing the shutter when such a transition occurs. See: // https://github.com/google/ExoPlayer/issues/5507. Player player = checkNotNull(PlayerView.this.player); - Timeline timeline = player.getCurrentTimeline(); + Timeline timeline = + player.isCommandAvailable(COMMAND_GET_TIMELINE) + ? player.getCurrentTimeline() + : Timeline.EMPTY; if (timeline.isEmpty()) { lastPeriodUidWithTracks = null; - } else if (!player.getCurrentTracks().isEmpty()) { + } else if (player.isCommandAvailable(COMMAND_GET_TRACKS) + && !player.getCurrentTracks().isEmpty()) { lastPeriodUidWithTracks = timeline.getPeriod(player.getCurrentPeriodIndex(), period, /* setIds= */ true).uid; } else if (lastPeriodUidWithTracks != null) { diff --git a/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionDialogBuilder.java b/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionDialogBuilder.java index 6cacef3ceb7..9455b89504b 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionDialogBuilder.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionDialogBuilder.java @@ -15,6 +15,9 @@ */ package androidx.media3.ui; +import static androidx.media3.common.Player.COMMAND_GET_TRACKS; +import static androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS; + import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; @@ -102,7 +105,9 @@ public TrackSelectionDialogBuilder( Context context, CharSequence title, Player player, @C.TrackType int trackType) { this.context = context; this.title = title; - List allTrackGroups = player.getCurrentTracks().getGroups(); + Tracks tracks = + player.isCommandAvailable(COMMAND_GET_TRACKS) ? player.getCurrentTracks() : Tracks.EMPTY; + List allTrackGroups = tracks.getGroups(); trackGroups = new ArrayList<>(); for (int i = 0; i < allTrackGroups.size(); i++) { Tracks.Group trackGroup = allTrackGroups.get(i); @@ -113,6 +118,9 @@ public TrackSelectionDialogBuilder( overrides = player.getTrackSelectionParameters().overrides; callback = (isDisabled, overrides) -> { + if (!player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { + return; + } TrackSelectionParameters.Builder parametersBuilder = player.getTrackSelectionParameters().buildUpon(); parametersBuilder.setTrackTypeDisabled(trackType, isDisabled); diff --git a/libraries/ui/src/test/java/androidx/media3/ui/DefaultMediaDescriptionAdapterTest.java b/libraries/ui/src/test/java/androidx/media3/ui/DefaultMediaDescriptionAdapterTest.java index 895fcd22d86..3f295789b0c 100644 --- a/libraries/ui/src/test/java/androidx/media3/ui/DefaultMediaDescriptionAdapterTest.java +++ b/libraries/ui/src/test/java/androidx/media3/ui/DefaultMediaDescriptionAdapterTest.java @@ -34,7 +34,7 @@ public class DefaultMediaDescriptionAdapterTest { @Test - public void getters_returnMediaMetadataValues() { + public void getters_withGetMetatadataCommandAvailable_returnMediaMetadataValues() { Context context = ApplicationProvider.getApplicationContext(); Player player = mock(Player.class); MediaMetadata mediaMetadata = @@ -43,6 +43,7 @@ public void getters_returnMediaMetadataValues() { PendingIntent.getActivity(context, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE); DefaultMediaDescriptionAdapter adapter = new DefaultMediaDescriptionAdapter(pendingIntent); + when(player.isCommandAvailable(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)).thenReturn(true); when(player.getMediaMetadata()).thenReturn(mediaMetadata); assertThat(adapter.createCurrentContentIntent(player)).isEqualTo(pendingIntent); @@ -51,4 +52,22 @@ public void getters_returnMediaMetadataValues() { assertThat(adapter.getCurrentContentText(player).toString()) .isEqualTo(mediaMetadata.artist.toString()); } + + @Test + public void getters_withoutGetMetatadataCommandAvailable_returnMediaMetadataValues() { + Context context = ApplicationProvider.getApplicationContext(); + Player player = mock(Player.class); + MediaMetadata mediaMetadata = + new MediaMetadata.Builder().setDisplayTitle("display title").setArtist("artist").build(); + PendingIntent pendingIntent = + PendingIntent.getActivity(context, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE); + DefaultMediaDescriptionAdapter adapter = new DefaultMediaDescriptionAdapter(pendingIntent); + + when(player.isCommandAvailable(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)).thenReturn(false); + when(player.getMediaMetadata()).thenReturn(mediaMetadata); + + assertThat(adapter.createCurrentContentIntent(player)).isEqualTo(pendingIntent); + assertThat(adapter.getCurrentContentTitle(player).toString()).isEqualTo(""); + assertThat(adapter.getCurrentContentText(player)).isNull(); + } } From 5e6f79ae63b7e18249239cc6090222f89aab187e Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 27 Jan 2023 09:50:24 +0000 Subject: [PATCH 128/141] Tweak UI behavior when commands are missing. For most missing commands, we already disable the corresponding controls. This change extends this to more UI elements that are disabled in case the corresponding action is unavailable. #minor-release PiperOrigin-RevId: 505057751 (cherry picked from commit b3e7696ba7d66a2d3c477858194a20789f4d75c7) --- .../androidx/media3/ui/PlayerControlView.java | 103 +++++++++++++----- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java index ec24bba35d3..d5168952d99 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java @@ -1010,7 +1010,10 @@ private void updateNavigation() { boolean enableFastForward = false; boolean enableNext = false; if (player != null) { - enableSeeking = player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM); + enableSeeking = + (showMultiWindowTimeBar && canShowMultiWindowTimeBar(player, window)) + ? player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM) + : player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM); enablePrevious = player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS); enableRewind = player.isCommandAvailable(COMMAND_SEEK_BACK); enableFastForward = player.isCommandAvailable(COMMAND_SEEK_FORWARD); @@ -1126,6 +1129,7 @@ private void updateShuffleButton() { private void updateTrackLists() { initTrackSelectionAdapter(); updateButton(textTrackSelectionAdapter.getItemCount() > 0, subtitleButton); + updateSettingsButton(); } private void initTrackSelectionAdapter() { @@ -1301,6 +1305,11 @@ private void updatePlaybackSpeedList() { playbackSpeedAdapter.updateSelectedIndex(player.getPlaybackParameters().speed); settingsAdapter.setSubTextAtPosition( SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedAdapter.getSelectedText()); + updateSettingsButton(); + } + + private void updateSettingsButton() { + updateButton(settingsAdapter.hasSettingsToShow(), settingsButton); } private void updateSettingsWindowSize() { @@ -1354,25 +1363,26 @@ private void updateButton(boolean enabled, @Nullable View view) { } private void seekToTimeBarPosition(Player player, long positionMs) { - if (multiWindowTimeBar - && player.isCommandAvailable(COMMAND_GET_TIMELINE) - && player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)) { - Timeline timeline = player.getCurrentTimeline(); - int windowCount = timeline.getWindowCount(); - int windowIndex = 0; - while (true) { - long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs(); - if (positionMs < windowDurationMs) { - break; - } else if (windowIndex == windowCount - 1) { - // Seeking past the end of the last window should seek to the end of the timeline. - positionMs = windowDurationMs; - break; + if (multiWindowTimeBar) { + if (player.isCommandAvailable(COMMAND_GET_TIMELINE) + && player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)) { + Timeline timeline = player.getCurrentTimeline(); + int windowCount = timeline.getWindowCount(); + int windowIndex = 0; + while (true) { + long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs(); + if (positionMs < windowDurationMs) { + break; + } else if (windowIndex == windowCount - 1) { + // Seeking past the end of the last window should seek to the end of the timeline. + positionMs = windowDurationMs; + break; + } + positionMs -= windowDurationMs; + windowIndex++; } - positionMs -= windowDurationMs; - windowIndex++; + player.seekTo(windowIndex, positionMs); } - player.seekTo(windowIndex, positionMs); } else if (player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)) { player.seekTo(positionMs); } @@ -1584,15 +1594,14 @@ private static boolean isHandledMediaKey(int keyCode) { * @return Whether the specified timeline can be shown on a multi-window time bar. */ private static boolean canShowMultiWindowTimeBar(Player player, Timeline.Window window) { - if (!player.isCommandAvailable(COMMAND_GET_TIMELINE) - || !player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)) { + if (!player.isCommandAvailable(COMMAND_GET_TIMELINE)) { return false; } Timeline timeline = player.getCurrentTimeline(); - if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { + int windowCount = timeline.getWindowCount(); + if (windowCount <= 1 || windowCount > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { return false; } - int windowCount = timeline.getWindowCount(); for (int i = 0; i < windowCount; i++) { if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) { return false; @@ -1635,17 +1644,24 @@ private final class ComponentListener @Override public void onEvents(Player player, Events events) { - if (events.containsAny(EVENT_PLAYBACK_STATE_CHANGED, EVENT_PLAY_WHEN_READY_CHANGED)) { + if (events.containsAny( + EVENT_PLAYBACK_STATE_CHANGED, + EVENT_PLAY_WHEN_READY_CHANGED, + EVENT_AVAILABLE_COMMANDS_CHANGED)) { updatePlayPauseButton(); } if (events.containsAny( - EVENT_PLAYBACK_STATE_CHANGED, EVENT_PLAY_WHEN_READY_CHANGED, EVENT_IS_PLAYING_CHANGED)) { + EVENT_PLAYBACK_STATE_CHANGED, + EVENT_PLAY_WHEN_READY_CHANGED, + EVENT_IS_PLAYING_CHANGED, + EVENT_AVAILABLE_COMMANDS_CHANGED)) { updateProgress(); } - if (events.contains(EVENT_REPEAT_MODE_CHANGED)) { + if (events.containsAny(EVENT_REPEAT_MODE_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { updateRepeatModeButton(); } - if (events.contains(EVENT_SHUFFLE_MODE_ENABLED_CHANGED)) { + if (events.containsAny( + EVENT_SHUFFLE_MODE_ENABLED_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { updateShuffleButton(); } if (events.containsAny( @@ -1658,13 +1674,14 @@ public void onEvents(Player player, Events events) { EVENT_AVAILABLE_COMMANDS_CHANGED)) { updateNavigation(); } - if (events.containsAny(EVENT_POSITION_DISCONTINUITY, EVENT_TIMELINE_CHANGED)) { + if (events.containsAny( + EVENT_POSITION_DISCONTINUITY, EVENT_TIMELINE_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { updateTimeline(); } - if (events.contains(EVENT_PLAYBACK_PARAMETERS_CHANGED)) { + if (events.containsAny(EVENT_PLAYBACK_PARAMETERS_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { updatePlaybackSpeedList(); } - if (events.contains(EVENT_TRACKS_CHANGED)) { + if (events.containsAny(EVENT_TRACKS_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { updateTrackLists(); } } @@ -1774,6 +1791,14 @@ public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { @Override public void onBindViewHolder(SettingViewHolder holder, int position) { + if (shouldShowSetting(position)) { + holder.itemView.setLayoutParams( + new RecyclerView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + } else { + holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(0, 0)); + } + holder.mainTextView.setText(mainTexts[position]); if (subTexts[position] == null) { @@ -1802,6 +1827,26 @@ public int getItemCount() { public void setSubTextAtPosition(int position, String subText) { this.subTexts[position] = subText; } + + public boolean hasSettingsToShow() { + return shouldShowSetting(SETTINGS_AUDIO_TRACK_SELECTION_POSITION) + || shouldShowSetting(SETTINGS_PLAYBACK_SPEED_POSITION); + } + + private boolean shouldShowSetting(int position) { + if (player == null) { + return false; + } + switch (position) { + case SETTINGS_AUDIO_TRACK_SELECTION_POSITION: + return player.isCommandAvailable(COMMAND_GET_TRACKS) + && player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS); + case SETTINGS_PLAYBACK_SPEED_POSITION: + return player.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH); + default: + return true; + } + } } private final class SettingViewHolder extends RecyclerView.ViewHolder { From c37442b24d955e7cb70212f37cf3510b3522623e Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Fri, 27 Jan 2023 11:25:33 +0000 Subject: [PATCH 129/141] Match MergingMediaPeriod track selection by period index in id MergingMediaPeriod creates its track groups with ids concatenating position in its periods array and the underlying child track group id. The ids can be used in selectTracks for matching to periods list. Issue: google/ExoPlayer#10930 PiperOrigin-RevId: 505074653 (cherry picked from commit 542a1ef03f361b29ec731a7334b2922cb54ef4c9) --- .../exoplayer/source/MergingMediaPeriod.java | 14 +++----- .../source/MergingMediaPeriodTest.java | 33 +++++++++++++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java index c71b5cffd05..1cf6a3734f8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java @@ -118,17 +118,13 @@ public long selectTracks( for (int i = 0; i < selections.length; i++) { Integer streamChildIndex = streams[i] == null ? null : streamPeriodIndices.get(streams[i]); streamChildIndices[i] = streamChildIndex == null ? C.INDEX_UNSET : streamChildIndex; - selectionChildIndices[i] = C.INDEX_UNSET; if (selections[i] != null) { TrackGroup mergedTrackGroup = selections[i].getTrackGroup(); - TrackGroup childTrackGroup = - checkNotNull(childTrackGroupByMergedTrackGroup.get(mergedTrackGroup)); - for (int j = 0; j < periods.length; j++) { - if (periods[j].getTrackGroups().indexOf(childTrackGroup) != C.INDEX_UNSET) { - selectionChildIndices[i] = j; - break; - } - } + // mergedTrackGroup.id is 'periods array index' + ":" + childTrackGroup.id + selectionChildIndices[i] = + Integer.parseInt(mergedTrackGroup.id.substring(0, mergedTrackGroup.id.indexOf(":"))); + } else { + selectionChildIndices[i] = C.INDEX_UNSET; } } streamPeriodIndices.clear(); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/MergingMediaPeriodTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/MergingMediaPeriodTest.java index 740e468c239..2ef9252c648 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/MergingMediaPeriodTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/MergingMediaPeriodTest.java @@ -198,6 +198,39 @@ public void selectTracks_withSameArguments_forwardsEqualSelectionsToChildSources assertThat(firstSelectionChild2).isEqualTo(secondSelectionChild2); } + // https://github.com/google/ExoPlayer/issues/10930 + @Test + public void selectTracks_withIdenticalFormats_selectsMatchingPeriod() throws Exception { + MergingMediaPeriod mergingMediaPeriod = + prepareMergingPeriod( + new MergingPeriodDefinition( + /* timeOffsetUs= */ 0, /* singleSampleTimeUs= */ 123_000, childFormat11), + new MergingPeriodDefinition( + /* timeOffsetUs= */ -3000, /* singleSampleTimeUs= */ 456_000, childFormat11)); + + ExoTrackSelection[] selectionArray = { + new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(1), /* track= */ 0) + }; + + SampleStream[] streams = new SampleStream[1]; + mergingMediaPeriod.selectTracks( + selectionArray, + /* mayRetainStreamFlags= */ new boolean[2], + streams, + /* streamResetFlags= */ new boolean[2], + /* positionUs= */ 0); + mergingMediaPeriod.continueLoading(/* positionUs= */ 0); + + FormatHolder formatHolder = new FormatHolder(); + DecoderInputBuffer inputBuffer = + new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + streams[0].readData(formatHolder, inputBuffer, FLAG_REQUIRE_FORMAT); + + assertThat(streams[0].readData(formatHolder, inputBuffer, /* readFlags= */ 0)) + .isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.timeUs).isEqualTo(456_000 - 3000); + } + private MergingMediaPeriod prepareMergingPeriod(MergingPeriodDefinition... definitions) throws Exception { MediaPeriod[] mediaPeriods = new MediaPeriod[definitions.length]; From bcdedb719d331587ad580e44158eda465acf97c3 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 27 Jan 2023 11:55:14 +0000 Subject: [PATCH 130/141] Double tap detection for Bluetooth media button events only Issue: androidx/media#233 #minor-release PiperOrigin-RevId: 505078751 (cherry picked from commit 5c82d6bc18429842160bb64a851bb1ab5c89ec39) --- RELEASENOTES.md | 2 ++ .../androidx/media3/session/MediaSessionLegacyStub.java | 8 +++++++- .../media3/session/MediaSessionKeyEventTest.java | 9 --------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5ceebf14d9f..cdd6711554f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -49,6 +49,8 @@ * Add `onSetMediaItems` callback listener to provide means to modify/set `MediaItem` list, starting index and position by session before setting onto Player ([#156](https://github.com/androidx/media/issues/156)). + * Avoid double tap detection for non-Bluetooth media button events + ([#233](https://github.com/androidx/media/issues/233)). * Metadata: * Parse multiple null-separated values from ID3 frames, as permitted by ID3 v2.4. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 1e9caba13bf..b7aa79e8cfe 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -121,6 +121,7 @@ private final ConnectionTimeoutHandler connectionTimeoutHandler; private final MediaPlayPauseKeyHandler mediaPlayPauseKeyHandler; private final MediaSessionCompat sessionCompat; + private final String appPackageName; @Nullable private VolumeProviderCompat volumeProviderCompat; private volatile long connectionTimeoutMs; @@ -133,6 +134,7 @@ public MediaSessionLegacyStub( Handler handler) { sessionImpl = session; Context context = sessionImpl.getContext(); + appPackageName = context.getPackageName(); sessionManager = MediaSessionManager.getSessionManager(context); controllerLegacyCbForBroadcast = new ControllerLegacyCbForBroadcast(); connectionTimeoutHandler = @@ -225,7 +227,11 @@ public boolean onMediaButtonEvent(Intent mediaButtonEvent) { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_HEADSETHOOK: - if (keyEvent.getRepeatCount() == 0) { + // Double tap detection only for media button events from external sources (for instance + // Bluetooth). Media button events from the app package are coming from the notification + // below targetApiLevel 33. + if (!appPackageName.equals(remoteUserInfo.getPackageName()) + && keyEvent.getRepeatCount() == 0) { if (mediaPlayPauseKeyHandler.hasPendingMediaPlayPauseKey()) { mediaPlayPauseKeyHandler.clearPendingMediaPlayPauseKey(); onSkipToNext(); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java index 245f9449728..a4733f64495 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java @@ -237,15 +237,6 @@ public void playPauseKeyEvent_playing_pause() throws Exception { player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS); } - @Test - public void playPauseKeyEvent_doubleTapIsTranslatedToSkipToNext() throws Exception { - dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, true); - - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS); - assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse(); - assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PAUSE)).isFalse(); - } - private void dispatchMediaKeyEvent(int keyCode, boolean doubleTap) { audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); From 631ff809f5d05583efe861f16ade02731a44689a Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 27 Jan 2023 14:01:47 +0000 Subject: [PATCH 131/141] Fix timestamp comparison for seeks in fMP4 When seeking in fMP4, we try to extract as little samples as possible by only starting at the preceding sync frame. This comparison should use <= to allow sync frames at exactly the seek position. Issue: google/ExoPlayer#10941 PiperOrigin-RevId: 505098172 (cherry picked from commit 00436a04a4f0fec8ee9154fc1568ca4013ca5c7d) --- RELEASENOTES.md | 7 +- .../extractor/mp4/FragmentedMp4Extractor.java | 6 +- .../mp4/sample_ac3_fragmented.mp4.1.dump | 18 ++--- .../mp4/sample_ac3_fragmented.mp4.2.dump | 12 +-- .../mp4/sample_eac3_fragmented.mp4.1.dump | 78 +++++++++---------- .../mp4/sample_eac3_fragmented.mp4.2.dump | 42 +++++----- 6 files changed, 75 insertions(+), 88 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cdd6711554f..5d021ece390 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,9 +17,12 @@ for seeking. * Use theme when loading drawables on API 21+ ([#220](https://github.com/androidx/media/issues/220)). +* Extractors: * Throw a ParserException instead of a NullPointerException if the sample - * table (stbl) is missing a required sample description (stsd) when - * parsing trak atoms. + table (stbl) is missing a required sample description (stsd) when + parsing trak atoms. + * Correctly skip samples when seeking directly to a sync frame in fMP4 + ([#10941](https://github.com/google/ExoPlayer/issues/10941)). * Audio: * Use the compressed audio format bitrate to calculate the min buffer size for `AudioTrack` in direct playbacks (passthrough). diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/FragmentedMp4Extractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/FragmentedMp4Extractor.java index 800c6010590..4ccca487c82 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/FragmentedMp4Extractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/FragmentedMp4Extractor.java @@ -1675,15 +1675,15 @@ public void resetFragmentInfo() { } /** - * Advances {@link #firstSampleToOutputIndex} to point to the sync sample before the specified - * seek time in the current fragment. + * Advances {@link #firstSampleToOutputIndex} to point to the sync sample at or before the + * specified seek time in the current fragment. * * @param timeUs The seek time, in microseconds. */ public void seek(long timeUs) { int searchIndex = currentSampleIndex; while (searchIndex < fragment.sampleCount - && fragment.getSamplePresentationTimeUs(searchIndex) < timeUs) { + && fragment.getSamplePresentationTimeUs(searchIndex) <= timeUs) { if (fragment.sampleIsSyncFrameTable[searchIndex]) { firstSampleToOutputIndex = searchIndex; } diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump index 0f902e441a8..4bcb712c344 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.1.dump @@ -7,8 +7,8 @@ seekMap: getPosition(288000) = [[timeUs=0, position=636]] numberOfTracks = 1 track 0: - total output bytes = 10752 - sample count = 7 + total output bytes = 9216 + sample count = 6 format 0: averageBitrate = 384000 peakBitrate = 384000 @@ -18,30 +18,26 @@ track 0: sampleRate = 48000 language = und sample 0: - time = 64000 - flags = 1 - data = length 1536, hash 5D09685 - sample 1: time = 96000 flags = 1 data = length 1536, hash A9A24E44 - sample 2: + sample 1: time = 128000 flags = 1 data = length 1536, hash 6F856273 - sample 3: + sample 2: time = 160000 flags = 1 data = length 1536, hash B1737D3C - sample 4: + sample 3: time = 192000 flags = 1 data = length 1536, hash 98FDEB9D - sample 5: + sample 4: time = 224000 flags = 1 data = length 1536, hash 99B9B943 - sample 6: + sample 5: time = 256000 flags = 1 data = length 1536, hash AAD9FCD2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump index d747be40c5b..c03c03e6c0a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.2.dump @@ -7,8 +7,8 @@ seekMap: getPosition(288000) = [[timeUs=0, position=636]] numberOfTracks = 1 track 0: - total output bytes = 6144 - sample count = 4 + total output bytes = 4608 + sample count = 3 format 0: averageBitrate = 384000 peakBitrate = 384000 @@ -18,18 +18,14 @@ track 0: sampleRate = 48000 language = und sample 0: - time = 160000 - flags = 1 - data = length 1536, hash B1737D3C - sample 1: time = 192000 flags = 1 data = length 1536, hash 98FDEB9D - sample 2: + sample 1: time = 224000 flags = 1 data = length 1536, hash 99B9B943 - sample 3: + sample 2: time = 256000 flags = 1 data = length 1536, hash AAD9FCD2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump index 027e7eb6333..e33b92c7bcb 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.1.dump @@ -7,8 +7,8 @@ seekMap: getPosition(1728000) = [[timeUs=0, position=638]] numberOfTracks = 1 track 0: - total output bytes = 148000 - sample count = 37 + total output bytes = 144000 + sample count = 36 format 0: peakBitrate = 1000000 id = 1 @@ -17,150 +17,146 @@ track 0: sampleRate = 48000 language = und sample 0: - time = 544000 - flags = 1 - data = length 4000, hash 27F20D29 - sample 1: time = 576000 flags = 1 data = length 4000, hash 6F565894 - sample 2: + sample 1: time = 608000 flags = 1 data = length 4000, hash A6F07C4A - sample 3: + sample 2: time = 640000 flags = 1 data = length 4000, hash 3A0CA15C - sample 4: + sample 3: time = 672000 flags = 1 data = length 4000, hash DB365414 - sample 5: + sample 4: time = 704000 flags = 1 data = length 4000, hash 31E08469 - sample 6: + sample 5: time = 736000 flags = 1 data = length 4000, hash 315F5C28 - sample 7: + sample 6: time = 768000 flags = 1 data = length 4000, hash CC65DF80 - sample 8: + sample 7: time = 800000 flags = 1 data = length 4000, hash 503FB64C - sample 9: + sample 8: time = 832000 flags = 1 data = length 4000, hash 817CF735 - sample 10: + sample 9: time = 864000 flags = 1 data = length 4000, hash 37391ADA - sample 11: + sample 10: time = 896000 flags = 1 data = length 4000, hash 37391ADA - sample 12: + sample 11: time = 928000 flags = 1 data = length 4000, hash 64DBF751 - sample 13: + sample 12: time = 960000 flags = 1 data = length 4000, hash 81AE828E - sample 14: + sample 13: time = 992000 flags = 1 data = length 4000, hash 767D6C98 - sample 15: + sample 14: time = 1024000 flags = 1 data = length 4000, hash A5F6D4E - sample 16: + sample 15: time = 1056000 flags = 1 data = length 4000, hash EABC6B0D - sample 17: + sample 16: time = 1088000 flags = 1 data = length 4000, hash F47EF742 - sample 18: + sample 17: time = 1120000 flags = 1 data = length 4000, hash 9B2549DA - sample 19: + sample 18: time = 1152000 flags = 1 data = length 4000, hash A12733C9 - sample 20: + sample 19: time = 1184000 flags = 1 data = length 4000, hash 95F62E99 - sample 21: + sample 20: time = 1216000 flags = 1 data = length 4000, hash A4D858 - sample 22: + sample 21: time = 1248000 flags = 1 data = length 4000, hash A4D858 - sample 23: + sample 22: time = 1280000 flags = 1 data = length 4000, hash 22C1A129 - sample 24: + sample 23: time = 1312000 flags = 1 data = length 4000, hash 2C51E4A1 - sample 25: + sample 24: time = 1344000 flags = 1 data = length 4000, hash 3782E8BB - sample 26: + sample 25: time = 1376000 flags = 1 data = length 4000, hash 2C51E4A1 - sample 27: + sample 26: time = 1408000 flags = 1 data = length 4000, hash BDB3D129 - sample 28: + sample 27: time = 1440000 flags = 1 data = length 4000, hash F642A55 - sample 29: + sample 28: time = 1472000 flags = 1 data = length 4000, hash 32F259F4 - sample 30: + sample 29: time = 1504000 flags = 1 data = length 4000, hash 4C987B7C - sample 31: + sample 30: time = 1536000 flags = 1 data = length 4000, hash 57C98E1C - sample 32: + sample 31: time = 1568000 flags = 1 data = length 4000, hash 4C987B7C - sample 33: + sample 32: time = 1600000 flags = 1 data = length 4000, hash 4C987B7C - sample 34: + sample 33: time = 1632000 flags = 1 data = length 4000, hash 4C987B7C - sample 35: + sample 34: time = 1664000 flags = 1 data = length 4000, hash 4C987B7C - sample 36: + sample 35: time = 1696000 flags = 1 data = length 4000, hash 4C987B7C diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump index db94e2636e7..a079fe334e4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.2.dump @@ -7,8 +7,8 @@ seekMap: getPosition(1728000) = [[timeUs=0, position=638]] numberOfTracks = 1 track 0: - total output bytes = 76000 - sample count = 19 + total output bytes = 72000 + sample count = 18 format 0: peakBitrate = 1000000 id = 1 @@ -17,78 +17,74 @@ track 0: sampleRate = 48000 language = und sample 0: - time = 1120000 - flags = 1 - data = length 4000, hash 9B2549DA - sample 1: time = 1152000 flags = 1 data = length 4000, hash A12733C9 - sample 2: + sample 1: time = 1184000 flags = 1 data = length 4000, hash 95F62E99 - sample 3: + sample 2: time = 1216000 flags = 1 data = length 4000, hash A4D858 - sample 4: + sample 3: time = 1248000 flags = 1 data = length 4000, hash A4D858 - sample 5: + sample 4: time = 1280000 flags = 1 data = length 4000, hash 22C1A129 - sample 6: + sample 5: time = 1312000 flags = 1 data = length 4000, hash 2C51E4A1 - sample 7: + sample 6: time = 1344000 flags = 1 data = length 4000, hash 3782E8BB - sample 8: + sample 7: time = 1376000 flags = 1 data = length 4000, hash 2C51E4A1 - sample 9: + sample 8: time = 1408000 flags = 1 data = length 4000, hash BDB3D129 - sample 10: + sample 9: time = 1440000 flags = 1 data = length 4000, hash F642A55 - sample 11: + sample 10: time = 1472000 flags = 1 data = length 4000, hash 32F259F4 - sample 12: + sample 11: time = 1504000 flags = 1 data = length 4000, hash 4C987B7C - sample 13: + sample 12: time = 1536000 flags = 1 data = length 4000, hash 57C98E1C - sample 14: + sample 13: time = 1568000 flags = 1 data = length 4000, hash 4C987B7C - sample 15: + sample 14: time = 1600000 flags = 1 data = length 4000, hash 4C987B7C - sample 16: + sample 15: time = 1632000 flags = 1 data = length 4000, hash 4C987B7C - sample 17: + sample 16: time = 1664000 flags = 1 data = length 4000, hash 4C987B7C - sample 18: + sample 17: time = 1696000 flags = 1 data = length 4000, hash 4C987B7C From bfc4ed4dd4307f4e16f584a609d6645020d7107c Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 27 Jan 2023 18:11:27 +0000 Subject: [PATCH 132/141] Inline method in PlayerService that is used from on call site only #cleanup #minor-release PiperOrigin-RevId: 505146915 (cherry picked from commit d7ef1ab5bd5a4508c0913011f5990bb03a57585a) --- .../media3/demo/session/PlaybackService.kt | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt index b5ba86ab224..192499d4e1f 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt @@ -15,7 +15,6 @@ */ package androidx.media3.demo.session -import android.app.Notification.BigTextStyle import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent.* @@ -272,34 +271,29 @@ class PlaybackService : MediaLibraryService() { * background. */ override fun onForegroundServiceStartNotAllowedException() { - createNotificationAndNotify() + val notificationManagerCompat = NotificationManagerCompat.from(this@PlaybackService) + ensureNotificationChannel(notificationManagerCompat) + val pendingIntent = + TaskStackBuilder.create(this@PlaybackService).run { + addNextIntent(Intent(this@PlaybackService, MainActivity::class.java)) + + val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0 + getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT) + } + val builder = + NotificationCompat.Builder(this@PlaybackService, CHANNEL_ID) + .setContentIntent(pendingIntent) + .setSmallIcon(R.drawable.media3_notification_small_icon) + .setContentTitle(getString(R.string.notification_content_title)) + .setStyle( + NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_content_text)) + ) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + notificationManagerCompat.notify(NOTIFICATION_ID, builder.build()) } } - private fun createNotificationAndNotify() { - var notificationManagerCompat = NotificationManagerCompat.from(this) - ensureNotificationChannel(notificationManagerCompat) - var pendingIntent = - TaskStackBuilder.create(this).run { - addNextIntent(Intent(this@PlaybackService, MainActivity::class.java)) - - val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0 - getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT) - } - - var builder = - NotificationCompat.Builder(this, CHANNEL_ID) - .setContentIntent(pendingIntent) - .setSmallIcon(R.drawable.media3_notification_small_icon) - .setContentTitle(getString(R.string.notification_content_title)) - .setStyle( - NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_content_text)) - ) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setAutoCancel(true) - notificationManagerCompat.notify(NOTIFICATION_ID, builder.build()) - } - private fun ensureNotificationChannel(notificationManagerCompat: NotificationManagerCompat) { if (Util.SDK_INT < 26 || notificationManagerCompat.getNotificationChannel(CHANNEL_ID) != null) { return From 5528baaad9903e202d41e36562bc38d7d350a527 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 30 Jan 2023 18:23:50 +0000 Subject: [PATCH 133/141] Do not assume a valid queue in 3rd party sessions This change fixes an issue that can be reproduced when a controller `onConnect` creates a `QueueTimeline` out of the state of a legacy session and then `prepare` is called. `activeQueueItemId`, `metadata` and the `queue` of the legacy session are used when a `QueueTimeline` is created. The change adds unit tests to cover the different combinatoric cases these properties being set or unset. PiperOrigin-RevId: 505731288 (cherry picked from commit 4a9cf7d069b1b35be807886d59d87c396b19876c) --- RELEASENOTES.md | 2 + .../session/MediaControllerImplLegacy.java | 6 +- .../media3/session/QueueTimeline.java | 171 ++++++++---- .../common/IRemoteMediaSessionCompat.aidl | 1 + ...aControllerWithMediaSessionCompatTest.java | 263 ++++++++++++++++++ .../MediaSessionCompatProviderService.java | 57 +++- .../session/RemoteMediaSessionCompat.java | 4 + 7 files changed, 444 insertions(+), 60 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5d021ece390..9734daf5775 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -54,6 +54,8 @@ onto Player ([#156](https://github.com/androidx/media/issues/156)). * Avoid double tap detection for non-Bluetooth media button events ([#233](https://github.com/androidx/media/issues/233)). + * Make `QueueTimeline` more robust in case of a shady legacy session state + ([#241](https://github.com/androidx/media/issues/241)). * Metadata: * Parse multiple null-separated values from ID3 frames, as permitted by ID3 v2.4. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index fc58fbcbc50..cd0a0a1fca8 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -1828,6 +1828,8 @@ private static ControllerInfo buildNewControllerInfo( + " MediaItem."); MediaItem fakeMediaItem = MediaUtils.convertToMediaItem(newLegacyPlayerInfo.mediaMetadataCompat, ratingType); + // Ad a tag to make sure the fake media item can't have an equal instance by accident. + fakeMediaItem = fakeMediaItem.buildUpon().setTag(new Object()).build(); currentTimeline = currentTimeline.copyWithFakeMediaItem(fakeMediaItem); currentMediaItemIndex = currentTimeline.getWindowCount() - 1; } else { @@ -1842,7 +1844,7 @@ private static ControllerInfo buildNewControllerInfo( if (hasMediaMetadataCompat) { MediaItem mediaItem = MediaUtils.convertToMediaItem( - currentTimeline.getMediaItemAt(currentMediaItemIndex).mediaId, + checkNotNull(currentTimeline.getMediaItemAt(currentMediaItemIndex)).mediaId, newLegacyPlayerInfo.mediaMetadataCompat, ratingType); currentTimeline = @@ -1999,7 +2001,7 @@ private static ControllerInfo buildNewControllerInfo( MediaItem oldCurrentMediaItem = checkStateNotNull(oldControllerInfo.playerInfo.getCurrentMediaItem()); int oldCurrentMediaItemIndexInNewTimeline = - ((QueueTimeline) newControllerInfo.playerInfo.timeline).findIndexOf(oldCurrentMediaItem); + ((QueueTimeline) newControllerInfo.playerInfo.timeline).indexOf(oldCurrentMediaItem); if (oldCurrentMediaItemIndexInNewTimeline == C.INDEX_UNSET) { // Old current item is removed. discontinuityReason = Player.DISCONTINUITY_REASON_REMOVE; diff --git a/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java b/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java index adaf65d7071..a7dc94c5117 100644 --- a/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java +++ b/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java @@ -15,6 +15,10 @@ */ package androidx.media3.session; +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat.QueueItem; import androidx.annotation.Nullable; import androidx.media3.common.C; @@ -25,20 +29,18 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; -import java.util.Collections; -import java.util.IdentityHashMap; +import java.util.HashMap; import java.util.List; import java.util.Map; /** - * An immutable class to represent the current {@link Timeline} backed by {@link QueueItem}. + * An immutable class to represent the current {@link Timeline} backed by {@linkplain QueueItem + * queue items}. * - *

          This supports the fake item that represents the removed but currently playing media item. In - * that case, a fake item would be inserted at the end of the {@link MediaItem media item list} - * converted from {@link QueueItem queue item list}. Without the fake item support, the timeline - * should be always recreated to handle the case when the fake item is no longer necessary and - * timeline change isn't precisely detected. Queue item doesn't support equals(), so it's better not - * to use equals() on the converted MediaItem. + *

          This timeline supports the case in which the current {@link MediaMetadataCompat} is not + * included in the queue of the session. In such a case a fake media item is inserted at the end of + * the timeline and the size of the timeline is by one larger than the size of the corresponding + * queue in the session. */ /* package */ final class QueueTimeline extends Timeline { @@ -48,85 +50,150 @@ private static final Object FAKE_WINDOW_UID = new Object(); private final ImmutableList mediaItems; - private final Map unmodifiableMediaItemToQueueIdMap; + private final ImmutableMap mediaItemToQueueIdMap; @Nullable private final MediaItem fakeMediaItem; + /** Creates a new instance. */ + public QueueTimeline(QueueTimeline queueTimeline) { + this.mediaItems = queueTimeline.mediaItems; + this.mediaItemToQueueIdMap = queueTimeline.mediaItemToQueueIdMap; + this.fakeMediaItem = queueTimeline.fakeMediaItem; + } + private QueueTimeline( ImmutableList mediaItems, - Map unmodifiableMediaItemToQueueIdMap, + ImmutableMap mediaItemToQueueIdMap, @Nullable MediaItem fakeMediaItem) { this.mediaItems = mediaItems; - this.unmodifiableMediaItemToQueueIdMap = unmodifiableMediaItemToQueueIdMap; + this.mediaItemToQueueIdMap = mediaItemToQueueIdMap; this.fakeMediaItem = fakeMediaItem; } - public QueueTimeline(QueueTimeline queueTimeline) { - this.mediaItems = queueTimeline.mediaItems; - this.unmodifiableMediaItemToQueueIdMap = queueTimeline.unmodifiableMediaItemToQueueIdMap; - this.fakeMediaItem = queueTimeline.fakeMediaItem; + /** Creates a {@link QueueTimeline} from a list of {@linkplain QueueItem queue items}. */ + public static QueueTimeline create(List queue) { + ImmutableList.Builder mediaItemsBuilder = new ImmutableList.Builder<>(); + ImmutableMap.Builder mediaItemToQueueIdMap = new ImmutableMap.Builder<>(); + for (int i = 0; i < queue.size(); i++) { + QueueItem queueItem = queue.get(i); + MediaItem mediaItem = MediaUtils.convertToMediaItem(queueItem); + mediaItemsBuilder.add(mediaItem); + mediaItemToQueueIdMap.put(mediaItem, queueItem.getQueueId()); + } + return new QueueTimeline( + mediaItemsBuilder.build(), mediaItemToQueueIdMap.buildOrThrow(), /* fakeMediaItem= */ null); } + /** + * Gets the queue ID of the media item at the given index or {@link QueueItem#UNKNOWN_ID} if not + * known. + * + * @param mediaItemIndex The media item index. + * @return The corresponding queue ID or {@link QueueItem#UNKNOWN_ID} if not known. + */ + public long getQueueId(int mediaItemIndex) { + MediaItem mediaItem = getMediaItemAt(mediaItemIndex); + @Nullable Long queueId = mediaItemToQueueIdMap.get(mediaItem); + return queueId == null ? QueueItem.UNKNOWN_ID : queueId; + } + + /** + * Copies the timeline with the given fake media item. + * + * @param fakeMediaItem The fake media item. + * @return A new {@link QueueTimeline} reflecting the update. + */ public QueueTimeline copyWithFakeMediaItem(@Nullable MediaItem fakeMediaItem) { - return new QueueTimeline(mediaItems, unmodifiableMediaItemToQueueIdMap, fakeMediaItem); + return new QueueTimeline(mediaItems, mediaItemToQueueIdMap, fakeMediaItem); } + /** + * Replaces the media item at {@code replaceIndex} with the new media item. + * + * @param replaceIndex The index at which to replace the media item. + * @param newMediaItem The new media item that replaces the old one. + * @return A new {@link QueueTimeline} reflecting the update. + */ public QueueTimeline copyWithNewMediaItem(int replaceIndex, MediaItem newMediaItem) { + checkArgument( + replaceIndex < mediaItems.size() + || (replaceIndex == mediaItems.size() && fakeMediaItem != null)); + if (replaceIndex == mediaItems.size()) { + return new QueueTimeline(mediaItems, mediaItemToQueueIdMap, newMediaItem); + } + MediaItem oldMediaItem = mediaItems.get(replaceIndex); + // Create the new play list. ImmutableList.Builder newMediaItemsBuilder = new ImmutableList.Builder<>(); newMediaItemsBuilder.addAll(mediaItems.subList(0, replaceIndex)); newMediaItemsBuilder.add(newMediaItem); newMediaItemsBuilder.addAll(mediaItems.subList(replaceIndex + 1, mediaItems.size())); + // Update the map of items to queue IDs accordingly. + Map newMediaItemToQueueIdMap = new HashMap<>(mediaItemToQueueIdMap); + Long queueId = checkNotNull(newMediaItemToQueueIdMap.remove(oldMediaItem)); + newMediaItemToQueueIdMap.put(newMediaItem, queueId); return new QueueTimeline( - newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem); + newMediaItemsBuilder.build(), ImmutableMap.copyOf(newMediaItemToQueueIdMap), fakeMediaItem); } + /** + * Replaces the media item at the given index with a list of new media items. The timeline grows + * by one less than the size of the new list of items. + * + * @param index The index of the media item to be replaced. + * @param newMediaItems The list of new {@linkplain MediaItem media items} to insert. + * @return A new {@link QueueTimeline} reflecting the update. + */ public QueueTimeline copyWithNewMediaItems(int index, List newMediaItems) { ImmutableList.Builder newMediaItemsBuilder = new ImmutableList.Builder<>(); newMediaItemsBuilder.addAll(mediaItems.subList(0, index)); newMediaItemsBuilder.addAll(newMediaItems); newMediaItemsBuilder.addAll(mediaItems.subList(index, mediaItems.size())); - return new QueueTimeline( - newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem); + return new QueueTimeline(newMediaItemsBuilder.build(), mediaItemToQueueIdMap, fakeMediaItem); } + /** + * Removes the range of media items in the current timeline. + * + * @param fromIndex The index to start removing items from. + * @param toIndex The index up to which to remove items (exclusive). + * @return A new {@link QueueTimeline} reflecting the update. + */ public QueueTimeline copyWithRemovedMediaItems(int fromIndex, int toIndex) { ImmutableList.Builder newMediaItemsBuilder = new ImmutableList.Builder<>(); newMediaItemsBuilder.addAll(mediaItems.subList(0, fromIndex)); newMediaItemsBuilder.addAll(mediaItems.subList(toIndex, mediaItems.size())); - return new QueueTimeline( - newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem); + return new QueueTimeline(newMediaItemsBuilder.build(), mediaItemToQueueIdMap, fakeMediaItem); } + /** + * Moves the defined range of media items to a new position. + * + * @param fromIndex The start index of the range to be moved. + * @param toIndex The (exclusive) end index of the range to be moved. + * @param newIndex The new index to move the first item of the range to. + * @return A new {@link QueueTimeline} reflecting the update. + */ public QueueTimeline copyWithMovedMediaItems(int fromIndex, int toIndex, int newIndex) { List list = new ArrayList<>(mediaItems); Util.moveItems(list, fromIndex, toIndex, newIndex); return new QueueTimeline( new ImmutableList.Builder().addAll(list).build(), - unmodifiableMediaItemToQueueIdMap, + mediaItemToQueueIdMap, fakeMediaItem); } - public static QueueTimeline create(List queue) { - ImmutableList.Builder mediaItemsBuilder = new ImmutableList.Builder<>(); - IdentityHashMap mediaItemToQueueIdMap = new IdentityHashMap<>(); - for (int i = 0; i < queue.size(); i++) { - QueueItem queueItem = queue.get(i); - MediaItem mediaItem = MediaUtils.convertToMediaItem(queueItem); - mediaItemsBuilder.add(mediaItem); - mediaItemToQueueIdMap.put(mediaItem, queueItem.getQueueId()); - } - return new QueueTimeline( - mediaItemsBuilder.build(), - Collections.unmodifiableMap(mediaItemToQueueIdMap), - /* fakeMediaItem= */ null); - } - - public long getQueueId(int mediaItemIndex) { - @Nullable MediaItem mediaItem = mediaItems.get(mediaItemIndex); - if (mediaItem == null) { - return QueueItem.UNKNOWN_ID; + /** + * Returns the media item index of the given media item in the timeline, or {@link C#INDEX_UNSET} + * if the item is not part of this timeline. + * + * @param mediaItem The media item of interest. + * @return The index of the item or {@link C#INDEX_UNSET} if the item is not part of the timeline. + */ + public int indexOf(MediaItem mediaItem) { + if (mediaItem.equals(fakeMediaItem)) { + return mediaItems.size(); } - Long queueId = unmodifiableMediaItemToQueueIdMap.get(mediaItem); - return queueId == null ? QueueItem.UNKNOWN_ID : queueId; + int mediaItemIndex = mediaItems.indexOf(mediaItem); + return mediaItemIndex == -1 ? C.INDEX_UNSET : mediaItemIndex; } @Nullable @@ -137,14 +204,6 @@ public MediaItem getMediaItemAt(int mediaItemIndex) { return (mediaItemIndex == mediaItems.size()) ? fakeMediaItem : null; } - public int findIndexOf(MediaItem mediaItem) { - if (mediaItem == fakeMediaItem) { - return mediaItems.size(); - } - int mediaItemIndex = mediaItems.indexOf(mediaItem); - return mediaItemIndex == -1 ? C.INDEX_UNSET : mediaItemIndex; - } - @Override public int getWindowCount() { return mediaItems.size() + ((fakeMediaItem == null) ? 0 : 1); @@ -198,14 +257,14 @@ public boolean equals(@Nullable Object obj) { return false; } QueueTimeline other = (QueueTimeline) obj; - return mediaItems == other.mediaItems - && unmodifiableMediaItemToQueueIdMap == other.unmodifiableMediaItemToQueueIdMap - && fakeMediaItem == other.fakeMediaItem; + return Objects.equal(mediaItems, other.mediaItems) + && Objects.equal(mediaItemToQueueIdMap, other.mediaItemToQueueIdMap) + && Objects.equal(fakeMediaItem, other.fakeMediaItem); } @Override public int hashCode() { - return Objects.hashCode(mediaItems, unmodifiableMediaItemToQueueIdMap, fakeMediaItem); + return Objects.hashCode(mediaItems, mediaItemToQueueIdMap, fakeMediaItem); } private static Window getWindow(Window window, MediaItem mediaItem, int windowIndex) { diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl index 3fe24ac8b95..196306d7893 100644 --- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl +++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl @@ -42,4 +42,5 @@ interface IRemoteMediaSessionCompat { void sendSessionEvent(String sessionTag, String event, in Bundle extras); void setCaptioningEnabled(String sessionTag, boolean enabled); void setSessionExtras(String sessionTag, in Bundle extras); + int getCallbackMethodCount(String sessionTag, String methodName); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java index e07afb4422d..748fbf9fd45 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java @@ -1779,6 +1779,269 @@ public void getTotalBufferedDuration() throws Exception { assertThat(totalBufferedDurationMs).isEqualTo(testTotalBufferedDurationMs); } + @Test + public void prepare_empty_correctInitializationState() throws Exception { + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .build()); + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + + // Assert the constructed timeline and start index after connecting to an empty session. + int mediaItemCount = threadTestRule.getHandler().postAndSync(controller::getMediaItemCount); + int currentMediaItemIndex = + threadTestRule.getHandler().postAndSync(controller::getCurrentMediaItemIndex); + assertThat(mediaItemCount).isEqualTo(0); + assertThat(currentMediaItemIndex).isEqualTo(0); + } + + @Test + public void prepare_withMetadata_callsPrepareFromMediaId() throws Exception { + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .build()); + session.setMetadata( + new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, "mediaItem_2") + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Title") + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Subtitle") + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, "Artist") + .build()); + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch countDownLatch = new CountDownLatch(1); + controller.addListener( + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) { + countDownLatch.countDown(); + } + } + }); + + // Assert the constructed timeline and start index for preparation. + int mediaItemCount = threadTestRule.getHandler().postAndSync(controller::getMediaItemCount); + int currentMediaItemIndex = + threadTestRule.getHandler().postAndSync(controller::getCurrentMediaItemIndex); + assertThat(mediaItemCount).isEqualTo(1); + assertThat(currentMediaItemIndex).isEqualTo(0); + + threadTestRule.getHandler().postAndSync(controller::prepare); + + // Assert whether the correct preparation method has been called and received by the session. + assertThat(countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + int callbackMethodCount = + session.getCallbackMethodCount( + MediaSessionCompatProviderService.METHOD_ON_PREPARE_FROM_MEDIA_ID); + assertThat(callbackMethodCount).isEqualTo(1); + } + + @Test + public void prepare_withMetadataAndActiveQueueItemId_callsPrepareFromMediaId() throws Exception { + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setActiveQueueItemId(4) + .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .build()); + session.setMetadata( + new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, "mediaItem_2") + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Title") + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Subtitle") + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, "Artist") + .build()); + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch countDownLatch = new CountDownLatch(1); + controller.addListener( + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) { + countDownLatch.countDown(); + } + } + }); + + // Assert the constructed timeline and start index for preparation. + int mediaItemCount = threadTestRule.getHandler().postAndSync(controller::getMediaItemCount); + int currentMediaItemIndex = + threadTestRule.getHandler().postAndSync(controller::getCurrentMediaItemIndex); + assertThat(mediaItemCount).isEqualTo(1); + assertThat(currentMediaItemIndex).isEqualTo(0); + + threadTestRule.getHandler().postAndSync(controller::prepare); + + // Assert whether the correct preparation method has been called and received by the session. + assertThat(countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + int callbackMethodCount = + session.getCallbackMethodCount( + MediaSessionCompatProviderService.METHOD_ON_PREPARE_FROM_MEDIA_ID); + assertThat(callbackMethodCount).isEqualTo(1); + } + + @Test + public void prepare_withQueue_callsPrepare() throws Exception { + List testMediaItems = MediaTestUtils.createMediaItems(10); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testMediaItems); + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .build()); + session.setQueue(testQueue); + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch countDownLatch = new CountDownLatch(1); + controller.addListener( + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) { + countDownLatch.countDown(); + } + } + }); + + // Assert the constructed timeline and start index for preparation. + int mediaItemCount = threadTestRule.getHandler().postAndSync(controller::getMediaItemCount); + int currentMediaItemIndex = + threadTestRule.getHandler().postAndSync(controller::getCurrentMediaItemIndex); + assertThat(mediaItemCount).isEqualTo(10); + assertThat(currentMediaItemIndex).isEqualTo(0); + + threadTestRule.getHandler().postAndSync(controller::prepare); + + // Assert whether the correct preparation method has been called and received by the session. + assertThat(countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + int callbackMethodCount = + session.getCallbackMethodCount(MediaSessionCompatProviderService.METHOD_ON_PREPARE); + assertThat(callbackMethodCount).isEqualTo(1); + } + + @Test + public void prepare_withQueueAndActiveQueueItemId_callsPrepare() throws Exception { + List testMediaItems = MediaTestUtils.createMediaItems(10); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testMediaItems); + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setActiveQueueItemId(5) + .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .build()); + session.setQueue(testQueue); + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch countDownLatch = new CountDownLatch(1); + controller.addListener( + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) { + countDownLatch.countDown(); + } + } + }); + + // Assert the constructed timeline and start index for preparation. + int mediaItemCount = threadTestRule.getHandler().postAndSync(controller::getMediaItemCount); + int currentMediaItemIndex = + threadTestRule.getHandler().postAndSync(controller::getCurrentMediaItemIndex); + assertThat(mediaItemCount).isEqualTo(10); + assertThat(currentMediaItemIndex).isEqualTo(5); + + threadTestRule.getHandler().postAndSync(controller::prepare); + + // Assert whether the correct preparation method has been called and received by the session. + assertThat(countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + int callbackMethodCount = + session.getCallbackMethodCount(MediaSessionCompatProviderService.METHOD_ON_PREPARE); + assertThat(callbackMethodCount).isEqualTo(1); + } + + @Test + public void prepare_withQueueAndMetadata_callsPrepareFromMediaId() throws Exception { + List testMediaItems = MediaTestUtils.createMediaItems(10); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testMediaItems); + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .build()); + session.setMetadata( + new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, "mediaItem_2") + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Title") + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Subtitle") + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, "Artist") + .build()); + session.setQueue(testQueue); + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch countDownLatch = new CountDownLatch(1); + controller.addListener( + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) { + countDownLatch.countDown(); + } + } + }); + + // Assert the constructed timeline and start index for preparation. + int mediaItemCount = threadTestRule.getHandler().postAndSync(controller::getMediaItemCount); + int currentMediaItemIndex = + threadTestRule.getHandler().postAndSync(controller::getCurrentMediaItemIndex); + assertThat(mediaItemCount).isEqualTo(11); + assertThat(currentMediaItemIndex).isEqualTo(10); + + threadTestRule.getHandler().postAndSync(controller::prepare); + + // Assert whether the correct preparation method has been called and received by the session. + assertThat(countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + int callbackMethodCount = + session.getCallbackMethodCount( + MediaSessionCompatProviderService.METHOD_ON_PREPARE_FROM_MEDIA_ID); + assertThat(callbackMethodCount).isEqualTo(1); + } + + @Test + public void prepare_withQueueAndMetadataAndActiveQueueItemId_callsPrepare() throws Exception { + List testMediaItems = MediaTestUtils.createMediaItems(10); + List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testMediaItems); + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setActiveQueueItemId(4) + .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .build()); + session.setMetadata( + new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, "mediaItem_5") + .build()); + session.setQueue(testQueue); + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch countDownLatch = new CountDownLatch(1); + controller.addListener( + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + if (events.contains(Player.EVENT_MEDIA_METADATA_CHANGED)) { + countDownLatch.countDown(); + } + } + }); + + // Assert the constructed timeline and start index for preparation. + int mediaItemCount = threadTestRule.getHandler().postAndSync(controller::getMediaItemCount); + int currentMediaItemIndex = + threadTestRule.getHandler().postAndSync(controller::getCurrentMediaItemIndex); + assertThat(mediaItemCount).isEqualTo(10); + assertThat(currentMediaItemIndex).isEqualTo(4); + + threadTestRule.getHandler().postAndSync(controller::prepare); + + // Assert whether the correct preparation method has been called and received by the session. + assertThat(countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + int callbackMethodCount = + session.getCallbackMethodCount(MediaSessionCompatProviderService.METHOD_ON_PREPARE); + assertThat(callbackMethodCount).isEqualTo(1); + } + @Nullable private Bitmap getBitmapFromMetadata(MediaMetadata metadata) throws Exception { @Nullable Bitmap bitmap = null; diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java index 3fac9431e1e..91346dffa62 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java @@ -49,9 +49,13 @@ @UnstableApi public class MediaSessionCompatProviderService extends Service { + public static final String METHOD_ON_PREPARE_FROM_MEDIA_ID = "onPrepareFromMediaId"; + public static final String METHOD_ON_PREPARE = "onPrepare"; + private static final String TAG = "MSCProviderService"; Map sessionMap = new HashMap<>(); + Map callbackMap = new HashMap<>(); RemoteMediaSessionCompatStub sessionBinder; TestHandler handler; @@ -88,7 +92,10 @@ public void create(String sessionTag) throws RemoteException { () -> { MediaSessionCompat session = new MediaSessionCompat(MediaSessionCompatProviderService.this, sessionTag); + CallCountingCallback callback = new CallCountingCallback(sessionTag); + session.setCallback(callback); sessionMap.put(sessionTag, session); + callbackMap.put(sessionTag, callback); }); } catch (Exception e) { Log.e(TAG, "Exception occurred while creating MediaSessionCompat", e); @@ -212,15 +219,61 @@ public void sendSessionEvent(String sessionTag, String event, Bundle extras) } @Override - public void setCaptioningEnabled(String sessionTag, boolean enabled) throws RemoteException { + public void setCaptioningEnabled(String sessionTag, boolean enabled) { MediaSessionCompat session = sessionMap.get(sessionTag); session.setCaptioningEnabled(enabled); } @Override - public void setSessionExtras(String sessionTag, Bundle extras) throws RemoteException { + public void setSessionExtras(String sessionTag, Bundle extras) { MediaSessionCompat session = sessionMap.get(sessionTag); session.setExtras(extras); } + + @Override + public int getCallbackMethodCount(String sessionTag, String methodName) { + CallCountingCallback callCountingCallback = callbackMap.get(sessionTag); + if (callCountingCallback != null) { + Integer count = callCountingCallback.callbackCallCounters.get(methodName); + return count != null ? count : 0; + } + return 0; + } + } + + private class CallCountingCallback extends MediaSessionCompat.Callback { + + private final String sessionTag; + private final Map callbackCallCounters; + + public CallCountingCallback(String sessionTag) { + this.sessionTag = sessionTag; + callbackCallCounters = new HashMap<>(); + } + + @Override + public void onPrepareFromMediaId(String mediaId, Bundle extras) { + countCallbackCall(METHOD_ON_PREPARE_FROM_MEDIA_ID); + sessionMap + .get(sessionTag) + .setMetadata( + new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId) + .build()); + } + + @Override + public void onPrepare() { + countCallbackCall(METHOD_ON_PREPARE); + sessionMap.get(sessionTag).setMetadata(new MediaMetadataCompat.Builder().build()); + } + + private void countCallbackCall(String callbackName) { + int count = 0; + if (callbackCallCounters.containsKey(callbackName)) { + count = callbackCallCounters.get(callbackName); + } + callbackCallCounters.put(callbackName, ++count); + } } } diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java index da94920c59e..887d9397286 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java @@ -111,6 +111,10 @@ public void setPlaybackToLocal(int stream) throws RemoteException { binder.setPlaybackToLocal(sessionTag, stream); } + public int getCallbackMethodCount(String callbackMethodName) throws RemoteException { + return binder.getCallbackMethodCount(sessionTag, callbackMethodName); + } + /** * Since we cannot pass VolumeProviderCompat directly, we pass volumeControl, maxVolume, * currentVolume instead. From 791c05b57a7a339f80652397f7ac600d55437486 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 31 Jan 2023 16:55:07 +0000 Subject: [PATCH 134/141] Fix (another) `LeanbackPlayerAdapter` param name mismatch I missed this when fixing `positionInMs` for Dackka in https://github.com/androidx/media/commit/aae6941981dfcfcdd46544f585335ff26d8f81e9 This time I manually verified that all the `@Override` methods have parameter names that match [the docs](https://developer.android.com/reference/androidx/leanback/media/PlayerAdapter). #minor-release PiperOrigin-RevId: 506017063 (cherry picked from commit d1a27bf2a81709bc7b03ad130bc9abd4d8b27164) --- .../androidx/media3/ui/leanback/LeanbackPlayerAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java b/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java index 51bc101b0d6..ff675b5e540 100644 --- a/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java +++ b/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java @@ -110,9 +110,9 @@ public void onDetachedFromHost() { } @Override - public void setProgressUpdatingEnabled(boolean enabled) { + public void setProgressUpdatingEnabled(boolean enable) { handler.removeCallbacks(this); - if (enabled) { + if (enable) { handler.post(this); } } From d49bd456b695c5d289b2e05dfcf6d03cb5fe20fe Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 2 Feb 2023 15:30:08 +0000 Subject: [PATCH 135/141] Merge pull request #10793 from fraunhoferfokus:dash-thumbnail-support PiperOrigin-RevId: 506261584 (cherry picked from commit c6569a36fbce6fc3ece55c9a904508bd4a4c45da) --- RELEASENOTES.md | 3 + .../java/androidx/media3/common/Format.java | 70 +++++++++++++++++++ .../androidx/media3/common/FormatTest.java | 2 + .../exoplayer/dash/DashMediaSource.java | 16 +++-- .../dash/manifest/DashManifestParser.java | 44 +++++++++++- .../dash/manifest/DashManifestParserTest.java | 18 +++-- .../test/assets/media/mpd/sample_mpd_images | 5 +- 7 files changed, 144 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9734daf5775..a85995fac9e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -31,6 +31,9 @@ `Subtitle.getEventTime` if a subtitle file contains no cues. * SubRip: Add support for UTF-16 files if they start with a byte order mark. +* DASH: + * Add full parsing for image adaptation sets, including tile counts + ([#3752](https://github.com/google/ExoPlayer/issues/3752)). * UI: * Fix the deprecated `PlayerView.setControllerVisibilityListener(PlayerControlView.VisibilityListener)` diff --git a/libraries/common/src/main/java/androidx/media3/common/Format.java b/libraries/common/src/main/java/androidx/media3/common/Format.java index 450585d1f10..b34bead66ea 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Format.java +++ b/libraries/common/src/main/java/androidx/media3/common/Format.java @@ -105,6 +105,13 @@ *

            *
          • {@link #accessibilityChannel} *
          + * + *

          Fields relevant to image formats

          + * + *
            + *
          • {@link #tileCountHorizontal} + *
          • {@link #tileCountVertical} + *
          */ public final class Format implements Bundleable { @@ -165,6 +172,11 @@ public static final class Builder { private int accessibilityChannel; + // Image specific + + private int tileCountHorizontal; + private int tileCountVertical; + // Provided by the source. private @C.CryptoType int cryptoType; @@ -188,6 +200,9 @@ public Builder() { pcmEncoding = NO_VALUE; // Text specific. accessibilityChannel = NO_VALUE; + // Image specific. + tileCountHorizontal = NO_VALUE; + tileCountVertical = NO_VALUE; // Provided by the source. cryptoType = C.CRYPTO_TYPE_NONE; } @@ -232,6 +247,9 @@ private Builder(Format format) { this.encoderPadding = format.encoderPadding; // Text specific. this.accessibilityChannel = format.accessibilityChannel; + // Image specific. + this.tileCountHorizontal = format.tileCountHorizontal; + this.tileCountVertical = format.tileCountVertical; // Provided by the source. this.cryptoType = format.cryptoType; } @@ -607,6 +625,32 @@ public Builder setAccessibilityChannel(int accessibilityChannel) { return this; } + // Image specific. + + /** + * Sets {@link Format#tileCountHorizontal}. The default value is {@link #NO_VALUE}. + * + * @param tileCountHorizontal The {@link Format#accessibilityChannel}. + * @return The builder. + */ + @CanIgnoreReturnValue + public Builder setTileCountHorizontal(int tileCountHorizontal) { + this.tileCountHorizontal = tileCountHorizontal; + return this; + } + + /** + * Sets {@link Format#tileCountVertical}. The default value is {@link #NO_VALUE}. + * + * @param tileCountVertical The {@link Format#accessibilityChannel}. + * @return The builder. + */ + @CanIgnoreReturnValue + public Builder setTileCountVertical(int tileCountVertical) { + this.tileCountVertical = tileCountVertical; + return this; + } + // Provided by source. /** @@ -779,6 +823,15 @@ public Format build() { /** The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */ @UnstableApi public final int accessibilityChannel; + // Image specific. + + /** + * The number of horizontal tiles in an image, or {@link #NO_VALUE} if not known or applicable. + */ + @UnstableApi public final int tileCountHorizontal; + /** The number of vertical tiles in an image, or {@link #NO_VALUE} if not known or applicable. */ + @UnstableApi public final int tileCountVertical; + // Provided by source. /** @@ -1008,6 +1061,9 @@ private Format(Builder builder) { encoderPadding = builder.encoderPadding == NO_VALUE ? 0 : builder.encoderPadding; // Text specific. accessibilityChannel = builder.accessibilityChannel; + // Image specific. + tileCountHorizontal = builder.tileCountHorizontal; + tileCountVertical = builder.tileCountVertical; // Provided by source. if (builder.cryptoType == C.CRYPTO_TYPE_NONE && drmInitData != null) { // Encrypted content cannot use CRYPTO_TYPE_NONE. @@ -1268,6 +1324,9 @@ public int hashCode() { result = 31 * result + encoderPadding; // Text specific. result = 31 * result + accessibilityChannel; + // Image specific. + result = 31 * result + tileCountHorizontal; + result = 31 * result + tileCountVertical; // Provided by the source. result = 31 * result + cryptoType; hashCode = result; @@ -1304,6 +1363,8 @@ public boolean equals(@Nullable Object obj) { && encoderDelay == other.encoderDelay && encoderPadding == other.encoderPadding && accessibilityChannel == other.accessibilityChannel + && tileCountHorizontal == other.tileCountHorizontal + && tileCountVertical == other.tileCountVertical && cryptoType == other.cryptoType && Float.compare(frameRate, other.frameRate) == 0 && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 @@ -1500,6 +1561,8 @@ public static String toLogString(@Nullable Format format) { private static final String FIELD_ENCODER_PADDING = Util.intToStringMaxRadix(27); private static final String FIELD_ACCESSIBILITY_CHANNEL = Util.intToStringMaxRadix(28); private static final String FIELD_CRYPTO_TYPE = Util.intToStringMaxRadix(29); + private static final String FIELD_TILE_COUNT_HORIZONTAL = Util.intToStringMaxRadix(30); + private static final String FIELD_TILE_COUNT_VERTICAL = Util.intToStringMaxRadix(31); @UnstableApi @Override @@ -1557,6 +1620,9 @@ public Bundle toBundle(boolean excludeMetadata) { bundle.putInt(FIELD_ENCODER_PADDING, encoderPadding); // Text specific. bundle.putInt(FIELD_ACCESSIBILITY_CHANNEL, accessibilityChannel); + // Image specific. + bundle.putInt(FIELD_TILE_COUNT_HORIZONTAL, tileCountHorizontal); + bundle.putInt(FIELD_TILE_COUNT_VERTICAL, tileCountVertical); // Source specific. bundle.putInt(FIELD_CRYPTO_TYPE, cryptoType); return bundle; @@ -1621,6 +1687,10 @@ private static Format fromBundle(Bundle bundle) { // Text specific. .setAccessibilityChannel( bundle.getInt(FIELD_ACCESSIBILITY_CHANNEL, DEFAULT.accessibilityChannel)) + // Image specific. + .setTileCountHorizontal( + bundle.getInt(FIELD_TILE_COUNT_HORIZONTAL, DEFAULT.tileCountHorizontal)) + .setTileCountVertical(bundle.getInt(FIELD_TILE_COUNT_VERTICAL, DEFAULT.tileCountVertical)) // Source specific. .setCryptoType(bundle.getInt(FIELD_CRYPTO_TYPE, DEFAULT.cryptoType)); diff --git a/libraries/common/src/test/java/androidx/media3/common/FormatTest.java b/libraries/common/src/test/java/androidx/media3/common/FormatTest.java index ab656935ff6..e15d6fb5d1f 100644 --- a/libraries/common/src/test/java/androidx/media3/common/FormatTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/FormatTest.java @@ -111,6 +111,8 @@ private static Format createTestFormat() { .setEncoderPadding(1002) .setAccessibilityChannel(2) .setCryptoType(C.CRYPTO_TYPE_CUSTOM_BASE) + .setTileCountHorizontal(20) + .setTileCountVertical(40) .build(); } diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java index d9603d742fe..00a96572a87 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java @@ -1058,9 +1058,11 @@ private static long getAvailableStartTimeInManifestUs( for (int i = 0; i < period.adaptationSets.size(); i++) { AdaptationSet adaptationSet = period.adaptationSets.get(i); List representations = adaptationSet.representations; - // Exclude text adaptation sets from duration calculations, if we have at least one audio - // or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029 - if ((haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) + // Exclude other adaptation sets from duration calculations, if we have at least one audio or + // video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029. + boolean adaptationSetIsNotAudioVideo = + adaptationSet.type != C.TRACK_TYPE_AUDIO && adaptationSet.type != C.TRACK_TYPE_VIDEO; + if ((haveAudioVideoAdaptationSets && adaptationSetIsNotAudioVideo) || representations.isEmpty()) { continue; } @@ -1090,9 +1092,11 @@ private static long getAvailableEndTimeInManifestUs( for (int i = 0; i < period.adaptationSets.size(); i++) { AdaptationSet adaptationSet = period.adaptationSets.get(i); List representations = adaptationSet.representations; - // Exclude text adaptation sets from duration calculations, if we have at least one audio - // or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029 - if ((haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) + // Exclude other adaptation sets from duration calculations, if we have at least one audio or + // video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029 + boolean adaptationSetIsNotAudioVideo = + adaptationSet.type != C.TRACK_TYPE_AUDIO && adaptationSet.type != C.TRACK_TYPE_VIDEO; + if ((haveAudioVideoAdaptationSets && adaptationSetIsNotAudioVideo) || representations.isEmpty()) { continue; } diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java index c5006ad7b73..0e0bb927b92 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java @@ -557,7 +557,9 @@ protected AdaptationSet buildAdaptationSet( ? C.TRACK_TYPE_VIDEO : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? C.TRACK_TYPE_TEXT - : C.TRACK_TYPE_UNKNOWN; + : MimeTypes.BASE_TYPE_IMAGE.equals(contentType) + ? C.TRACK_TYPE_IMAGE + : C.TRACK_TYPE_UNKNOWN; } /** @@ -810,6 +812,7 @@ protected Format buildFormat( roleFlags |= parseRoleFlagsFromAccessibilityDescriptors(accessibilityDescriptors); roleFlags |= parseRoleFlagsFromProperties(essentialProperties); roleFlags |= parseRoleFlagsFromProperties(supplementalProperties); + @Nullable Pair tileCounts = parseTileCountFromProperties(essentialProperties); Format.Builder formatBuilder = new Format.Builder() @@ -820,7 +823,9 @@ protected Format buildFormat( .setPeakBitrate(bitrate) .setSelectionFlags(selectionFlags) .setRoleFlags(roleFlags) - .setLanguage(language); + .setLanguage(language) + .setTileCountHorizontal(tileCounts != null ? tileCounts.first : Format.NO_VALUE) + .setTileCountVertical(tileCounts != null ? tileCounts.second : Format.NO_VALUE); if (MimeTypes.isVideo(sampleMimeType)) { formatBuilder.setWidth(width).setHeight(height).setFrameRate(frameRate); @@ -1629,6 +1634,41 @@ protected String[] parseProfiles(XmlPullParser xpp, String attributeName, String return attributeValue.split(","); } + // Thumbnail tile information parsing + + /** + * Parses given descriptors for thumbnail tile information. + * + * @param essentialProperties List of descriptors that contain thumbnail tile information. + * @return A pair of Integer values, where the first is the count of horizontal tiles and the + * second is the count of vertical tiles, or null if no thumbnail tile information is found. + */ + @Nullable + protected Pair parseTileCountFromProperties( + List essentialProperties) { + for (int i = 0; i < essentialProperties.size(); i++) { + Descriptor descriptor = essentialProperties.get(i); + if ((Ascii.equalsIgnoreCase("http://dashif.org/thumbnail_tile", descriptor.schemeIdUri) + || Ascii.equalsIgnoreCase( + "http://dashif.org/guidelines/thumbnail_tile", descriptor.schemeIdUri)) + && descriptor.value != null) { + String size = descriptor.value; + String[] sizeSplit = Util.split(size, "x"); + if (sizeSplit.length != 2) { + continue; + } + try { + int tileCountHorizontal = Integer.parseInt(sizeSplit[0]); + int tileCountVertical = Integer.parseInt(sizeSplit[1]); + return Pair.create(tileCountHorizontal, tileCountVertical); + } catch (NumberFormatException e) { + // Ignore property if it's malformed. + } + } + } + return null; + } + // Utility methods. /** diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java index 04d53b1841b..29510717d7c 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java @@ -252,11 +252,19 @@ public void parseMediaPresentationDescription_images() throws IOException { ApplicationProvider.getApplicationContext(), SAMPLE_MPD_IMAGES)); AdaptationSet adaptationSet = manifest.getPeriod(0).adaptationSets.get(0); - Format format = adaptationSet.representations.get(0).format; - - assertThat(format.sampleMimeType).isEqualTo("image/jpeg"); - assertThat(format.width).isEqualTo(320); - assertThat(format.height).isEqualTo(180); + Format format0 = adaptationSet.representations.get(0).format; + Format format1 = adaptationSet.representations.get(1).format; + + assertThat(format0.sampleMimeType).isEqualTo("image/jpeg"); + assertThat(format0.width).isEqualTo(320); + assertThat(format0.height).isEqualTo(180); + assertThat(format0.tileCountHorizontal).isEqualTo(12); + assertThat(format0.tileCountVertical).isEqualTo(16); + assertThat(format1.sampleMimeType).isEqualTo("image/jpeg"); + assertThat(format1.width).isEqualTo(640); + assertThat(format1.height).isEqualTo(360); + assertThat(format1.tileCountHorizontal).isEqualTo(2); + assertThat(format1.tileCountVertical).isEqualTo(4); } @Test diff --git a/libraries/test_data/src/test/assets/media/mpd/sample_mpd_images b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_images index 981a29a23ae..7d0779e9570 100644 --- a/libraries/test_data/src/test/assets/media/mpd/sample_mpd_images +++ b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_images @@ -4,7 +4,10 @@ - + + + + From 065418cc28b91ab855425a097b5b1bae445d7c3b Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 1 Feb 2023 13:06:28 +0000 Subject: [PATCH 136/141] Publish ConcatenatingMediaSource2 Can be used to combine multiple media items into a single timeline window. Issue: androidx/media#247 Issue: google/ExoPlayer#4868 PiperOrigin-RevId: 506283307 (cherry picked from commit fcd3af6431cfcd79a3ee3cc4fee38e8db3c0554e) --- RELEASENOTES.md | 3 + .../source/ConcatenatingMediaSource2.java | 610 ++++++++++++ .../source/ConcatenatingMediaSource2Test.java | 911 ++++++++++++++++++ 3 files changed, 1524 insertions(+) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2.java create mode 100644 libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2Test.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a85995fac9e..f2d0c6587e6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,9 @@ for seeking. * Use theme when loading drawables on API 21+ ([#220](https://github.com/androidx/media/issues/220)). + * Add `ConcatenatingMediaSource2` that allows combining multiple media + items into a single window + ([#247](https://github.com/androidx/media/issues/247)). * Extractors: * Throw a ParserException instead of a NullPointerException if the sample table (stbl) is missing a required sample description (stsd) when diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2.java new file mode 100644 index 00000000000..7aacffc6f7f --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2.java @@ -0,0 +1,610 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.source; + +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import android.content.Context; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.util.Pair; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Player; +import androidx.media3.common.Timeline; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import androidx.media3.datasource.TransferListener; +import androidx.media3.exoplayer.upstream.Allocator; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.IdentityHashMap; + +/** + * Concatenates multiple {@link MediaSource MediaSources}, combining everything in one single {@link + * Timeline.Window}. + * + *

          This class can only be used under the following conditions: + * + *

            + *
          • All sources must be non-empty. + *
          • All {@link Timeline.Window Windows} defined by the sources, except the first, must have an + * {@link Timeline.Window#getPositionInFirstPeriodUs() period offset} of zero. This excludes, + * for example, live streams or {@link ClippingMediaSource} with a non-zero start position. + *
          + */ +@UnstableApi +public final class ConcatenatingMediaSource2 extends CompositeMediaSource { + + /** A builder for {@link ConcatenatingMediaSource2} instances. */ + public static final class Builder { + + private final ImmutableList.Builder mediaSourceHoldersBuilder; + + private int index; + @Nullable private MediaItem mediaItem; + @Nullable private MediaSource.Factory mediaSourceFactory; + + /** Creates the builder. */ + public Builder() { + mediaSourceHoldersBuilder = ImmutableList.builder(); + } + + /** + * Instructs the builder to use a {@link DefaultMediaSourceFactory} to convert {@link MediaItem + * MediaItems} to {@link MediaSource MediaSources} for all future calls to {@link + * #add(MediaItem)} or {@link #add(MediaItem, long)}. + * + * @param context A {@link Context}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder useDefaultMediaSourceFactory(Context context) { + return setMediaSourceFactory(new DefaultMediaSourceFactory(context)); + } + + /** + * Sets a {@link MediaSource.Factory} that is used to convert {@link MediaItem MediaItems} to + * {@link MediaSource MediaSources} for all future calls to {@link #add(MediaItem)} or {@link + * #add(MediaItem, long)}. + * + * @param mediaSourceFactory A {@link MediaSource.Factory}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) { + this.mediaSourceFactory = checkNotNull(mediaSourceFactory); + return this; + } + + /** + * Sets the {@link MediaItem} to be used for the concatenated media source. + * + *

          This {@link MediaItem} will be used as {@link Timeline.Window#mediaItem} for the + * concatenated source and will be returned by {@link Player#getCurrentMediaItem()}. + * + *

          The default is {@code MediaItem.fromUri(Uri.EMPTY)}. + * + * @param mediaItem The {@link MediaItem}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setMediaItem(MediaItem mediaItem) { + this.mediaItem = mediaItem; + return this; + } + + /** + * Adds a {@link MediaItem} to the concatenation. + * + *

          {@link #useDefaultMediaSourceFactory(Context)} or {@link + * #setMediaSourceFactory(MediaSource.Factory)} must be called before this method. + * + *

          This method must not be used with media items for progressive media that can't provide + * their duration with their first {@link Timeline} update. Use {@link #add(MediaItem, long)} + * instead. + * + * @param mediaItem The {@link MediaItem}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder add(MediaItem mediaItem) { + return add(mediaItem, /* initialPlaceholderDurationMs= */ C.TIME_UNSET); + } + + /** + * Adds a {@link MediaItem} to the concatenation and specifies its initial placeholder duration + * used while the actual duration is still unknown. + * + *

          {@link #useDefaultMediaSourceFactory(Context)} or {@link + * #setMediaSourceFactory(MediaSource.Factory)} must be called before this method. + * + *

          Setting a placeholder duration is required for media items for progressive media that + * can't provide their duration with their first {@link Timeline} update. It may also be used + * for other items to make the duration known immediately. + * + * @param mediaItem The {@link MediaItem}. + * @param initialPlaceholderDurationMs The initial placeholder duration in milliseconds used + * while the actual duration is still unknown, or {@link C#TIME_UNSET} to not define one. + * The placeholder duration is used for every {@link Timeline.Window} defined by {@link + * Timeline} of the {@link MediaItem}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder add(MediaItem mediaItem, long initialPlaceholderDurationMs) { + checkNotNull(mediaItem); + checkStateNotNull( + mediaSourceFactory, + "Must use useDefaultMediaSourceFactory or setMediaSourceFactory first."); + return add(mediaSourceFactory.createMediaSource(mediaItem), initialPlaceholderDurationMs); + } + + /** + * Adds a {@link MediaSource} to the concatenation. + * + *

          This method must not be used for sources like {@link ProgressiveMediaSource} that can't + * provide their duration with their first {@link Timeline} update. Use {@link #add(MediaSource, + * long)} instead. + * + * @param mediaSource The {@link MediaSource}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder add(MediaSource mediaSource) { + return add(mediaSource, /* initialPlaceholderDurationMs= */ C.TIME_UNSET); + } + + /** + * Adds a {@link MediaSource} to the concatenation and specifies its initial placeholder + * duration used while the actual duration is still unknown. + * + *

          Setting a placeholder duration is required for sources like {@link ProgressiveMediaSource} + * that can't provide their duration with their first {@link Timeline} update. It may also be + * used for other sources to make the duration known immediately. + * + * @param mediaSource The {@link MediaSource}. + * @param initialPlaceholderDurationMs The initial placeholder duration in milliseconds used + * while the actual duration is still unknown, or {@link C#TIME_UNSET} to not define one. + * The placeholder duration is used for every {@link Timeline.Window} defined by {@link + * Timeline} of the {@link MediaSource}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder add(MediaSource mediaSource, long initialPlaceholderDurationMs) { + checkNotNull(mediaSource); + checkState( + !(mediaSource instanceof ProgressiveMediaSource) + || initialPlaceholderDurationMs != C.TIME_UNSET, + "Progressive media source must define an initial placeholder duration."); + mediaSourceHoldersBuilder.add( + new MediaSourceHolder(mediaSource, index++, Util.msToUs(initialPlaceholderDurationMs))); + return this; + } + + /** Builds the concatenating media source. */ + public ConcatenatingMediaSource2 build() { + checkArgument(index > 0, "Must add at least one source to the concatenation."); + if (mediaItem == null) { + mediaItem = MediaItem.fromUri(Uri.EMPTY); + } + return new ConcatenatingMediaSource2(mediaItem, mediaSourceHoldersBuilder.build()); + } + } + + private static final int MSG_UPDATE_TIMELINE = 0; + + private final MediaItem mediaItem; + private final ImmutableList mediaSourceHolders; + private final IdentityHashMap mediaSourceByMediaPeriod; + + @Nullable private Handler playbackThreadHandler; + private boolean timelineUpdateScheduled; + + private ConcatenatingMediaSource2( + MediaItem mediaItem, ImmutableList mediaSourceHolders) { + this.mediaItem = mediaItem; + this.mediaSourceHolders = mediaSourceHolders; + mediaSourceByMediaPeriod = new IdentityHashMap<>(); + } + + @Nullable + @Override + public Timeline getInitialTimeline() { + return maybeCreateConcatenatedTimeline(); + } + + @Override + public MediaItem getMediaItem() { + return mediaItem; + } + + @Override + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + playbackThreadHandler = new Handler(/* callback= */ this::handleMessage); + for (int i = 0; i < mediaSourceHolders.size(); i++) { + MediaSourceHolder holder = mediaSourceHolders.get(i); + prepareChildSource(/* id= */ i, holder.mediaSource); + } + scheduleTimelineUpdate(); + } + + @SuppressWarnings("MissingSuperCall") + @Override + protected void enableInternal() { + // Suppress enabling all child sources here as they can be lazily enabled when creating periods. + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + int holderIndex = getChildIndex(id.periodUid); + MediaSourceHolder holder = mediaSourceHolders.get(holderIndex); + MediaPeriodId childMediaPeriodId = + id.copyWithPeriodUid(getChildPeriodUid(id.periodUid)) + .copyWithWindowSequenceNumber( + getChildWindowSequenceNumber( + id.windowSequenceNumber, mediaSourceHolders.size(), holder.index)); + enableChildSource(holder.index); + holder.activeMediaPeriods++; + MediaPeriod mediaPeriod = + holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); + mediaSourceByMediaPeriod.put(mediaPeriod, holder); + disableUnusedMediaSources(); + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + MediaSourceHolder holder = checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); + holder.mediaSource.releasePeriod(mediaPeriod); + holder.activeMediaPeriods--; + if (!mediaSourceByMediaPeriod.isEmpty()) { + disableUnusedMediaSources(); + } + } + + @Override + protected void releaseSourceInternal() { + super.releaseSourceInternal(); + if (playbackThreadHandler != null) { + playbackThreadHandler.removeCallbacksAndMessages(null); + playbackThreadHandler = null; + } + timelineUpdateScheduled = false; + } + + @Override + protected void onChildSourceInfoRefreshed( + Integer childSourceId, MediaSource mediaSource, Timeline newTimeline) { + scheduleTimelineUpdate(); + } + + @Override + @Nullable + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Integer childSourceId, MediaPeriodId mediaPeriodId) { + int childIndex = + getChildIndexFromChildWindowSequenceNumber( + mediaPeriodId.windowSequenceNumber, mediaSourceHolders.size()); + if (childSourceId != childIndex) { + // Ensure the reported media period id has the expected window sequence number. Otherwise it + // does not belong to this child source. + return null; + } + long windowSequenceNumber = + getWindowSequenceNumberFromChildWindowSequenceNumber( + mediaPeriodId.windowSequenceNumber, mediaSourceHolders.size()); + Object periodUid = getPeriodUid(childSourceId, mediaPeriodId.periodUid); + return mediaPeriodId + .copyWithPeriodUid(periodUid) + .copyWithWindowSequenceNumber(windowSequenceNumber); + } + + @Override + protected int getWindowIndexForChildWindowIndex(Integer childSourceId, int windowIndex) { + return 0; + } + + private boolean handleMessage(Message msg) { + if (msg.what == MSG_UPDATE_TIMELINE) { + updateTimeline(); + } + return true; + } + + private void scheduleTimelineUpdate() { + if (!timelineUpdateScheduled) { + checkNotNull(playbackThreadHandler).obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget(); + timelineUpdateScheduled = true; + } + } + + private void updateTimeline() { + timelineUpdateScheduled = false; + @Nullable ConcatenatedTimeline timeline = maybeCreateConcatenatedTimeline(); + if (timeline != null) { + refreshSourceInfo(timeline); + } + } + + private void disableUnusedMediaSources() { + for (int i = 0; i < mediaSourceHolders.size(); i++) { + MediaSourceHolder holder = mediaSourceHolders.get(i); + if (holder.activeMediaPeriods == 0) { + disableChildSource(holder.index); + } + } + } + + @Nullable + private ConcatenatedTimeline maybeCreateConcatenatedTimeline() { + Timeline.Window window = new Timeline.Window(); + Timeline.Period period = new Timeline.Period(); + ImmutableList.Builder timelinesBuilder = ImmutableList.builder(); + ImmutableList.Builder firstPeriodIndicesBuilder = ImmutableList.builder(); + ImmutableList.Builder periodOffsetsInWindowUsBuilder = ImmutableList.builder(); + int periodCount = 0; + boolean isSeekable = true; + boolean isDynamic = false; + long durationUs = 0; + long defaultPositionUs = 0; + long nextPeriodOffsetInWindowUs = 0; + boolean manifestsAreIdentical = true; + boolean hasInitialManifest = false; + @Nullable Object initialManifest = null; + for (int i = 0; i < mediaSourceHolders.size(); i++) { + MediaSourceHolder holder = mediaSourceHolders.get(i); + Timeline timeline = holder.mediaSource.getTimeline(); + checkArgument(!timeline.isEmpty(), "Can't concatenate empty child Timeline."); + timelinesBuilder.add(timeline); + firstPeriodIndicesBuilder.add(periodCount); + periodCount += timeline.getPeriodCount(); + for (int j = 0; j < timeline.getWindowCount(); j++) { + timeline.getWindow(/* windowIndex= */ j, window); + if (!hasInitialManifest) { + initialManifest = window.manifest; + hasInitialManifest = true; + } + manifestsAreIdentical = + manifestsAreIdentical && Util.areEqual(initialManifest, window.manifest); + + long windowDurationUs = window.durationUs; + if (windowDurationUs == C.TIME_UNSET) { + if (holder.initialPlaceholderDurationUs == C.TIME_UNSET) { + // Source duration isn't known yet and we have no placeholder duration. + return null; + } + windowDurationUs = holder.initialPlaceholderDurationUs; + } + durationUs += windowDurationUs; + if (holder.index == 0 && j == 0) { + defaultPositionUs = window.defaultPositionUs; + nextPeriodOffsetInWindowUs = -window.positionInFirstPeriodUs; + } else { + checkArgument( + window.positionInFirstPeriodUs == 0, + "Can't concatenate windows. A window has a non-zero offset in a period."); + } + // Assume placeholder windows are seekable to not prevent seeking in other periods. + isSeekable &= window.isSeekable || window.isPlaceholder; + isDynamic |= window.isDynamic; + } + int childPeriodCount = timeline.getPeriodCount(); + for (int j = 0; j < childPeriodCount; j++) { + periodOffsetsInWindowUsBuilder.add(nextPeriodOffsetInWindowUs); + timeline.getPeriod(/* periodIndex= */ j, period); + long periodDurationUs = period.durationUs; + if (periodDurationUs == C.TIME_UNSET) { + checkArgument( + childPeriodCount == 1, + "Can't concatenate multiple periods with unknown duration in one window."); + long windowDurationUs = + window.durationUs != C.TIME_UNSET + ? window.durationUs + : holder.initialPlaceholderDurationUs; + periodDurationUs = windowDurationUs + window.positionInFirstPeriodUs; + } + nextPeriodOffsetInWindowUs += periodDurationUs; + } + } + return new ConcatenatedTimeline( + mediaItem, + timelinesBuilder.build(), + firstPeriodIndicesBuilder.build(), + periodOffsetsInWindowUsBuilder.build(), + isSeekable, + isDynamic, + durationUs, + defaultPositionUs, + manifestsAreIdentical ? initialManifest : null); + } + + /** + * Returns the period uid for the concatenated source from the child index and child period uid. + */ + private static Object getPeriodUid(int childIndex, Object childPeriodUid) { + return Pair.create(childIndex, childPeriodUid); + } + + /** Returns the child index from the period uid of the concatenated source. */ + @SuppressWarnings("unchecked") + private static int getChildIndex(Object periodUid) { + return ((Pair) periodUid).first; + } + + /** Returns the uid of child period from the period uid of the concatenated source. */ + @SuppressWarnings("unchecked") + private static Object getChildPeriodUid(Object periodUid) { + return ((Pair) periodUid).second; + } + + /** Returns the window sequence number used for the child source. */ + private static long getChildWindowSequenceNumber( + long windowSequenceNumber, int childCount, int childIndex) { + return windowSequenceNumber * childCount + childIndex; + } + + /** Returns the index of the child source from a child window sequence number. */ + private static int getChildIndexFromChildWindowSequenceNumber( + long childWindowSequenceNumber, int childCount) { + return (int) (childWindowSequenceNumber % childCount); + } + + /** Returns the concatenated window sequence number from a child window sequence number. */ + private static long getWindowSequenceNumberFromChildWindowSequenceNumber( + long childWindowSequenceNumber, int childCount) { + return childWindowSequenceNumber / childCount; + } + + /* package */ static final class MediaSourceHolder { + + public final MaskingMediaSource mediaSource; + public final int index; + public final long initialPlaceholderDurationUs; + + public int activeMediaPeriods; + + public MediaSourceHolder( + MediaSource mediaSource, int index, long initialPlaceholderDurationUs) { + this.mediaSource = new MaskingMediaSource(mediaSource, /* useLazyPreparation= */ false); + this.index = index; + this.initialPlaceholderDurationUs = initialPlaceholderDurationUs; + } + } + + private static final class ConcatenatedTimeline extends Timeline { + + private final MediaItem mediaItem; + private final ImmutableList timelines; + private final ImmutableList firstPeriodIndices; + private final ImmutableList periodOffsetsInWindowUs; + private final boolean isSeekable; + private final boolean isDynamic; + private final long durationUs; + private final long defaultPositionUs; + @Nullable private final Object manifest; + + public ConcatenatedTimeline( + MediaItem mediaItem, + ImmutableList timelines, + ImmutableList firstPeriodIndices, + ImmutableList periodOffsetsInWindowUs, + boolean isSeekable, + boolean isDynamic, + long durationUs, + long defaultPositionUs, + @Nullable Object manifest) { + this.mediaItem = mediaItem; + this.timelines = timelines; + this.firstPeriodIndices = firstPeriodIndices; + this.periodOffsetsInWindowUs = periodOffsetsInWindowUs; + this.isSeekable = isSeekable; + this.isDynamic = isDynamic; + this.durationUs = durationUs; + this.defaultPositionUs = defaultPositionUs; + this.manifest = manifest; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public int getPeriodCount() { + return periodOffsetsInWindowUs.size(); + } + + @Override + public final Window getWindow( + int windowIndex, Window window, long defaultPositionProjectionUs) { + return window.set( + Window.SINGLE_WINDOW_UID, + mediaItem, + manifest, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, + isSeekable, + isDynamic, + /* liveConfiguration= */ null, + defaultPositionUs, + durationUs, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ getPeriodCount() - 1, + /* positionInFirstPeriodUs= */ -periodOffsetsInWindowUs.get(0)); + } + + @Override + public final Period getPeriodByUid(Object periodUid, Period period) { + int childIndex = getChildIndex(periodUid); + Object childPeriodUid = getChildPeriodUid(periodUid); + Timeline timeline = timelines.get(childIndex); + int periodIndex = + firstPeriodIndices.get(childIndex) + timeline.getIndexOfPeriod(childPeriodUid); + timeline.getPeriodByUid(childPeriodUid, period); + period.windowIndex = 0; + period.positionInWindowUs = periodOffsetsInWindowUs.get(periodIndex); + period.uid = periodUid; + return period; + } + + @Override + public final Period getPeriod(int periodIndex, Period period, boolean setIds) { + int childIndex = getChildIndexByPeriodIndex(periodIndex); + int firstPeriodIndexInChild = firstPeriodIndices.get(childIndex); + timelines.get(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds); + period.windowIndex = 0; + period.positionInWindowUs = periodOffsetsInWindowUs.get(periodIndex); + if (setIds) { + period.uid = getPeriodUid(childIndex, checkNotNull(period.uid)); + } + return period; + } + + @Override + public final int getIndexOfPeriod(Object uid) { + if (!(uid instanceof Pair) || !(((Pair) uid).first instanceof Integer)) { + return C.INDEX_UNSET; + } + int childIndex = getChildIndex(uid); + Object periodUid = getChildPeriodUid(uid); + int periodIndexInChild = timelines.get(childIndex).getIndexOfPeriod(periodUid); + return periodIndexInChild == C.INDEX_UNSET + ? C.INDEX_UNSET + : firstPeriodIndices.get(childIndex) + periodIndexInChild; + } + + @Override + public final Object getUidOfPeriod(int periodIndex) { + int childIndex = getChildIndexByPeriodIndex(periodIndex); + int firstPeriodIndexInChild = firstPeriodIndices.get(childIndex); + Object periodUidInChild = + timelines.get(childIndex).getUidOfPeriod(periodIndex - firstPeriodIndexInChild); + return getPeriodUid(childIndex, periodUidInChild); + } + + private int getChildIndexByPeriodIndex(int periodIndex) { + return Util.binarySearchFloor( + firstPeriodIndices, periodIndex + 1, /* inclusive= */ false, /* stayInBounds= */ false); + } + } +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2Test.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2Test.java new file mode 100644 index 00000000000..14d4e943063 --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2Test.java @@ -0,0 +1,911 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.source; + +import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil; +import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState; +import static com.google.common.truth.Truth.assertThat; +import static java.lang.Math.max; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.util.Pair; +import androidx.annotation.Nullable; +import androidx.media3.common.AdPlaybackState; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.Player; +import androidx.media3.common.Timeline; +import androidx.media3.common.util.Util; +import androidx.media3.datasource.TransferListener; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.analytics.PlayerId; +import androidx.media3.exoplayer.util.EventLogger; +import androidx.media3.test.utils.FakeMediaSource; +import androidx.media3.test.utils.FakeTimeline; +import androidx.media3.test.utils.TestExoPlayerBuilder; +import androidx.test.core.app.ApplicationProvider; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; + +/** Unit tests for {@link ConcatenatingMediaSource2}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class ConcatenatingMediaSource2Test { + + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") + public static ImmutableList params() { + ImmutableList.Builder builder = ImmutableList.builder(); + + // Full example with an offset in the initial window, MediaSource with multiple windows and + // periods, and sources with ad insertion. + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ 123, /* adGroupTimesUs...= */ 0, 300_000) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1) + .withAdDurationsUs(new long[][] {new long[] {2_000_000}, new long[] {4_000_000}}); + builder.add( + new TestConfig( + "initial_offset_multiple_windows_and_ads", + buildConcatenatingMediaSource( + buildMediaSource( + buildWindow( + /* periodCount= */ 2, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationMs= */ 1000, + /* defaultPositionMs= */ 123, + /* windowOffsetInFirstPeriodMs= */ 50), + buildWindow( + /* periodCount= */ 2, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* durationMs= */ 2500)), + buildMediaSource( + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationMs= */ 500, + adPlaybackState)), + buildMediaSource( + buildWindow( + /* periodCount= */ 3, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationMs= */ 1800))), + /* expectedAdDiscontinuities= */ 3, + new ExpectedTimelineData( + /* isSeekable= */ false, + /* isDynamic= */ false, + /* defaultPositionMs= */ 123, + /* periodDurationsMs= */ new long[] {550, 500, 1250, 1250, 500, 600, 600, 600}, + /* periodOffsetsInWindowMs= */ new long[] { + -50, 500, 1000, 2250, 3500, 4000, 4600, 5200 + }, + /* periodIsPlaceholder= */ new boolean[] { + false, false, false, false, false, false, false, false + }, + /* windowDurationMs= */ 5800, + /* manifest= */ null) + .withAdPlaybackState(/* periodIndex= */ 4, adPlaybackState))); + + builder.add( + new TestConfig( + "multipleMediaSource_sameManifest", + buildConcatenatingMediaSource( + buildMediaSource( + new Object[] {"manifest"}, + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationMs= */ 1000)), + buildMediaSource( + new Object[] {"manifest"}, + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationMs= */ 1000))), + /* expectedAdDiscontinuities= */ 0, + new ExpectedTimelineData( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* defaultPositionMs= */ 0, + /* periodDurationsMs= */ new long[] {1000, 1000}, + /* periodOffsetsInWindowMs= */ new long[] {0, 1000}, + /* periodIsPlaceholder= */ new boolean[] {false, false}, + /* windowDurationMs= */ 2000, + /* manifest= */ "manifest"))); + + builder.add( + new TestConfig( + "multipleMediaSource_differentManifest", + buildConcatenatingMediaSource( + buildMediaSource( + new Object[] {"manifest1"}, + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationMs= */ 1000)), + buildMediaSource( + new Object[] {"manifest2"}, + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationMs= */ 1000))), + /* expectedAdDiscontinuities= */ 0, + new ExpectedTimelineData( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* defaultPositionMs= */ 0, + /* periodDurationsMs= */ new long[] {1000, 1000}, + /* periodOffsetsInWindowMs= */ new long[] {0, 1000}, + /* periodIsPlaceholder= */ new boolean[] {false, false}, + /* windowDurationMs= */ 2000, + /* manifest= */ null))); + + // Counter-example for isSeekable and isDynamic. + builder.add( + new TestConfig( + "isSeekable_isDynamic_counter_example", + buildConcatenatingMediaSource( + buildMediaSource( + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationMs= */ 1000)), + buildMediaSource( + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationMs= */ 500))), + /* expectedAdDiscontinuities= */ 0, + new ExpectedTimelineData( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* defaultPositionMs= */ 0, + /* periodDurationsMs= */ new long[] {1000, 500}, + /* periodOffsetsInWindowMs= */ new long[] {0, 1000}, + /* periodIsPlaceholder= */ new boolean[] {false, false}, + /* windowDurationMs= */ 1500, + /* manifest= */ null))); + + // Unknown window and period durations. + builder.add( + new TestConfig( + "unknown_window_and_period_durations", + buildConcatenatingMediaSource( + /* placeholderDurationMs= */ 420, + buildMediaSource( + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* durationMs= */ C.TIME_UNSET, + /* defaultPositionMs= */ 123, + /* windowOffsetInFirstPeriodMs= */ 50)), + buildMediaSource( + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationMs= */ C.TIME_UNSET))), + /* expectedAdDiscontinuities= */ 0, + new ExpectedTimelineData( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* defaultPositionMs= */ 0, + /* periodDurationsMs= */ new long[] {C.TIME_UNSET, C.TIME_UNSET}, + /* periodOffsetsInWindowMs= */ new long[] {0, 420}, + /* periodIsPlaceholder= */ new boolean[] {true, true}, + /* windowDurationMs= */ 840, + /* manifest= */ null), + new ExpectedTimelineData( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* defaultPositionMs= */ 123, + /* periodDurationsMs= */ new long[] {C.TIME_UNSET, C.TIME_UNSET}, + /* periodOffsetsInWindowMs= */ new long[] {-50, 420}, + /* periodIsPlaceholder= */ new boolean[] {false, false}, + /* windowDurationMs= */ 840, + /* manifest= */ null))); + + // Duplicate sources and nested concatenation. + builder.add( + new TestConfig( + "duplicated_and_nested_sources", + () -> { + MediaSource duplicateSource = + buildMediaSource( + buildWindow( + /* periodCount= */ 2, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationMs= */ 1000)) + .get(); + Supplier duplicateSourceSupplier = () -> duplicateSource; + return buildConcatenatingMediaSource( + duplicateSourceSupplier, + buildConcatenatingMediaSource( + duplicateSourceSupplier, duplicateSourceSupplier), + buildConcatenatingMediaSource( + duplicateSourceSupplier, duplicateSourceSupplier), + duplicateSourceSupplier) + .get(); + }, + /* expectedAdDiscontinuities= */ 0, + new ExpectedTimelineData( + /* isSeekable= */ true, + /* isDynamic= */ false, + /* defaultPositionMs= */ 0, + /* periodDurationsMs= */ new long[] { + 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500 + }, + /* periodOffsetsInWindowMs= */ new long[] { + 0, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 5500 + }, + /* periodIsPlaceholder= */ new boolean[] { + false, false, false, false, false, false, false, false, false, false, false, false + }, + /* windowDurationMs= */ 6000, + /* manifest= */ null))); + + // Concatenation with initial placeholder durations and delayed timeline updates. + builder.add( + new TestConfig( + "initial_placeholder_and_delayed_preparation", + buildConcatenatingMediaSource( + /* placeholderDurationMs= */ 5000, + buildMediaSource( + /* preparationDelayCount= */ 1, + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationMs= */ 4000, + /* defaultPositionMs= */ 123, + /* windowOffsetInFirstPeriodMs= */ 50)), + buildMediaSource( + /* preparationDelayCount= */ 3, + buildWindow( + /* periodCount= */ 2, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationMs= */ 7000)), + buildMediaSource( + /* preparationDelayCount= */ 2, + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* durationMs= */ 6000))), + /* expectedAdDiscontinuities= */ 0, + new ExpectedTimelineData( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* defaultPositionMs= */ 0, + /* periodDurationsMs= */ new long[] {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}, + /* periodOffsetsInWindowMs= */ new long[] {0, 5000, 10000}, + /* periodIsPlaceholder= */ new boolean[] {true, true, true}, + /* windowDurationMs= */ 15000, + /* manifest= */ null), + new ExpectedTimelineData( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* defaultPositionMs= */ 123, + /* periodDurationsMs= */ new long[] {4050, C.TIME_UNSET, C.TIME_UNSET}, + /* periodOffsetsInWindowMs= */ new long[] {-50, 4000, 9000}, + /* periodIsPlaceholder= */ new boolean[] {false, true, true}, + /* windowDurationMs= */ 14000, + /* manifest= */ null), + new ExpectedTimelineData( + /* isSeekable= */ false, + /* isDynamic= */ true, + /* defaultPositionMs= */ 123, + /* periodDurationsMs= */ new long[] {4050, C.TIME_UNSET, 6000}, + /* periodOffsetsInWindowMs= */ new long[] {-50, 4000, 9000}, + /* periodIsPlaceholder= */ new boolean[] {false, true, false}, + /* windowDurationMs= */ 15000, + /* manifest= */ null), + new ExpectedTimelineData( + /* isSeekable= */ false, + /* isDynamic= */ false, + /* defaultPositionMs= */ 123, + /* periodDurationsMs= */ new long[] {4050, 3500, 3500, 6000}, + /* periodOffsetsInWindowMs= */ new long[] {-50, 4000, 7500, 11000}, + /* periodIsPlaceholder= */ new boolean[] {false, false, false, false}, + /* windowDurationMs= */ 17000, + /* manifest= */ null))); + + // Concatenation with initial placeholder durations and some immediate timeline updates. + builder.add( + new TestConfig( + "initial_placeholder_and_immediate_partial_preparation", + buildConcatenatingMediaSource( + /* placeholderDurationMs= */ 5000, + buildMediaSource( + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationMs= */ 4000, + /* defaultPositionMs= */ 123, + /* windowOffsetInFirstPeriodMs= */ 50)), + buildMediaSource( + /* preparationDelayCount= */ 1, + buildWindow( + /* periodCount= */ 2, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationMs= */ 7000)), + buildMediaSource( + buildWindow( + /* periodCount= */ 1, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* durationMs= */ 6000))), + /* expectedAdDiscontinuities= */ 0, + new ExpectedTimelineData( + /* isSeekable= */ true, + /* isDynamic= */ true, + /* defaultPositionMs= */ 0, + /* periodDurationsMs= */ new long[] {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET}, + /* periodOffsetsInWindowMs= */ new long[] {0, 5000, 10000}, + /* periodIsPlaceholder= */ new boolean[] {true, true, true}, + /* windowDurationMs= */ 15000, + /* manifest= */ null), + new ExpectedTimelineData( + /* isSeekable= */ false, + /* isDynamic= */ true, + /* defaultPositionMs= */ 123, + /* periodDurationsMs= */ new long[] {4050, C.TIME_UNSET, 6000}, + /* periodOffsetsInWindowMs= */ new long[] {-50, 4000, 9000}, + /* periodIsPlaceholder= */ new boolean[] {false, true, false}, + /* windowDurationMs= */ 15000, + /* manifest= */ null), + new ExpectedTimelineData( + /* isSeekable= */ false, + /* isDynamic= */ false, + /* defaultPositionMs= */ 123, + /* periodDurationsMs= */ new long[] {4050, 3500, 3500, 6000}, + /* periodOffsetsInWindowMs= */ new long[] {-50, 4000, 7500, 11000}, + /* periodIsPlaceholder= */ new boolean[] {false, false, false, false}, + /* windowDurationMs= */ 17000, + /* manifest= */ null))); + return builder.build(); + } + + @ParameterizedRobolectricTestRunner.Parameter public TestConfig config; + + private static final String TEST_MEDIA_ITEM_ID = "test_media_item_id"; + + @Test + public void prepareSource_reportsExpectedTimelines() throws Exception { + MediaSource mediaSource = config.mediaSourceSupplier.get(); + ArrayList timelines = new ArrayList<>(); + mediaSource.prepareSource( + (source, timeline) -> timelines.add(timeline), + /* mediaTransferListener= */ null, + PlayerId.UNSET); + runMainLooperUntil(() -> timelines.size() == config.expectedTimelineData.size()); + + for (int i = 0; i < config.expectedTimelineData.size(); i++) { + Timeline timeline = timelines.get(i); + ExpectedTimelineData expectedData = config.expectedTimelineData.get(i); + assertThat(timeline.getWindowCount()).isEqualTo(1); + assertThat(timeline.getPeriodCount()).isEqualTo(expectedData.periodDurationsMs.length); + + Timeline.Window window = timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()); + assertThat(window.getDurationMs()).isEqualTo(expectedData.windowDurationMs); + assertThat(window.isDynamic).isEqualTo(expectedData.isDynamic); + assertThat(window.isSeekable).isEqualTo(expectedData.isSeekable); + assertThat(window.getDefaultPositionMs()).isEqualTo(expectedData.defaultPositionMs); + assertThat(window.getPositionInFirstPeriodMs()) + .isEqualTo(-expectedData.periodOffsetsInWindowMs[0]); + assertThat(window.firstPeriodIndex).isEqualTo(0); + assertThat(window.lastPeriodIndex).isEqualTo(expectedData.periodDurationsMs.length - 1); + assertThat(window.uid).isEqualTo(Timeline.Window.SINGLE_WINDOW_UID); + assertThat(window.mediaItem.mediaId).isEqualTo(TEST_MEDIA_ITEM_ID); + assertThat(window.isPlaceholder).isFalse(); + assertThat(window.elapsedRealtimeEpochOffsetMs).isEqualTo(C.TIME_UNSET); + assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.liveConfiguration).isNull(); + assertThat(window.manifest).isEqualTo(expectedData.manifest); + + HashSet uidSet = new HashSet<>(); + for (int j = 0; j < timeline.getPeriodCount(); j++) { + Timeline.Period period = + timeline.getPeriod(/* periodIndex= */ j, new Timeline.Period(), /* setIds= */ true); + assertThat(period.getDurationMs()).isEqualTo(expectedData.periodDurationsMs[j]); + assertThat(period.windowIndex).isEqualTo(0); + assertThat(period.getPositionInWindowMs()) + .isEqualTo(expectedData.periodOffsetsInWindowMs[j]); + assertThat(period.isPlaceholder).isEqualTo(expectedData.periodIsPlaceholder[j]); + uidSet.add(period.uid); + assertThat(timeline.getIndexOfPeriod(period.uid)).isEqualTo(j); + assertThat(timeline.getUidOfPeriod(j)).isEqualTo(period.uid); + assertThat(timeline.getPeriodByUid(period.uid, new Timeline.Period())).isEqualTo(period); + } + assertThat(uidSet).hasSize(timeline.getPeriodCount()); + } + } + + @Test + public void prepareSource_afterRelease_reportsSameFinalTimeline() throws Exception { + // Fully prepare source once. + MediaSource mediaSource = config.mediaSourceSupplier.get(); + ArrayList timelines = new ArrayList<>(); + MediaSource.MediaSourceCaller caller = (source, timeline) -> timelines.add(timeline); + mediaSource.prepareSource(caller, /* mediaTransferListener= */ null, PlayerId.UNSET); + runMainLooperUntil(() -> timelines.size() == config.expectedTimelineData.size()); + + // Release and re-prepare. + mediaSource.releaseSource(caller); + AtomicReference secondTimeline = new AtomicReference<>(); + MediaSource.MediaSourceCaller secondCaller = (source, timeline) -> secondTimeline.set(timeline); + mediaSource.prepareSource(secondCaller, /* mediaTransferListener= */ null, PlayerId.UNSET); + + // Assert that we receive the same final timeline. + runMainLooperUntil(() -> Iterables.getLast(timelines).equals(secondTimeline.get())); + } + + @Test + public void preparePeriod_reportsExpectedPeriodLoadEvents() throws Exception { + // Prepare source and register listener. + MediaSource mediaSource = config.mediaSourceSupplier.get(); + MediaSourceEventListener eventListener = mock(MediaSourceEventListener.class); + mediaSource.addEventListener(new Handler(Looper.myLooper()), eventListener); + ArrayList timelines = new ArrayList<>(); + mediaSource.prepareSource( + (source, timeline) -> timelines.add(timeline), + /* mediaTransferListener= */ null, + PlayerId.UNSET); + runMainLooperUntil(() -> timelines.size() == config.expectedTimelineData.size()); + + // Iterate through all periods and ads. Create and prepare them twice, because the MediaSource + // should support creating the same period more than once. + ArrayList mediaPeriods = new ArrayList<>(); + ArrayList mediaPeriodIds = new ArrayList<>(); + Timeline timeline = Iterables.getLast(timelines); + for (int i = 0; i < timeline.getPeriodCount(); i++) { + Timeline.Period period = + timeline.getPeriod(/* periodIndex= */ i, new Timeline.Period(), /* setIds= */ true); + MediaSource.MediaPeriodId mediaPeriodId = + new MediaSource.MediaPeriodId(period.uid, /* windowSequenceNumber= */ 15); + MediaPeriod mediaPeriod = + mediaSource.createPeriod(mediaPeriodId, /* allocator= */ null, /* startPositionUs= */ 0); + blockingPrepareMediaPeriod(mediaPeriod); + mediaPeriods.add(mediaPeriod); + mediaPeriodIds.add(mediaPeriodId); + + mediaPeriodId = mediaPeriodId.copyWithWindowSequenceNumber(/* windowSequenceNumber= */ 25); + mediaPeriod = + mediaSource.createPeriod(mediaPeriodId, /* allocator= */ null, /* startPositionUs= */ 0); + blockingPrepareMediaPeriod(mediaPeriod); + mediaPeriods.add(mediaPeriod); + mediaPeriodIds.add(mediaPeriodId); + + for (int j = 0; j < period.getAdGroupCount(); j++) { + for (int k = 0; k < period.getAdCountInAdGroup(j); k++) { + mediaPeriodId = + new MediaSource.MediaPeriodId( + period.uid, + /* adGroupIndex= */ j, + /* adIndexInAdGroup= */ k, + /* windowSequenceNumber= */ 35); + mediaPeriod = + mediaSource.createPeriod( + mediaPeriodId, /* allocator= */ null, /* startPositionUs= */ 0); + blockingPrepareMediaPeriod(mediaPeriod); + mediaPeriods.add(mediaPeriod); + mediaPeriodIds.add(mediaPeriodId); + + mediaPeriodId = + mediaPeriodId.copyWithWindowSequenceNumber(/* windowSequenceNumber= */ 45); + mediaPeriod = + mediaSource.createPeriod( + mediaPeriodId, /* allocator= */ null, /* startPositionUs= */ 0); + blockingPrepareMediaPeriod(mediaPeriod); + mediaPeriods.add(mediaPeriod); + mediaPeriodIds.add(mediaPeriodId); + } + } + } + // Release all periods again. + for (MediaPeriod mediaPeriod : mediaPeriods) { + mediaSource.releasePeriod(mediaPeriod); + } + + // Verify each load started and completed event is called with the correct mediaPeriodId. + for (MediaSource.MediaPeriodId mediaPeriodId : mediaPeriodIds) { + verify(eventListener) + .onLoadStarted( + /* windowIndex= */ eq(0), + /* mediaPeriodId= */ eq(mediaPeriodId), + /* loadEventInfo= */ any(), + /* mediaLoadData= */ any()); + verify(eventListener) + .onLoadCompleted( + /* windowIndex= */ eq(0), + /* mediaPeriodId= */ eq(mediaPeriodId), + /* loadEventInfo= */ any(), + /* mediaLoadData= */ any()); + } + } + + @Test + public void playback_fromDefaultPosition_startsFromCorrectPositionAndPlaysToEnd() + throws Exception { + ExoPlayer player = + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); + player.setMediaSource(config.mediaSourceSupplier.get()); + Player.Listener eventListener = mock(Player.Listener.class); + player.addListener(eventListener); + player.addAnalyticsListener(new EventLogger()); + + player.prepare(); + runUntilPlaybackState(player, Player.STATE_READY); + long positionAfterPrepareMs = player.getCurrentPosition(); + boolean isDynamic = player.isCurrentMediaItemDynamic(); + if (!isDynamic) { + // Dynamic streams never enter the ENDED state. + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); + } + player.release(); + + ExpectedTimelineData expectedData = Iterables.getLast(config.expectedTimelineData); + assertThat(positionAfterPrepareMs).isEqualTo(expectedData.defaultPositionMs); + if (!isDynamic) { + verify( + eventListener, + times(config.expectedAdDiscontinuities + expectedData.periodDurationsMs.length - 1)) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION)); + } + } + + @Test + public void + playback_fromSpecificPeriodPositionInFirstPeriod_startsFromCorrectPositionAndPlaysToEnd() + throws Exception { + ExoPlayer player = + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); + MediaSource mediaSource = config.mediaSourceSupplier.get(); + player.setMediaSource(mediaSource); + Player.Listener eventListener = mock(Player.Listener.class); + player.addListener(eventListener); + player.addAnalyticsListener(new EventLogger()); + + long startWindowPositionMs = 24; + player.seekTo(startWindowPositionMs); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_READY); + long windowPositionAfterPrepareMs = player.getCurrentPosition(); + boolean isDynamic = player.isCurrentMediaItemDynamic(); + if (!isDynamic) { + // Dynamic streams never enter the ENDED state. + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); + } + player.release(); + + ExpectedTimelineData expectedData = Iterables.getLast(config.expectedTimelineData); + assertThat(windowPositionAfterPrepareMs).isEqualTo(startWindowPositionMs); + if (!isDynamic) { + verify( + eventListener, + times(expectedData.periodDurationsMs.length - 1 + config.expectedAdDiscontinuities)) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION)); + } + } + + @Test + public void + playback_fromSpecificPeriodPositionInSubsequentPeriod_startsFromCorrectPositionAndPlaysToEnd() + throws Exception { + Timeline.Period period = new Timeline.Period(); + Timeline.Window window = new Timeline.Window(); + ExoPlayer player = + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); + MediaSource mediaSource = config.mediaSourceSupplier.get(); + player.setMediaSource(mediaSource); + Player.Listener eventListener = mock(Player.Listener.class); + player.addListener(eventListener); + player.addAnalyticsListener(new EventLogger()); + + ExpectedTimelineData initialTimelineData = config.expectedTimelineData.get(0); + int startPeriodIndex = max(1, initialTimelineData.periodDurationsMs.length - 2); + long startPeriodPositionMs = 24; + long startWindowPositionMs = + initialTimelineData.periodOffsetsInWindowMs[startPeriodIndex] + startPeriodPositionMs; + player.seekTo(startWindowPositionMs); + player.prepare(); + runUntilPlaybackState(player, Player.STATE_READY); + Timeline timeline = player.getCurrentTimeline(); + long windowPositionAfterPrepareMs = player.getContentPosition(); + Pair periodPositionUs = + timeline.getPeriodPositionUs(window, period, 0, Util.msToUs(windowPositionAfterPrepareMs)); + int periodIndexAfterPrepare = timeline.getIndexOfPeriod(periodPositionUs.first); + long periodPositionAfterPrepareMs = Util.usToMs(periodPositionUs.second); + boolean isDynamic = player.isCurrentMediaItemDynamic(); + if (!isDynamic) { + // Dynamic streams never enter the ENDED state. + player.play(); + runUntilPlaybackState(player, Player.STATE_ENDED); + } + player.release(); + + ExpectedTimelineData expectedData = Iterables.getLast(config.expectedTimelineData); + assertThat(periodPositionAfterPrepareMs).isEqualTo(startPeriodPositionMs); + if (timeline.getPeriod(periodIndexAfterPrepare, period).getAdGroupCount() == 0) { + assertThat(periodIndexAfterPrepare).isEqualTo(startPeriodIndex); + if (!isDynamic) { + verify(eventListener, times(expectedData.periodDurationsMs.length - startPeriodIndex - 1)) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION)); + } + } else { + // Seek beyond ad period: assert roll forward to un-played ad period. + assertThat(periodIndexAfterPrepare).isLessThan(startPeriodIndex); + verify(eventListener, atLeast(expectedData.periodDurationsMs.length - startPeriodIndex - 1)) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION)); + timeline.getPeriod(periodIndexAfterPrepare, period); + assertThat(period.getAdGroupIndexForPositionUs(period.durationUs)) + .isNotEqualTo(C.INDEX_UNSET); + } + } + + private static void blockingPrepareMediaPeriod(MediaPeriod mediaPeriod) { + ConditionVariable mediaPeriodPrepared = new ConditionVariable(); + mediaPeriod.prepare( + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + mediaPeriodPrepared.open(); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + mediaPeriod.continueLoading(/* positionUs= */ 0); + } + }, + /* positionUs= */ 0); + mediaPeriodPrepared.block(); + } + + private static Supplier buildConcatenatingMediaSource( + Supplier... sources) { + return buildConcatenatingMediaSource(/* placeholderDurationMs= */ C.TIME_UNSET, sources); + } + + private static Supplier buildConcatenatingMediaSource( + long placeholderDurationMs, Supplier... sources) { + return () -> { + ConcatenatingMediaSource2.Builder builder = new ConcatenatingMediaSource2.Builder(); + builder.setMediaItem(new MediaItem.Builder().setMediaId(TEST_MEDIA_ITEM_ID).build()); + for (Supplier source : sources) { + builder.add(source.get(), placeholderDurationMs); + } + return builder.build(); + }; + } + + private static Supplier buildMediaSource( + FakeTimeline.TimelineWindowDefinition... windows) { + return buildMediaSource(/* preparationDelayCount= */ 0, windows); + } + + private static Supplier buildMediaSource( + int preparationDelayCount, FakeTimeline.TimelineWindowDefinition... windows) { + return buildMediaSource(preparationDelayCount, /* manifests= */ null, windows); + } + + private static Supplier buildMediaSource( + Object[] manifests, FakeTimeline.TimelineWindowDefinition... windows) { + return buildMediaSource(/* preparationDelayCount= */ 0, manifests, windows); + } + + private static Supplier buildMediaSource( + int preparationDelayCount, + @Nullable Object[] manifests, + FakeTimeline.TimelineWindowDefinition... windows) { + + return () -> { + // Simulate delay by repeatedly sending messages to self. This ensures that all other message + // handling trigger by source preparation finishes before the new timeline update arrives. + AtomicInteger delayCount = new AtomicInteger(10 * preparationDelayCount); + return new FakeMediaSource( + /* timeline= */ null, + new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()) { + @Override + public synchronized void prepareSourceInternal( + @Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + Handler delayHandler = new Handler(Looper.myLooper()); + Runnable handleDelay = + new Runnable() { + @Override + public void run() { + if (delayCount.getAndDecrement() == 0) { + setNewSourceInfo( + manifests != null + ? new FakeTimeline(manifests, windows) + : new FakeTimeline(windows)); + } else { + delayHandler.post(this); + } + } + }; + delayHandler.post(handleDelay); + } + }; + }; + } + + private static FakeTimeline.TimelineWindowDefinition buildWindow( + int periodCount, boolean isSeekable, boolean isDynamic, long durationMs) { + return buildWindow( + periodCount, + isSeekable, + isDynamic, + durationMs, + /* defaultPositionMs= */ 0, + /* windowOffsetInFirstPeriodMs= */ 0); + } + + private static FakeTimeline.TimelineWindowDefinition buildWindow( + int periodCount, + boolean isSeekable, + boolean isDynamic, + long durationMs, + long defaultPositionMs, + long windowOffsetInFirstPeriodMs) { + return buildWindow( + periodCount, + isSeekable, + isDynamic, + durationMs, + defaultPositionMs, + windowOffsetInFirstPeriodMs, + AdPlaybackState.NONE); + } + + private static FakeTimeline.TimelineWindowDefinition buildWindow( + int periodCount, + boolean isSeekable, + boolean isDynamic, + long durationMs, + AdPlaybackState adPlaybackState) { + return buildWindow( + periodCount, + isSeekable, + isDynamic, + durationMs, + /* defaultPositionMs= */ 0, + /* windowOffsetInFirstPeriodMs= */ 0, + adPlaybackState); + } + + private static FakeTimeline.TimelineWindowDefinition buildWindow( + int periodCount, + boolean isSeekable, + boolean isDynamic, + long durationMs, + long defaultPositionMs, + long windowOffsetInFirstPeriodMs, + AdPlaybackState adPlaybackState) { + return new FakeTimeline.TimelineWindowDefinition( + periodCount, + /* id= */ new Object(), + isSeekable, + isDynamic, + /* isLive= */ false, + /* isPlaceholder= */ false, + Util.msToUs(durationMs), + Util.msToUs(defaultPositionMs), + Util.msToUs(windowOffsetInFirstPeriodMs), + ImmutableList.of(adPlaybackState), + new MediaItem.Builder().setMediaId("").build()); + } + + private static final class TestConfig { + + public final Supplier mediaSourceSupplier; + public final ImmutableList expectedTimelineData; + + private final int expectedAdDiscontinuities; + private final String tag; + + public TestConfig( + String tag, + Supplier mediaSourceSupplier, + int expectedAdDiscontinuities, + ExpectedTimelineData... expectedTimelineData) { + this.tag = tag; + this.mediaSourceSupplier = mediaSourceSupplier; + this.expectedTimelineData = ImmutableList.copyOf(expectedTimelineData); + this.expectedAdDiscontinuities = expectedAdDiscontinuities; + } + + @Override + public String toString() { + return tag; + } + } + + private static final class ExpectedTimelineData { + + public final boolean isSeekable; + public final boolean isDynamic; + public final long defaultPositionMs; + public final long[] periodDurationsMs; + public final long[] periodOffsetsInWindowMs; + public final boolean[] periodIsPlaceholder; + public final long windowDurationMs; + public final AdPlaybackState[] adPlaybackState; + @Nullable public final Object manifest; + + public ExpectedTimelineData( + boolean isSeekable, + boolean isDynamic, + long defaultPositionMs, + long[] periodDurationsMs, + long[] periodOffsetsInWindowMs, + boolean[] periodIsPlaceholder, + long windowDurationMs, + @Nullable Object manifest) { + this.isSeekable = isSeekable; + this.isDynamic = isDynamic; + this.defaultPositionMs = defaultPositionMs; + this.periodDurationsMs = periodDurationsMs; + this.periodOffsetsInWindowMs = periodOffsetsInWindowMs; + this.periodIsPlaceholder = periodIsPlaceholder; + this.windowDurationMs = windowDurationMs; + this.adPlaybackState = new AdPlaybackState[periodDurationsMs.length]; + this.manifest = manifest; + } + + @CanIgnoreReturnValue + public ExpectedTimelineData withAdPlaybackState( + int periodIndex, AdPlaybackState adPlaybackState) { + this.adPlaybackState[periodIndex] = adPlaybackState; + return this; + } + } +} From 9bf18dbb4e84bc54665335fa00261c8a2928807b Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 2 Feb 2023 14:47:38 +0000 Subject: [PATCH 137/141] Session: advertise legacy FLAG_HANDLES_QUEUE_COMMANDS This change includes 3 things: - when the legacy media session is created, FLAG_HANDLES_QUEUE_COMMANDS is advertised if the player has the COMMAND_CHANGE_MEDIA_ITEMS available. - when the player changes its available commands, a new PlaybackStateCompat is sent to the remote media controller to advertise the updated PlyabackStateCompat actions. - when the player changes its available commands, the legacy media session flags are sent accoridingly: FLAG_HANDLES_QUEUE_COMMANDS is set only if the COMMAND_CHANGE_MEDIA_ITEMS is available. #minor-release PiperOrigin-RevId: 506605905 (cherry picked from commit ebe7ece1eb7e2106bc9fff02db2666410d3e0aa8) --- .../session/MediaSessionLegacyStub.java | 22 +- ...tateCompatActionsWithMediaSessionTest.java | 214 ++++++++++++++++++ 2 files changed, 234 insertions(+), 2 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index b7aa79e8cfe..13cf696db08 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -126,6 +126,7 @@ private volatile long connectionTimeoutMs; @Nullable private FutureCallback pendingBitmapLoadCallback; + private int sessionFlags; public MediaSessionLegacyStub( MediaSessionImpl session, @@ -161,8 +162,6 @@ public MediaSessionLegacyStub( sessionCompat.setSessionActivity(sessionActivity); } - sessionCompat.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS); - @SuppressWarnings("nullness:assignment") @Initialized MediaSessionLegacyStub thisRef = this; @@ -254,6 +253,17 @@ public boolean onMediaButtonEvent(Intent mediaButtonEvent) { return false; } + private void maybeUpdateFlags(PlayerWrapper playerWrapper) { + int newFlags = + playerWrapper.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS) + ? MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS + : 0; + if (sessionFlags != newFlags) { + sessionFlags = newFlags; + sessionCompat.setFlags(sessionFlags); + } + } + private void handleMediaPlayPauseOnHandler(RemoteUserInfo remoteUserInfo) { mediaPlayPauseKeyHandler.clearPendingMediaPlayPauseKey(); dispatchSessionTaskWithPlayerCommand( @@ -894,6 +904,13 @@ public ControllerLegacyCbForBroadcast() { lastDurationMs = C.TIME_UNSET; } + @Override + public void onAvailableCommandsChangedFromPlayer(int seq, Player.Commands availableCommands) { + PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); + maybeUpdateFlags(playerWrapper); + sessionImpl.getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat()); + } + @Override public void onDisconnected(int seq) throws RemoteException { // Calling MediaSessionCompat#release() is already done in release(). @@ -936,6 +953,7 @@ public void onPlayerChanged( onDeviceInfoChanged(seq, newPlayerWrapper.getDeviceInfo()); // Rest of changes are all notified via PlaybackStateCompat. + maybeUpdateFlags(newPlayerWrapper); @Nullable MediaItem newMediaItem = newPlayerWrapper.getCurrentMediaItemWithCommandCheck(); if (oldPlayerWrapper == null || !Util.areEqual(oldPlayerWrapper.getCurrentMediaItemWithCommandCheck(), newMediaItem)) { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java index 7f50a474550..a70c8abb406 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java @@ -17,21 +17,28 @@ package androidx.media3.session; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; +import static androidx.media3.test.session.common.TestUtils.getEventsAsList; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.assertThrows; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; +import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import androidx.annotation.Nullable; +import androidx.core.util.Predicate; import androidx.media3.common.C; import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; +import androidx.media3.common.SimpleBasePlayer; import androidx.media3.common.Timeline; import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.Consumer; @@ -1261,6 +1268,173 @@ public void onRepeatModeChanged(int repeatMode) { releasePlayer(player); } + @Test + public void playerWithCommandChangeMediaItems_flagHandleQueueIsAdvertised() throws Exception { + Player player = + createPlayerWithAvailableCommand(createDefaultPlayer(), Player.COMMAND_CHANGE_MEDIA_ITEMS); + MediaSession mediaSession = + createMediaSession( + player, + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, + MediaSession.ControllerInfo controller, + List mediaItems) { + return Futures.immediateFuture( + ImmutableList.of(MediaItem.fromUri("asset://media/wav/sample.wav"))); + } + }); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + // Wait until a playback state is sent to the controller. + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()); + assertThat(controllerCompat.getFlags() & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) + .isNotEqualTo(0); + + ArrayList receivedTimelines = new ArrayList<>(); + ArrayList receivedTimelineReasons = new ArrayList<>(); + CountDownLatch latch = new CountDownLatch(2); + Player.Listener listener = + new Player.Listener() { + @Override + public void onTimelineChanged( + Timeline timeline, @Player.TimelineChangeReason int reason) { + receivedTimelines.add(timeline); + receivedTimelineReasons.add(reason); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.addQueueItem( + new MediaDescriptionCompat.Builder().setMediaId("mediaId").build()); + controllerCompat.addQueueItem( + new MediaDescriptionCompat.Builder().setMediaId("mediaId").build(), /* index= */ 0); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedTimelines).hasSize(2); + assertThat(receivedTimelines.get(0).getWindowCount()).isEqualTo(1); + assertThat(receivedTimelines.get(1).getWindowCount()).isEqualTo(2); + assertThat(receivedTimelineReasons) + .containsExactly( + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerWithoutCommandChangeMediaItems_flagHandleQueueNotAdvertised() throws Exception { + Player player = + createPlayerWithExcludedCommand(createDefaultPlayer(), Player.COMMAND_CHANGE_MEDIA_ITEMS); + MediaSession mediaSession = + createMediaSession( + player, + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, + MediaSession.ControllerInfo controller, + List mediaItems) { + return Futures.immediateFuture( + ImmutableList.of(MediaItem.fromUri("asset://media/wav/sample.wav"))); + } + }); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + // Wait until a playback state is sent to the controller. + getFirstPlaybackState(controllerCompat, threadTestRule.getHandler()); + assertThat(controllerCompat.getFlags() & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) + .isEqualTo(0); + assertThrows( + UnsupportedOperationException.class, + () -> + controllerCompat.addQueueItem( + new MediaDescriptionCompat.Builder().setMediaId("mediaId").build())); + assertThrows( + UnsupportedOperationException.class, + () -> + controllerCompat.addQueueItem( + new MediaDescriptionCompat.Builder().setMediaId("mediaId").build(), + /* index= */ 0)); + + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void playerChangesAvailableCommands_actionsAreUpdated() throws Exception { + // TODO(b/261158047): Add COMMAND_RELEASE to the available commands so that we can release the + // player. + ControllingCommandsPlayer player = + new ControllingCommandsPlayer( + Player.Commands.EMPTY, threadTestRule.getHandler().getLooper()); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + LinkedBlockingDeque receivedPlaybackStateCompats = + new LinkedBlockingDeque<>(); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + receivedPlaybackStateCompats.add(state); + } + }; + controllerCompat.registerCallback(callback, threadTestRule.getHandler()); + + ArrayList receivedEvents = new ArrayList<>(); + ConditionVariable eventsArrived = new ConditionVariable(); + player.addListener( + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + receivedEvents.add(events); + eventsArrived.open(); + } + }); + threadTestRule + .getHandler() + .postAndSync( + () -> { + player.setAvailableCommands( + new Player.Commands.Builder().add(Player.COMMAND_PREPARE).build()); + }); + + assertThat(eventsArrived.block(TIMEOUT_MS)).isTrue(); + assertThat(getEventsAsList(receivedEvents.get(0))) + .containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED); + assertThat( + waitUntilPlaybackStateArrived( + receivedPlaybackStateCompats, + /* predicate= */ playbackStateCompat -> + (playbackStateCompat.getActions() & PlaybackStateCompat.ACTION_PREPARE) != 0)) + .isTrue(); + + eventsArrived.open(); + threadTestRule + .getHandler() + .postAndSync( + () -> { + player.setAvailableCommands(Player.Commands.EMPTY); + }); + + assertThat(eventsArrived.block(TIMEOUT_MS)).isTrue(); + assertThat( + waitUntilPlaybackStateArrived( + receivedPlaybackStateCompats, + /* predicate= */ playbackStateCompat -> + (playbackStateCompat.getActions() & PlaybackStateCompat.ACTION_PREPARE) == 0)) + .isTrue(); + assertThat(getEventsAsList(receivedEvents.get(1))) + .containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED); + + mediaSession.release(); + // This player is instantiated to use the threadTestRule, so it's released on that thread. + threadTestRule.getHandler().postAndSync(player::release); + } + private PlaybackStateCompat getFirstPlaybackState( MediaControllerCompat mediaControllerCompat, Handler handler) throws InterruptedException { LinkedBlockingDeque playbackStateCompats = new LinkedBlockingDeque<>(); @@ -1347,6 +1521,21 @@ private static Player createPlayerWithExcludedCommand( player, Player.Commands.EMPTY, new Player.Commands.Builder().add(excludedCommand).build()); } + private static boolean waitUntilPlaybackStateArrived( + LinkedBlockingDeque playbackStateCompats, + Predicate predicate) + throws InterruptedException { + while (true) { + @Nullable + PlaybackStateCompat playbackStateCompat = playbackStateCompats.poll(TIMEOUT_MS, MILLISECONDS); + if (playbackStateCompat == null) { + return false; + } else if (predicate.test(playbackStateCompat)) { + return true; + } + } + } + /** * Returns an {@link Player} where {@code availableCommands} are always included and {@code * excludedCommands} are always excluded from the {@linkplain Player#getAvailableCommands() @@ -1371,4 +1560,29 @@ public boolean isCommandAvailable(int command) { } }; } + + private static class ControllingCommandsPlayer extends SimpleBasePlayer { + + private Commands availableCommands; + + public ControllingCommandsPlayer(Commands availableCommands, Looper applicationLooper) { + super(applicationLooper); + this.availableCommands = availableCommands; + } + + public void setAvailableCommands(Commands availableCommands) { + this.availableCommands = availableCommands; + invalidateState(); + } + + @Override + protected State getState() { + return new State.Builder().setAvailableCommands(availableCommands).build(); + } + + @Override + protected ListenableFuture handleRelease() { + return Futures.immediateVoidFuture(); + } + } } From f983d912e5b6cdbc47d60b094e2873244d9d99ae Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 2 Feb 2023 16:49:56 +0000 Subject: [PATCH 138/141] Fix release note entry --- RELEASENOTES.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f2d0c6587e6..44c3c864f3c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -110,8 +110,6 @@ This release corresponds to the * Fix bug where removing listeners during the player release can cause an `IllegalStateException` ([#10758](https://github.com/google/ExoPlayer/issues/10758)). - * Add `ExoPlayer.Builder.setPlaybackLooper` that sets a pre-existing - playback thread for a new ExoPlayer instance. * Build: * Enforce minimum `compileSdkVersion` to avoid compilation errors ([#10684](https://github.com/google/ExoPlayer/issues/10684)). From 3fdaf78fc45207aa331b1bac5092684470a23279 Mon Sep 17 00:00:00 2001 From: christosts Date: Mon, 13 Feb 2023 15:15:02 +0000 Subject: [PATCH 139/141] Prepare media3 release notes for rc01 PiperOrigin-RevId: 509218510 (cherry picked from commit 73909222706c6d7a56e0fb2d09ed8b49eca5b2be) --- RELEASENOTES.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 44c3c864f3c..32b921e94b9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,9 @@ # Release notes -### Unreleased changes +### 1.0.0-rc01 (2023-02-16) + +This release corresponds to the +[ExoPlayer 2.18.3 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.3). * Core library: * Tweak the renderer's decoder ordering logic to uphold the @@ -21,8 +24,8 @@ items into a single window ([#247](https://github.com/androidx/media/issues/247)). * Extractors: - * Throw a ParserException instead of a NullPointerException if the sample - table (stbl) is missing a required sample description (stsd) when + * Throw a `ParserException` instead of a `NullPointerException` if the + sample table (stbl) is missing a required sample description (stsd) when parsing trak atoms. * Correctly skip samples when seeking directly to a sync frame in fMP4 ([#10941](https://github.com/google/ExoPlayer/issues/10941)). @@ -34,6 +37,14 @@ `Subtitle.getEventTime` if a subtitle file contains no cues. * SubRip: Add support for UTF-16 files if they start with a byte order mark. +* Metadata: + * Parse multiple null-separated values from ID3 frames, as permitted by + ID3 v2.4. + * Add `MediaMetadata.mediaType` to denote the type of content or the type + of folder described by the metadata. + * Add `MediaMetadata.isBrowsable` as a replacement for + `MediaMetadata.folderType`. The folder type will be deprecated in the + next release. * DASH: * Add full parsing for image adaptation sets, including tile counts ([#3752](https://github.com/google/ExoPlayer/issues/3752)). From 9f432499fb6fec2f30c0ff2b35aded999b00485b Mon Sep 17 00:00:00 2001 From: christosts Date: Mon, 13 Feb 2023 15:34:53 +0000 Subject: [PATCH 140/141] Minor fixes in release notes PiperOrigin-RevId: 509222489 (cherry picked from commit a90728fdc66cc2a8929cce9d67081681e0168115) --- RELEASENOTES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 32b921e94b9..38c631388ea 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -81,9 +81,9 @@ This release corresponds to the * Add `MediaMetadata.isBrowsable` as a replacement for `MediaMetadata.folderType`. The folder type will be deprecated in the next release. -* Cast extension +* Cast extension: * Bump Cast SDK version to 21.2.0. -* IMA extension +* IMA extension: * Remove player listener of the `ImaServerSideAdInsertionMediaSource` on the application thread to avoid threading issues. * Add a property `focusSkipButtonWhenAvailable` to the @@ -93,7 +93,7 @@ This release corresponds to the `ImaServerSideAdInsertionMediaSource.AdsLoader` to programmatically request to focus the skip button. * Bump IMA SDK version to 3.29.0. -* Demo app +* Demo app: * Request notification permission for download notifications at runtime ([#10884](https://github.com/google/ExoPlayer/issues/10884)). From 98bf30d2afd4fe83c9de5131353c6fb7af94e499 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 14 Feb 2023 13:49:22 +0000 Subject: [PATCH 141/141] Version bump for ExoPlayer 2.18.3 & media3-1.0.0-rc01 #minor-release PiperOrigin-RevId: 509501665 (cherry picked from commit 20eae0e041e1922fd79ca36218054b293a9da7da) --- .github/ISSUE_TEMPLATE/bug.yml | 1 + README.md | 12 +++++------- constants.gradle | 4 ++-- .../androidx/media3/common/MediaLibraryInfo.java | 6 +++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 38ed1ba728c..005d4e2e68f 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -17,6 +17,7 @@ body: label: Media3 Version description: What version of Media3 are you using? options: + - 1.0.0-rc01 - 1.0.0-beta03 - 1.0.0-beta02 - 1.0.0-beta01 diff --git a/README.md b/README.md index 9f50f679baf..d0b375b92da 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,13 @@ Android, including local playback (via ExoPlayer) and media sessions. ## Current status -AndroidX Media is currently in beta and we welcome your feedback via the -[issue tracker][]. Please consult the [release notes][] for more details about -the beta release. +AndroidX Media is currently in release candidate and we welcome your feedback +via the [issue tracker][]. Please consult the [release notes][] for more details +about the current release. ExoPlayer's new home will be in AndroidX Media, but for now we are publishing it -both in AndroidX Media and via the existing [ExoPlayer project][]. While -AndroidX Media is in beta we recommend that production apps using ExoPlayer -continue to depend on the existing ExoPlayer project. We are still handling -ExoPlayer issues on the [ExoPlayer issue tracker][]. +both in AndroidX Media and via the existing [ExoPlayer project][] and we are +still handling ExoPlayer issues on the [ExoPlayer issue tracker][]. You'll find some [Media3 documentation on developer.android.com][], including a [migration guide for existing ExoPlayer and MediaSession users][]. diff --git a/constants.gradle b/constants.gradle index ac9b80f1d6c..39884d57c73 100644 --- a/constants.gradle +++ b/constants.gradle @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. project.ext { - releaseVersion = '1.0.0-beta03' - releaseVersionCode = 1_000_000_1_03 + releaseVersion = '1.0.0-rc01' + releaseVersionCode = 1_000_000_2_01 minSdkVersion = 16 appTargetSdkVersion = 33 // API version before restricting local file access. diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java index 603392d3696..ad013f4cd3c 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java @@ -29,11 +29,11 @@ public final class MediaLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "1.0.0-beta03"; + public static final String VERSION = "1.0.0-rc01"; /** The version of the library expressed as {@code TAG + "/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta03"; + public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-rc01"; /** * The version of the library expressed as an integer, for example 1002003300. @@ -47,7 +47,7 @@ public final class MediaLibraryInfo { * (123-045-006-3-00). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 1_000_000_1_03; + public static final int VERSION_INT = 1_000_000_2_01; /** Whether the library was compiled with {@link Assertions} checks enabled. */ public static final boolean ASSERTIONS_ENABLED = true;