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 1eb87be715..33f9a6146d 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 604b607842..dbc2fa059e 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 72733a90e9..e49a9a5a3c 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 final class MediaCodecInfo { } /** - * 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 final class MediaCodecInfo { return false; } - if (!isCodecProfileAndLevelSupported(format)) { + if (!isCodecProfileAndLevelSupported(format, /* checkPerformanceCapabilities= */ true)) { return false; } @@ -283,15 +284,24 @@ public final class MediaCodecInfo { } } + /** + * 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 @@ public final class MediaCodecInfo { 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 79c3b9ca7a..4a4c43e6d4 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 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } 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 c3200150d0..e97e053084 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 final class MediaCodecUtil { } /** - * 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 4e3f48ea06..d42ab38fe0 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.core.app.ApplicationProvider; 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 class MediaCodecVideoRendererTest { .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 b3b61dac55..376c69534d 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 c1b67c6207..8fe6eece28 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.Tracks; 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 @@ import java.lang.annotation.Target; */ /* 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 @@ import java.lang.annotation.Target; 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 0000000000..7e8738f9d9 --- /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 bd77a50cac..9ca07c700a 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 final class DefaultAudioSink implements AudioSink { 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 33f9a6146d..50b763ca1a 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 84769d802f..eae251688e 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.AudioDeviceInfo; 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 @@ public interface ExoPlayer extends Player { /* package */ long detachSurfaceTimeoutMs; /* package */ boolean pauseAtEndOfMediaItems; /* package */ boolean usePlatformDiagnostics; + @Nullable /* package */ Looper playbackLooper; /* package */ boolean buildCalled; /** @@ -527,6 +529,7 @@ public interface ExoPlayer extends Player { *
  • {@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 interface ExoPlayer extends Player { 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 1d087d23b3..d896bc7654 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 @@ import java.util.concurrent.TimeoutException; 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 312a9c67ae..d92a5e8b87 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 @@ import java.util.concurrent.atomic.AtomicBoolean; 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 @@ import java.util.concurrent.atomic.AtomicBoolean; 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 @@ import java.util.concurrent.atomic.AtomicBoolean; 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 @@ import java.util.concurrent.atomic.AtomicBoolean; @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 @@ import java.util.concurrent.atomic.AtomicBoolean; * @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 @@ import java.util.concurrent.atomic.AtomicBoolean; * @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 @@ import java.util.concurrent.atomic.AtomicBoolean; /* 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 eeb4f3674a..60de48cae1 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.LibraryResult.RESULT_SUCCESS; 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.core.util.ObjectsCompat; 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.MediaLibraryService.LibraryParams; 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 @@ import java.util.concurrent.atomic.AtomicReference; 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 @@ import java.util.concurrent.atomic.AtomicReference; /* 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 @@ import java.util.concurrent.atomic.AtomicReference; } ListenableFuture> future = librarySessionImpl.onGetItemOnHandler(controller, itemId); - sendLibraryResultWithMediaItemWhenReady(result, future); + ListenableFuture browserItemFuture = + Util.transformFutureAsync(future, createMediaItemToBrowserItemAsyncFunction()); + sendLibraryResultWithMediaItemWhenReady(result, browserItemFuture); }); } @@ -362,17 +377,12 @@ import java.util.concurrent.atomic.AtomicReference; 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 @@ import java.util.concurrent.atomic.AtomicReference; 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 @@ import java.util.concurrent.atomic.AtomicReference; 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 @@ import java.util.concurrent.atomic.AtomicReference; 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 ae17cc834f..3f89c5dd73 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 e9ec255abc..4e36a19ece 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 class MediaBrowserCompatWithMediaLibraryServiceTest 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 class MediaBrowserCompatWithMediaLibraryServiceTest 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 class MediaBrowserCompatWithMediaLibraryServiceTest BundleSubject.assertThat(description.getExtras()) .string(METADATA_EXTRA_KEY) .isEqualTo(METADATA_EXTRA_VALUE); + assertThat(description.getIconBitmap()).isNotNull(); } @Test @@ -245,6 +248,7 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest 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 class MediaBrowserCompatWithMediaLibraryServiceTest 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 1d53a84794..d7a8fca105 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.RatingCompat; 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 final class MediaUtilsTest { 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 final class MediaUtilsTest { 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 cfad2d7550..e3023a00a2 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.SUBSCRIB 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 androidx.media3.test.session.common.TestUtils; 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 class MockMediaLibraryService extends MediaLibraryService { 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 @@ public class MockMediaLibraryService extends MediaLibraryService { // 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 @@ public class MockMediaLibraryService extends MediaLibraryService { 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 4cbbb83222..705115745f 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 1a09119032..30e610ed14 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 @@ public class PlayerNotificationManager { 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 658e9f56be..54d62208ef 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 @@ public class 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 class TestPlayerRunHelper { 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 class TestPlayerRunHelper { 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 class TestPlayerRunHelper { 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 class TestPlayerRunHelper { */ 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 class TestPlayerRunHelper { 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 class TestPlayerRunHelper { */ 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 class TestPlayerRunHelper { 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 class TestPlayerRunHelper { 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 @@ public class TestPlayerRunHelper { 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 4a4c43e6d4..815b4c369e 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 @@ public abstract class MediaCodecRenderer extends BaseRenderer { || (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 9ca07c700a..605f5f0d44 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 final class DefaultAudioSink implements AudioSink { 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 | 2332 ++++++++++++++++- .../media3/common/SimpleBasePlayerTest.java | 1576 ++++++++++- 2 files changed, 3813 insertions(+), 95 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 f3a073be7f..350a23920c 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.Size; 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 abstract class SimpleBasePlayer extends BasePlayer { 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,6 +261,542 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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); @@ -144,11 +809,211 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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. */ @@ -167,7 +1032,44 @@ public abstract class SimpleBasePlayer extends BasePlayer { State state = (State) o; return playWhenReady == state.playWhenReady && playWhenReadyChangeReason == state.playWhenReadyChangeReason - && availableCommands.equals(state.availableCommands); + && 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 @@ -176,14 +1078,903 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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; + } + + /** + * 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; + } + + /** + * 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); + } + } + + /** 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); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PeriodData)) { + return false; + } + 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 + 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { } @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() { + verifyApplicationThreadAndInitState(); + return state.playerError; + } + + @Override + public final void setRepeatMode(@Player.RepeatMode int repeatMode) { // TODO: implement. throw new IllegalStateException(); } @Override - public final void setRepeatMode(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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @Override public final PlaybackParameters getPlaybackParameters() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.playbackParameters; } @Override @@ -406,14 +2200,14 @@ public abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @Override public final float getVolume() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.volume; } @Override @@ -586,38 +2389,38 @@ public abstract class SimpleBasePlayer extends BasePlayer { @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { } 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 1b13cb00fc..aef72f644f 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.AtomicBoolean; 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 class SimpleBasePlayerTest { /* 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 class SimpleBasePlayerTest { } @Test - public void stateBuilderSetAvailableCommands_setsAvailableCommands() { + 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(); + + 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 stateBuilderBuild_setsCorrectValues() { Commands commands = new Commands.Builder() .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE) .build(); - State state = new State.Builder().setAvailableCommands(commands).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; - assertThat(state.availableCommands).isEqualTo(commands); - } - - @Test - public void stateBuilderSetPlayWhenReady_setsStatePlayWhenReadyAndReason() { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { /* 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 @@ public class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { /* 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(); - Commands commands = new Commands.Builder().add(Player.COMMAND_GET_TEXT).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(); + 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 @@ public class SimpleBasePlayerTest { 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 @@ public class SimpleBasePlayerTest { 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 5b0e76b817..e9eba03445 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 final class SessionToken implements Bundleable { } } + /** Creates a session token connected to a Media3 session. */ /* package */ SessionToken( int uid, int type, @@ -143,21 +144,9 @@ public final class SessionToken implements Bundleable { 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 final class SessionToken implements Bundleable { 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 @@ public final class SessionToken implements Bundleable { 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 ca25291cf4..45ee44b3af 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 class MediaSessionCompatCallbackWithMediaControllerTest { } 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 fef653bf6e..1583c1c964 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 9c79738439..48b141dca2 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 18f1dd5c69..b121886070 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 e9eba03445..09c5e61de7 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.IntDef; 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 final class SessionToken implements Bundleable { 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 @@ public final class SessionToken implements Bundleable { 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 0b4e9da76d..992221c889 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 android.net.Uri; 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.PsExtractor; 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 final class DefaultExtractorsFactory implements ExtractorsFactory { 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 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { extractors.add(new PsExtractor()); break; case FileTypes.TS: - extractors.add(new TsExtractor(tsMode, tsFlags, tsTimestampSearchBytes)); + extractors.add( + new TsExtractor( + tsMode, + new TimestampAdjuster(0), + new DefaultTsPayloadReaderFactory(tsFlags, tsSubtitleFormats), + tsTimestampSearchBytes)); break; case FileTypes.WAV: extractors.add(new WavExtractor()); From 5a96fc792f2dd841b5884bb7359d1d38d3d0217c Mon Sep 17 00:00:00 2001 From: tonihei 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 5e8ae12591..516dcf10d6 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.FutureCallback; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; // 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 3f89c5dd73..97d240032c 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } /** - * 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 3d4084434b..77d06b7244 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 class MediaControllerCompatCallbackWithMediaSessionTest { 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 class MediaControllerCompatCallbackWithMediaSessionTest { .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 f090ca8b9c..6f5697fc34 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 androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; 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.Events; 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 class MediaControllerMediaSessionCompatCallbackAggregationTest { @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 class MediaControllerMediaSessionCompatCallbackAggregationTest { 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 class MediaControllerMediaSessionCompatCallbackAggregationTest { 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 class MediaControllerMediaSessionCompatCallbackAggregationTest { 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 @@ public class MediaControllerMediaSessionCompatCallbackAggregationTest { .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 e0c56563f7..e10dd5ae96 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 class MediaControllerStateMaskingWithMediaSessionCompatTest { @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 class MediaControllerStateMaskingWithMediaSessionCompatTest { @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 class MediaControllerStateMaskingWithMediaSessionCompatTest { 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 class MediaControllerStateMaskingWithMediaSessionCompatTest { @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 class MediaControllerStateMaskingWithMediaSessionCompatTest { 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 class MediaControllerStateMaskingWithMediaSessionCompatTest { @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 class MediaControllerStateMaskingWithMediaSessionCompatTest { @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 class MediaControllerStateMaskingWithMediaSessionCompatTest { @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 724c82492b..b735477ce7 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { .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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { @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 class MediaControllerWithMediaSessionCompatTest { @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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { @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 45ee44b3af..82e4008c4a 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 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { @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 class MediaSessionCompatCallbackWithMediaControllerTest { @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 class MediaSessionCompatCallbackWithMediaControllerTest { @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 class MediaSessionCompatCallbackWithMediaControllerTest { @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 class MediaSessionCompatCallbackWithMediaControllerTest { 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 class MediaSessionCompatCallbackWithMediaControllerTest { 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 d7a8fca105..8ff1473cbc 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 final class MediaUtilsTest { } @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 2b1cd045ce..5d9e56a7c7 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 final class MediaTestUtils { .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 final class MediaTestUtils { 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 0ebcd49075..d79385f5e4 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 97d240032c..64a25e0eb9 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.Player.COMMAND_STOP; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 65e7847199..6cf1ce19e1 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 @@ import java.util.List; 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 4e36a19ece..4e3ae96507 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_EXT 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 class MediaBrowserCompatWithMediaLibraryServiceTest 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 12caed0f8e..4199b8f610 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 @@ public class MediaBrowserCompatWithMediaSessionServiceTest { 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 d9f8a0afd0..30bd1c83a2 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 @@ package androidx.media3.session; 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 class MediaBrowserListenerWithMediaBrowserServiceCompatTest { 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 8ff1473cbc..80dd8073b4 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.MediaSessionCompat; 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 final class MediaUtilsTest { 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 final class MediaUtilsTest { 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 final class MediaUtilsTest { .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 65ce255a30..19bce9153d 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.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STA 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.MediaDescriptionCompat; 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 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat { 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 e3023a00a2..d460bee20b 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_COMPLETION_STATU 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_I 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 class MockMediaLibraryService extends MediaLibraryService { .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 | 100 +++++++++++++----- ...ntrollerWithFrameworkMediaSessionTest.java | 7 +- .../media3/session/SessionTokenTest.java | 27 +++++ 6 files changed, 110 insertions(+), 37 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 50b763ca1a..854f943d89 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 2697fd11dd..9da5c30fe5 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 class MediaSession { /** * 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 b15df33919..739c0baefa 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 class MediaStyleNotificationHelper { 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 09c5e61de7..7a25ccc1c9 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.content.pm.ServiceInfo; 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 final class SessionToken implements Bundleable { } /** - * 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, 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, Object compatToken) { - checkNotNull(context, "context must not be null"); - checkNotNull(compatToken, "compatToken must not be null"); - checkArgument(compatToken instanceof MediaSessionCompat.Token); - + 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 @@ public final class SessionToken implements Bundleable { } } }); - // 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 final class SessionToken implements Bundleable { 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 @@ public final class SessionToken implements Bundleable { } } - 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 3c9102296c..6fe9bd8957 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.MediaSession; 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 class MediaControllerWithFrameworkMediaSessionTest { @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 class MediaControllerWithFrameworkMediaSessionTest { 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 b2d54e2abf..f0efaea3de 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.MOCK_MEDIA3_SE 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.Bundle; 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 class SessionTokenTest { 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 class SessionTokenTest { 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 class SessionTokenTest { 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 class SessionTokenTest { 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 class SessionTokenTest { 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 9c72d87966..82f561561b 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 @@ public final class AacUtil { 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 0000000000..f9d71a3cc4 --- /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 d7cb969585..e2864b8a49 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.common.util.Log; 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 @@ public final class MediaControllerTestRule extends ExternalResource { 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 5e05ae6301..2fc70006a3 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 @@ public interface Player { * 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 d1a348cd3a..7c1eb001d2 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 4f7940b30a..6560cea856 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.Assertions.checkStateNotNull; 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.Process; 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.Log; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; @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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } @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 @@ import org.checkerframework.checker.nullness.qual.NonNull; /* 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 59b49bdda5..f9673ccf05 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.Player.Commands; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 9da5c30fe5..6b25c8d56c 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 @@ public class MediaSession { 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 705115745f..d01fb6eee3 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; } playerInfo = newPlayerWrapper.createPlayerInfoForBundling(); - onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false); + onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ false, /* excludeTracks= */ false); } public void release() { @@ -374,7 +379,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; (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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; .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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; // 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; } 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; } 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; } 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; } 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; } session.playerInfo = session.playerInfo.copyWithMaxSeekToPreviousPositionMs(maxSeekToPreviousPositionMs); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); } @Nullable @@ -1224,10 +1258,12 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 7160d1e176..b13b4d61fb 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.PlaybackParameters; 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 @@ import java.util.concurrent.ExecutionException; 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 64a25e0eb9..f58882351d 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.MediaSessionCompat.QueueItem; 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.Timeline.Window; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 8fe6eece28..79c780c36e 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 @@ import java.lang.annotation.Target; */ 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 8cb138e0f9..13f7d64d4e 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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { }); 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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { } @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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { .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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { 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 80dd8073b4..83c5a4e3f8 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.MediaBrowserCompat.MediaItem.FLAG_PLAYABL 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.RatingCompat; 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.Player; 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 final class MediaUtilsTest { 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 93c1c99c65..f3fd3ef1ec 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 008ba329eb..45de1f5a94 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 class DefaultMediaNotificationProvider implements MediaNotification.Provi 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 516dcf10d6..a80301d509 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Initialized MediaSessionLegacyStub thisRef = this; sessionCompat.setCallback(thisRef, handler); - mainHandler = new Handler(Looper.getMainLooper()); } /** Starts to receive commands. */ @@ -1205,7 +1203,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } }; 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 e4a61f3e0b..cfbe95a611 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 final class Ac3Util { 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 final class Ac3Util { .setSampleRate(sampleRate) .setDrmInitData(drmInitData) .setLanguage(language) + .setAverageBitrate(constantBitrate) + .setPeakBitrate(constantBitrate) .build(); } @@ -180,7 +185,9 @@ public final class Ac3Util { */ 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 final class Ac3Util { .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 c2e51faaef..71eed666b7 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 80f0790cd0..a6fbd97784 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 a8d1588940..e02699e2de 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 17bf79c850..4b7e17e7c9 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 c2e51faaef..71eed666b7 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 3724592554..84217c2e01 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 e9019d4ab1..1edd06253f 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 2b9cb1cd52..01fd6af916 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 eb313f941d..c303da0e15 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 3724592554..84217c2e01 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 8000864576..aba5268ea2 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 49ab3da0aa..ac03cfd484 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 19bfc7c5fa..1a61f528ac 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 d34514d8a8..431599a9be 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 8000864576..aba5268ea2 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 a7f3c63f8d..6da60d472a 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 a627d00633..646dd35d91 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 31013410b6..a7ba576bf5 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 13ff558eaa..280d6febc4 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 a7f3c63f8d..6da60d472a 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 ecc28b7208..c98e27dc19 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 d9ed0c417d..9c9cee29df 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 741d5199ea..85c07f6d2d 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 98fe8c793d..56387fb3c7 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 ecc28b7208..c98e27dc19 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 c5902f5d19..c73a6282e8 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 8fa0cbf7fe..78b392053e 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 603ca0de80..2558363342 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 cd42dac917..084d2aa030 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 c5902f5d19..c73a6282e8 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 cfbe95a611..e4a61f3e0b 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 final class Ac3Util { 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 final class Ac3Util { .setSampleRate(sampleRate) .setDrmInitData(drmInitData) .setLanguage(language) - .setAverageBitrate(constantBitrate) - .setPeakBitrate(constantBitrate) .build(); } @@ -185,9 +180,7 @@ public final class Ac3Util { */ 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 final class Ac3Util { .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 71eed666b7..c2e51faaef 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 a6fbd97784..80f0790cd0 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 e02699e2de..a8d1588940 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 4b7e17e7c9..17bf79c850 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 71eed666b7..c2e51faaef 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 84217c2e01..3724592554 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 1edd06253f..e9019d4ab1 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 01fd6af916..2b9cb1cd52 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 c303da0e15..eb313f941d 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 84217c2e01..3724592554 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 aba5268ea2..8000864576 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 ac03cfd484..49ab3da0aa 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 1a61f528ac..19bfc7c5fa 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 431599a9be..d34514d8a8 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 aba5268ea2..8000864576 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 6da60d472a..a7f3c63f8d 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 646dd35d91..a627d00633 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 a7ba576bf5..31013410b6 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 280d6febc4..13ff558eaa 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 6da60d472a..a7f3c63f8d 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 c98e27dc19..ecc28b7208 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 9c9cee29df..d9ed0c417d 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 85c07f6d2d..741d5199ea 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 56387fb3c7..98fe8c793d 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 c98e27dc19..ecc28b7208 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 c73a6282e8..c5902f5d19 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 78b392053e..8fa0cbf7fe 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 2558363342..603ca0de80 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 084d2aa030..cd42dac917 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 c73a6282e8..c5902f5d19 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 e4a61f3e0b..cfbe95a611 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 final class Ac3Util { 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 final class Ac3Util { .setSampleRate(sampleRate) .setDrmInitData(drmInitData) .setLanguage(language) + .setAverageBitrate(constantBitrate) + .setPeakBitrate(constantBitrate) .build(); } @@ -180,7 +185,9 @@ public final class Ac3Util { */ 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 final class Ac3Util { .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 c2e51faaef..71eed666b7 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 80f0790cd0..a6fbd97784 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 a8d1588940..e02699e2de 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 17bf79c850..4b7e17e7c9 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 c2e51faaef..71eed666b7 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 3724592554..84217c2e01 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 e9019d4ab1..1edd06253f 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 2b9cb1cd52..01fd6af916 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 eb313f941d..c303da0e15 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 3724592554..84217c2e01 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 8000864576..aba5268ea2 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 49ab3da0aa..ac03cfd484 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 19bfc7c5fa..1a61f528ac 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 d34514d8a8..431599a9be 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 8000864576..aba5268ea2 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 a7f3c63f8d..6da60d472a 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 a627d00633..646dd35d91 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 31013410b6..a7ba576bf5 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 13ff558eaa..280d6febc4 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 a7f3c63f8d..6da60d472a 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 ecc28b7208..c98e27dc19 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 d9ed0c417d..9c9cee29df 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 741d5199ea..85c07f6d2d 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 98fe8c793d..56387fb3c7 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 ecc28b7208..c98e27dc19 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 c5902f5d19..c73a6282e8 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 8fa0cbf7fe..78b392053e 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 603ca0de80..2558363342 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 cd42dac917..084d2aa030 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 c5902f5d19..c73a6282e8 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 cfbe95a611..78baffd5b5 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 @@ public final class Ac3Util { */ 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 final class Ac3Util { */ 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 78baffd5b5..b9279635d8 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 final class Ac3Util { 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 final class Ac3Util { 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 71eed666b7..97bfb758dc 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 a6fbd97784..9e9b211ed9 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 e02699e2de..4e872b3fd5 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 4b7e17e7c9..138696f8a7 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 71eed666b7..97bfb758dc 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 84217c2e01..448e79e1fd 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 1edd06253f..0f902e441a 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 01fd6af916..d747be40c5 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 c303da0e15..76738dd5b3 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 84217c2e01..448e79e1fd 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 aba5268ea2..a50ee9fecd 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 ac03cfd484..089c940439 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 1a61f528ac..5d481314d5 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 431599a9be..0242518866 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 aba5268ea2..a50ee9fecd 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 6da60d472a..734051bb2d 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 646dd35d91..027e7eb633 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 a7ba576bf5..db94e2636e 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 280d6febc4..854d952ad8 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 6da60d472a..734051bb2d 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 c98e27dc19..45a51b50ae 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 9c9cee29df..4ad3e45f53 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 85c07f6d2d..0c53717c22 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 56387fb3c7..c8cd33b57b 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 c98e27dc19..45a51b50ae 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 c73a6282e8..87930b0bfb 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 78b392053e..e1aa764cfb 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 2558363342..c9f083805f 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 084d2aa030..a3875f612d 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 c73a6282e8..87930b0bfb 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 d92a5e8b87..6c3c64075b 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 @@ import java.util.concurrent.atomic.AtomicBoolean; 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 f812c11ed5..7649d1bbe8 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.C; 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 @@ import com.google.common.collect.ImmutableList; 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 @@ import com.google.common.collect.ImmutableList; * 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 5bc6e1026a..21cd5ceec4 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.Iterator; 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 @@ import java.util.Set; 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 @@ import java.util.Set; public MediaSourceList( MediaSourceListInfoRefreshListener listener, AnalyticsCollector analyticsCollector, - Handler analyticsCollectorHandler, + HandlerWrapper analyticsCollectorHandler, PlayerId playerId) { this.playerId = playerId; mediaSourceListInfoListener = listener; @@ -109,12 +112,10 @@ import java.util.Set; 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 @@ import java.util.Set; 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 @@ import java.util.Set; * @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 @@ import java.util.Set; 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 @@ import java.util.Set; 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 @@ import java.util.Set; @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 @@ import java.util.Set; @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 @@ import java.util.Set; @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 @@ import java.util.Set; 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 @@ import java.util.Set; 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 @@ import java.util.Set; 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 @@ import java.util.Set; 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 31a0726ef7..14f92f873d 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.TrackGroup; 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 final class ExoPlayerTest { 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 final class ExoPlayerTest { 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 final class ExoPlayerTest { 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 ca3bd02eeb..2ab4681030 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.mockito.Mockito.mock; 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.Player; 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 final class MediaPeriodQueueTest { 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 edb874091c..ea3c6ce33d 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 class MediaSourceListTest { 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 c25ddebdcf..ceefe172c7 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_ 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.inOrder; 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.Tracks; 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.LoadEventInfo; 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.Test; 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { /* 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { /* 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { } }, 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { } }, 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { @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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { .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 @@ public final class DefaultAnalyticsCollectorTest { } } }; + 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 final class DefaultAnalyticsCollectorTest { } } }; + 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 @@ public final class DefaultAnalyticsCollectorTest { } } }; + 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 @@ public final class DefaultAnalyticsCollectorTest { @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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { .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 final class DefaultAnalyticsCollectorTest { 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 final class DefaultAnalyticsCollectorTest { 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 7a476d16b5..dc219d9b98 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 @@ import androidx.media3.exoplayer.audio.AudioRendererEventListener; @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 class FakeAudioRenderer extends FakeRenderer { 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 9c9a4bb1b7..45de4ecad1 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @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 class FakeVideoRenderer extends FakeRenderer { 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 @@ public class FakeVideoRenderer extends FakeRenderer { @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 @@ public class FakeVideoRenderer extends FakeRenderer { @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 @@ public class FakeVideoRenderer extends FakeRenderer { @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 f5e02695bf..afc58e3ef8 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.annotation.Nullable; 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 class TestExoPlayerBuilder { 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 54d62208ef..173b6d2683 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 class TestPlayerRunHelper { } } + /** + * 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 6a55842551..201d9b1296 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 @@ public final class Metadata implements Parcelable { } /** - * 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 bbd558208a..db37f66405 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 @@ public class FakeClock implements Clock { } 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 393ce98408..51dd19bab5 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.base.Objects; 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 final class FakeClockTest { FakeClock fakeClock = new FakeClock( /* bootTimeMs= */ 150, /* initialTimeMs= */ 200, /* isAutoAdvancing= */ false); + assertThat(fakeClock.currentTimeMillis()).isEqualTo(350); } @@ -55,17 +58,24 @@ public final class FakeClockTest { 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 final class FakeClockTest { .sendToTarget(); ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); + handlerThread.quitSafely(); assertThat(callback.messages) .containsExactly( @@ -126,6 +137,7 @@ public final class FakeClockTest { fakeClock.advanceTime(50); shadowOf(handler.getLooper()).idle(); + handlerThread.quitSafely(); assertThat(callback.messages).hasSize(4); assertThat(Iterables.getLast(callback.messages)) @@ -146,6 +158,7 @@ public final class FakeClockTest { handler.obtainMessage(/* what= */ 4).sendToTarget(); ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); + handlerThread.quitSafely(); assertThat(callback.messages) .containsExactly( @@ -192,6 +205,8 @@ public final class FakeClockTest { fakeClock.advanceTime(1000); shadowOf(handler.getLooper()).idle(); assertTestRunnableStates(new boolean[] {true, true, true, true, true}, testRunnables); + + handlerThread.quitSafely(); } @Test @@ -203,7 +218,6 @@ public final class FakeClockTest { 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 final class FakeClockTest { 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 final class FakeClockTest { 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 final class FakeClockTest { 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 final class FakeClockTest { }); ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); + handlerThread.quitSafely(); assertThat(clockTimes).containsExactly(0L, 20L, 50L, 70L, 100L).inOrder(); } @@ -333,6 +346,8 @@ public final class FakeClockTest { }); 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 final class FakeClockTest { 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 854f943d89..24a1db8359 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 14f92f873d..eef9c43b4b 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 final class ExoPlayerTest { 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 8409d3f4d9..9e248def39 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 class MetadataRendererTest { 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 e5467d7a54..150c852a91 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 final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 4dacad9b75..19854db16b 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.common.util.Util; 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { } 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); + } - return new TextInformationFrame(id, null, value); + 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); + } + + ImmutableList result = values.build(); + return result.isEmpty() ? ImmutableList.of("") : result; } @Nullable @@ -501,7 +520,7 @@ public final class Id3Decoder extends SimpleMetadataDecoder { 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { : 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 c33ef14e31..04d3d17463 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.annotation.Nullable; 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 @@ import java.util.List; 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); } - /* package */ TextInformationFrame(Parcel in) { - super(castNonNull(in.readString())); - description = in.readString(); - value = castNonNull(in.readString()); + /** + * @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)); } + 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 final class TextInformationFrame extends Id3Frame { 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 final class TextInformationFrame extends Id3Frame { 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 final class TextInformationFrame extends Id3Frame { } 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 final class TextInformationFrame extends Id3Frame { 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 final class TextInformationFrame extends Id3Frame { 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 final class TextInformationFrame extends Id3Frame { 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 final class TextInformationFrame extends Id3Frame { 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 6279da9e63..3c90957f64 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 @@ public final class Mp3Extractor implements Extractor { 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 4c111af241..023d573e29 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.Id3Frame; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ? 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 d2042f1798..38c164d14e 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 static com.google.common.truth.Truth.assertThat; 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 222df1785d..b786a4f23f 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 static com.google.common.truth.Truth.assertThat; 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 final class ChapterTocFrameTest { 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 55e81ab93c..ce884844df 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 final class Id3DecoderTest { 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 final class Id3DecoderTest { 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 final class Id3DecoderTest { 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 final class Id3DecoderTest { 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 final class Id3DecoderTest { 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 bafb57e3cf..ce9123c308 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 class TextInformationFrameTest { 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= */ "TYE", /* description= */ null, /* value= */ recordingYear), + /* id= */ "TP2", + /* description= */ null, + /* values= */ ImmutableList.of(albumArtist)), + new TextInformationFrame( + /* 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 class TextInformationFrameTest { 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 d2a8d6a442..b6a9c0947d 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 250d1add95..725c496cec 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 e5057cff25..c310e1ffdc 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 afaead1d88..1423a1df78 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 d2a8d6a442..b6a9c0947d 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 ca9f1a74a1..68f3fe89a6 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 36314d9433..364d99e3cc 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 0e8cc73341..ce1cee5dd2 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 8ef6f9cb33..0d23e7d7db 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 ca9f1a74a1..68f3fe89a6 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 c252057e47..84b9b78db4 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 76fcbc0f8e..3c0e31eb8e 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 4f9b29dc55..1e877253dc 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 220965634f..702eefce2e 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 c252057e47..84b9b78db4 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 20a69e34a8..352641de04 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 ef3785beb3..811fd0aaaa 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 12697f6d12..7e2745f280 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 2ab479e633..11102a27ce 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 20a69e34a8..352641de04 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 804961f690..a2e644c980 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 a974548364..621c343df1 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 19fd0f36d2..f5585c7bf6 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 80ca2a76c7..d94ad775db 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 804961f690..a2e644c980 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 321bc3a832..1bcbd8e43f 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 4d8fe681ce..2cb5ff29f5 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 a3e5cd60d0..bfe2e5b1b0 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 a498d93b5e..f90a082a27 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 321bc3a832..1bcbd8e43f 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 f9c1539402..fb6ca4f7d2 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 d0c0ab1586..86625de6e0 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 cc688289e2..f63cc0a500 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 42e14dbd2d..ec53664fc5 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 f9c1539402..fb6ca4f7d2 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 8c1813ef83..54ba703ee5 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 5011cfa353..4ae939fe5a 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 ad7c5fbe40..ff45deced5 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 9e8fbd7584..4a58490278 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 8c1813ef83..54ba703ee5 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 be627cc4d4..2ee74557d5 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 5ec2d5f904..b08fed3442 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 90f6bb0017..f37630c1cd 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 ce884844df..8b9ce52840 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 final class Id3DecoderTest { 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 final class Id3DecoderTest { 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 final class Id3DecoderTest { 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 final class Id3DecoderTest { 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 final class Id3DecoderTest { 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 final class Id3DecoderTest { 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 final class Id3DecoderTest { 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 final class Id3DecoderTest { 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 19854db16b..0a6f8bb3b4 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.common.util.Util; 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { + 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { @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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { } 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { } @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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { } 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { } @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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { 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 @@ public final class Id3Decoder extends SimpleMetadataDecoder { /** * 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 24a1db8359..22c5a30d27 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 e7fdf2dd46..f2df8effff 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 abstract class DownloadService extends Service { Util.startForegroundService(context, intent); } + /** + * Clear all {@linkplain DownloadManagerHelper download manager helpers} before restarting the + * service. + * + *

    Calling 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 22c5a30d27..5eeaf2f8d2 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 32dbee1e1e..87f7e6f20c 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 a7a2481843..5e0b045cd8 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 @@ import com.google.android.gms.cast.MediaTrack; /** 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 @@ import com.google.android.gms.cast.MediaTrack; 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 968253e4b3..ac44ebb603 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 @@ public final class 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 @@ public final class C { ENCODING_AC4, ENCODING_DTS, ENCODING_DTS_HD, - ENCODING_DOLBY_TRUEHD + ENCODING_DOLBY_TRUEHD, + ENCODING_OPUS, }) public @interface Encoding {} @@ -325,6 +326,10 @@ public final class 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 859773b2a6..1bcab6a3ee 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 final class MimeTypes { 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 605f5f0d44..4adcffdaf8 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.Ac3Util; import androidx.media3.extractor.Ac4Util; import androidx.media3.extractor.DtsUtil; import androidx.media3.extractor.MpegAudioUtil; +import androidx.media3.extractor.OpusUtil; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.InlineMe; import com.google.errorprone.annotations.InlineMeValidationDisabled; @@ -1787,6 +1788,8 @@ public final class DefaultAudioSink implements AudioSink { ? 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 62c72a5722..317f06d05c 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.Ac3Util; import androidx.media3.extractor.Ac4Util; import androidx.media3.extractor.DtsUtil; import androidx.media3.extractor.MpegAudioUtil; +import androidx.media3.extractor.OpusUtil; import com.google.errorprone.annotations.CanIgnoreReturnValue; /** Provide the buffer size to use when creating an {@link AudioTrack}. */ @@ -255,6 +256,8 @@ public class DefaultAudioTrackBufferSizeProvider 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 81a1adedd1..a1ecff461e 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 class OpusUtil { 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 95996f6a80..00e12a7b56 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 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; @Override protected long preparePayload(ParsableByteArray packet) { - return convertTimeToGranule(getPacketDurationUs(packet.getData())); + return convertTimeToGranule(OpusUtil.getPacketDurationUs(packet.getData())); } @Override @@ -121,42 +121,6 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; } } - /** - * 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 1bc6b3431c..48c74327c2 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 final class OpusUtilTest { } @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 @@ public final class OpusUtilTest { 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 5eeaf2f8d2..887eea6c08 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 4adcffdaf8..e94d6d2416 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 final class DefaultAudioSink implements AudioSink { * @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 @@ public final class DefaultAudioSink implements AudioSink { @OutputMode int outputMode, int pcmFrameSize, int sampleRate, + int bitrate, double maxAudioTrackPlaybackSpeed); } @@ -791,6 +794,7 @@ public final class DefaultAudioSink implements AudioSink { 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 317f06d05c..ef40d2c4c1 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.common.util.Util.constrainValue; 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.DtsUtil; 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 class DefaultAudioTrackBufferSizeProvider @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 class DefaultAudioTrackBufferSizeProvider /** 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 @@ public class DefaultAudioTrackBufferSizeProvider } /** 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 7f6f41314f..fae6430f8b 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 androidx.media3.exoplayer.audio.DefaultAudioTrackBufferSizeProvide 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 638dbf5661..0d2723bb54 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 class DefaultAudioTrackBufferSizeProviderEncodedTest { 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 class DefaultAudioTrackBufferSizeProviderEncodedTest { /* 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 0b922a9c3e..d27999ed65 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 com.google.common.truth.Truth.assertThat; 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 class DefaultAudioTrackBufferSizeProviderPcmTest { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize).isEqualTo(roundUpToFrame(1234567890)); @@ -103,6 +105,7 @@ public class DefaultAudioTrackBufferSizeProviderPcmTest { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize) @@ -121,6 +124,7 @@ public class DefaultAudioTrackBufferSizeProviderPcmTest { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize) @@ -139,6 +143,7 @@ public class DefaultAudioTrackBufferSizeProviderPcmTest { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize) @@ -157,6 +162,7 @@ public class DefaultAudioTrackBufferSizeProviderPcmTest { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize) @@ -175,6 +181,7 @@ public class DefaultAudioTrackBufferSizeProviderPcmTest { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1); assertThat(bufferSize) @@ -190,6 +197,7 @@ public class DefaultAudioTrackBufferSizeProviderPcmTest { /* outputMode= */ OUTPUT_MODE_PCM, /* pcmFrameSize= */ getPcmFrameSize(), /* sampleRate= */ sampleRate, + /* bitrate= */ Format.NO_VALUE, /* maxAudioTrackPlaybackSpeed= */ 1 / 5F); assertThat(bufferSize) @@ -205,6 +213,7 @@ public class DefaultAudioTrackBufferSizeProviderPcmTest { /* 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 45de1f5a94..5cdc263033 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 class DefaultMediaNotificationProvider implements MediaNotification.Provi 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 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi 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 bf23e9c894..fe7616bce3 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 class DefaultMediaNotificationProviderTest { 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 f58882351d..7caf715ea5 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } 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 350a23920c..acb6b2c397 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { /** * 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 abstract class SimpleBasePlayer extends BasePlayer { /** * 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 abstract class SimpleBasePlayer extends BasePlayer { * 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { && 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { @Override public int getWindowCount() { - return playlistItems.size(); + return playlist.size(); } @Override @@ -1181,7 +1181,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { /** * 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { * {@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 abstract class SimpleBasePlayer extends BasePlayer { * 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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { } 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { } } - /** 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 abstract class SimpleBasePlayer extends BasePlayer { /** * 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 abstract class SimpleBasePlayer extends BasePlayer { /** * 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 abstract class SimpleBasePlayer extends BasePlayer { } } - /** 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { } 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { } 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { // 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 aef72f644f..d8a0e336d0 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 class SimpleBasePlayerTest { .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 class SimpleBasePlayerTest { } @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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { .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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { } @Test - public void stateBuilderBuild_multiplePlaylistItemsWithSameIds_throwsException() { + public void stateBuilderBuild_multipleMediaItemsWithSameIds_throwsException() { Object uid = new Object(); assertThrows( @@ -475,8 +478,8 @@ public class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { } @Test - public void playlistItemBuilderBuild_setsCorrectValues() { + public void mediaItemDataBuilderBuild_setsCorrectValues() { Object uid = new Object(); Tracks tracks = new Tracks( @@ -635,8 +638,8 @@ public class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { .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 class SimpleBasePlayerTest { } @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 class SimpleBasePlayerTest { } @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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { .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 @@ public class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { .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 @@ public class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { .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 class SimpleBasePlayerTest { /* 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 class SimpleBasePlayerTest { .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 @@ public class SimpleBasePlayerTest { } @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 @@ public class SimpleBasePlayerTest { 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 @@ public class SimpleBasePlayerTest { 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 @@ public class SimpleBasePlayerTest { 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 @@ public class SimpleBasePlayerTest { 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 @@ public class SimpleBasePlayerTest { 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 @@ public class SimpleBasePlayerTest { @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 6b25c8d56c..2bf05e7a27 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 class MediaSession { /* 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 b9279635d8..9fe613aac2 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 final class Ac3Util { 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 @@ public final class Ac3Util { 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 final class Ac3Util { int sampleCount; boolean lfeon; int channelCount; + int bitrate; if (isEac3) { // Subsection E.1.2. data.skipBits(16); // syncword @@ -293,6 +298,7 @@ public final class Ac3Util { 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 final class Ac3Util { 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 final class Ac3Util { 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 @@ public final class Ac3Util { } } + /** + * 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 1d80fbca08..02d674c973 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.IntDef; 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 @@ public final class Ac3Reader implements ElementaryStreamReader { || 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 3f582caedd..8aad7940f2 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 3f582caedd..8aad7940f2 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 f3d9d3997d..f8be0e618c 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 f3d9d3997d..f8be0e618c 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 27d0c450fd..143245058f 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 960882156b..62c215256f 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 561963e10c..f3ac4e2018 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 d778af898d..9f141492b2 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 f48ba43854..e6cea3993f 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 997d7a6b02..da9814ead3 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 a98cb798cb..f992ac64e8 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 355b403293..3a305ed662 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 355b403293..3a305ed662 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 dfc89c5f19..2ccaceef7f 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 c06294df2c..ccf162fc31 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 9104607498..286fc25b16 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 c490b7eca8..cfbfae20b6 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 0aae4097a7..4bb8650d72 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 f8888698bd..5a8f918105 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 f8888698bd..5a8f918105 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 a3cf812691..2fc2f49280 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 77951bd767..771c5216c5 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 0354754df2..452d8eea13 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 742d87e271..8da152a79f 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 269dd63593..82ce1d24bb 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 887eea6c08..0d6f026f79 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 05d37b29de..b1d23866a0 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 final class MediaMetadata implements Bundleable { @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 @@ public final class MediaMetadata implements Bundleable { 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 final class MediaMetadata implements Bundleable { 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 final class MediaMetadata implements Bundleable { 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 final class MediaMetadata implements Bundleable { } } + /** + * 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 final class MediaMetadata implements Bundleable { * *

    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 final class MediaMetadata implements Bundleable { @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 @@ public final class MediaMetadata implements Bundleable { 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 final class MediaMetadata implements Bundleable { && 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 final class MediaMetadata implements Bundleable { totalDiscCount, genre, compilation, - station); + station, + mediaType); } // Bundleable implementation. @@ -891,7 +1094,8 @@ public final class MediaMetadata implements Bundleable { FIELD_GENRE, FIELD_COMPILATION, FIELD_STATION, - FIELD_EXTRAS + FIELD_MEDIA_TYPE, + FIELD_EXTRAS, }) private @interface FieldNumber {} @@ -926,6 +1130,7 @@ public final class MediaMetadata implements Bundleable { 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 final class MediaMetadata implements Bundleable { 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 @@ public final class MediaMetadata implements Bundleable { 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 7e606597c4..4d66cd922a 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 class MediaMetadataTest { assertThat(mediaMetadata.genre).isNull(); assertThat(mediaMetadata.compilation).isNull(); assertThat(mediaMetadata.station).isNull(); + assertThat(mediaMetadata.mediaType).isNull(); assertThat(mediaMetadata.extras).isNull(); } @@ -149,6 +150,7 @@ public class MediaMetadataTest { .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 | 509 +++++- .../androidx/media3/common/util/Size.java | 3 + .../media3/common/SimpleBasePlayerTest.java | 1511 ++++++++++++++++- 3 files changed, 1955 insertions(+), 68 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 acb6b2c397..f42e912fc5 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.castNonNull; 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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { throw new IllegalStateException(); } + @Override + protected final void repeatCurrentMediaItem() { + // TODO: implement. + throw new IllegalStateException(); + } + @Override public final long getSeekBackIncrement() { verifyApplicationThreadAndInitState(); @@ -2169,8 +2204,16 @@ public abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @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 @@ -2332,58 +2414,122 @@ public abstract class SimpleBasePlayer extends BasePlayer { return state.volume; } - @Override - public final void clearVideoSurface() { - // TODO: implement. - throw new IllegalStateException(); - } - - @Override - public final void clearVideoSurface(@Nullable Surface surface) { - // TODO: implement. - throw new IllegalStateException(); - } - @Override public final void setVideoSurface(@Nullable Surface surface) { - // 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_VIDEO_SURFACE)) { + return; + } + if (surface == null) { + clearVideoSurface(); + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVideoOutput(surface), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setSurfaceSize(Size.UNKNOWN).build()); } @Override public final void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { - // TODO: implement. - throw new IllegalStateException(); - } - - @Override - public final void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { - // 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_VIDEO_SURFACE)) { + return; + } + if (surfaceHolder == null) { + clearVideoSurface(); + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVideoOutput(surfaceHolder), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setSurfaceSize(getSurfaceHolderSize(surfaceHolder)).build()); } @Override public final void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { - // TODO: implement. - throw new IllegalStateException(); - } - - @Override - public final void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) { - // 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_VIDEO_SURFACE)) { + return; + } + if (surfaceView == null) { + clearVideoSurface(); + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVideoOutput(surfaceView), + /* placeholderStateSupplier= */ () -> + state + .buildUpon() + .setSurfaceSize(getSurfaceHolderSize(surfaceView.getHolder())) + .build()); } @Override public final void setVideoTextureView(@Nullable TextureView textureView) { - // 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_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 clearVideoSurface() { + clearVideoOutput(/* videoOutput= */ null); + } + + @Override + public final void clearVideoSurface(@Nullable Surface surface) { + clearVideoOutput(surface); + } + + @Override + public final void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { + clearVideoOutput(surfaceHolder); + } + + @Override + 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 abstract class SimpleBasePlayer extends BasePlayer { @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { } /** - * 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { } 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 dddb834edd..5ffaac5911 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 d8a0e336d0..dc306838b5 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.never; 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.Cue; 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 class SimpleBasePlayerTest { .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 @@ public class SimpleBasePlayerTest { 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 | 162 ++++++++-- .../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, 476 insertions(+), 53 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 0d6f026f79..ce0948036b 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 0367ab8f22..bd1117bc78 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 @@ package androidx.media3.common.util; 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 @@ import java.util.Arrays; @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 final class ParsableByteArray { } /** - * Reads a line of text. + * Reads a line of text in UTF-8. * - *

    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. - * - * @return The line not including any line-termination characters, or null if the end of the data - * has already been reached. + *

    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'). 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(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 final class ParsableByteArray { 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 ddaf7ee981..cddf95c9f8 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 final class ParsableByteArrayTest { } @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 1ecc7f425d..6147ff92ad 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.ParsableByteArray; 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 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { 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 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } // 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 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { // 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 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { 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 e0339d8f97..e66888b807 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.ForegroundColorSpan; 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.SimpleSubtitleDecoder; 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 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { 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 e9a4b8f8b8..259a72809d 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 final class SubripDecoderTest { 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 cddf95c9f8..2a97c0dfd9 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 final class ParsableByteArrayTest { 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 final class ParsableByteArrayTest { @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 final class ParsableByteArrayTest { @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 final class ParsableByteArrayTest { 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 final class ParsableByteArrayTest { 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 final class ParsableByteArrayTest { 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 final class ParsableByteArrayTest { 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 final class ParsableByteArrayTest { 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 final class ParsableByteArrayTest { (byte) 0x20, }; ParsableByteArray byteArray = new ParsableByteArray(data); + assertThat(byteArray.readString(data.length)).isEqualTo("ä ö ® π √ ± 谢 "); assertThat(byteArray.getPosition()).isEqualTo(data.length); } @@ -430,6 +439,7 @@ public final class ParsableByteArrayTest { 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 final class ParsableByteArrayTest { 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 final class ParsableByteArrayTest { } @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 final class ParsableByteArrayTest { 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 final class ParsableByteArrayTest { @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 final class ParsableByteArrayTest { 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 final class ParsableByteArrayTest { @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 259a72809d..642e20e259 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 final class SubripDecoderTest { 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 final class SubripDecoderTest { 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 final class SubripDecoderTest { 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 final class SubripDecoderTest { 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 final class SubripDecoderTest { 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 final class SubripDecoderTest { 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 final class SubripDecoderTest { 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 final class SubripDecoderTest { 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 final class SubripDecoderTest { 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 final class SubripDecoderTest { 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 final class SubripDecoderTest { 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 5cdc263033..b0fd6bc36a 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 class DefaultMediaNotificationProvider implements MediaNotification.Provi 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 class DefaultMediaNotificationProvider implements MediaNotification.Provi @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 class DefaultMediaNotificationProvider implements MediaNotification.Provi 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 a80301d509..2215b07071 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (this != pendingBitmapLoadCallback) { return; } - Log.d(TAG, "Failed to load bitmap", t); + Log.w(TAG, getBitmapLoadErrorMessage(t)); } }; Futures.addCallback( @@ -1270,4 +1270,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 cc8291c27d..16ca1a25a5 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 d79385f5e4..8dda126ce4 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.app.PendingIntent; 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 7caf715ea5..c805129dd4 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 83c5a4e3f8..59209c334a 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 final class MediaUtilsTest { } @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 final class MediaUtilsTest { } @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 final class MediaUtilsTest { 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 5d9e56a7c7..2a6af52728 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 final class MediaTestUtils { 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 0e7694dc03..810a6ac9b7 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 f42e912fc5..893968b3b0 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { && 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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 dc306838b5..0ef67a9e7d 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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { .setDeviceInfo(deviceInfo) .setDeviceVolume(5) .setIsDeviceMuted(true) - .setAudioSessionId(78) - .setSkipSilenceEnabled(true) .setSurfaceSize(surfaceSize) .setNewlyRenderedFirstFrame(true) .setTimedMetadata(timedMetadata) @@ -311,8 +307,6 @@ public class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { .setDeviceInfo(deviceInfo) .setDeviceVolume(5) .setIsDeviceMuted(true) - .setAudioSessionId(78) - .setSkipSilenceEnabled(true) .setSurfaceSize(surfaceSize) .setPlaylist(playlist) .setPlaylistMetadata(playlistMetadata) @@ -1160,8 +1152,6 @@ public class SimpleBasePlayerTest { .setDeviceInfo(deviceInfo) .setDeviceVolume(5) .setIsDeviceMuted(true) - .setAudioSessionId(78) - .setSkipSilenceEnabled(true) .setSurfaceSize(surfaceSize) .setNewlyRenderedFirstFrame(true) .setTimedMetadata(timedMetadata) @@ -1227,11 +1217,9 @@ public class SimpleBasePlayerTest { 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 @@ public class SimpleBasePlayerTest { 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 @@ public class SimpleBasePlayerTest { 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 ce0948036b..f26f8fae06 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 2ddbd5908b..8cd8bb0c37 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 @@ public final class TextRenderer extends BaseRenderer implements Callback { @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 eef9c43b4b..8aafe98324 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 final class ExoPlayerTest { .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 final class ExoPlayerTest { .build() .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET); + assertThat(target.positionMs).isEqualTo(C.TIME_UNSET); } @Test @@ -12292,7 +12292,7 @@ public final class ExoPlayerTest { 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 bd9cf63426..d601b74a18 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 static androidx.media3.common.util.Assertions.checkState; 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 @@ import androidx.media3.common.util.Util; 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 @@ import androidx.media3.common.util.Util; * 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 @@ import androidx.media3.common.util.Util; 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 de50b10f56..c9627dd9ff 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 @@ public final class Transformer { /* 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 e5dc534a8a..d0b0e4c0f3 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 android.os.Looper; 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 class FallbackListenerTest { 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 class FallbackListenerTest { 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 class FallbackListenerTest { 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 class FallbackListenerTest { 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 @@ public class FallbackListenerTest { 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 e92ed9db6a..065741b659 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 ccd919b9b7..d8b25289b7 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 216342644a..f2ef17bac6 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 e0c51e762f..793664d5ad 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 42e17153e7..4e75082831 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 c64f8b3bae..4a5dbbd674 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 6be5b52137..cb62baa0b4 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 7f52a665b3..27890cff86 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 7d738f5230..150fcef72a 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 75e89ad5f5..a4f741490a 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 21127e65c9..a819fc23ad 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 6d1046a073..e381d2f8e1 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 44845605c6..26195664a8 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 fc63129a9d..e504c7a730 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 50fc67fe3b..532c8f61b8 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 6f6b0d3b6a..0fca23f366 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 8c52a83963..3f7ec5035f 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 34f31c312d..f89d324d06 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 b2143d26e3..06ada682a7 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 04bfa67662..f83220fe1d 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 1650a22881..076985bee7 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 ed3d5dd3a5..7fa6c6d267 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 cd15318192..22b82fa5b5 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 b0fa26d687..f8d78bb573 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 48d5e10036..4dbd123eee 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 0ff37e7aea..2edafcd386 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 0f09831abd..fe864c584d 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 893968b3b0..731dca5630 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { private final Timeline.Period period; private @MonotonicNonNull State state; + private boolean released; /** * Creates the base class. @@ -1999,7 +2000,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { */ protected final void invalidateState() { verifyApplicationThreadAndInitState(); - if (!pendingOperations.isEmpty()) { + if (!pendingOperations.isEmpty() || released) { return; } updateStateAndInformListeners(getState()); @@ -2672,6 +2690,18 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { () -> { 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 0ef67a9e7d..5c53e5e27b 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.anyBoolean; 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.lang.reflect.Modifier; 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 @@ public class SimpleBasePlayerTest { 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 f26f8fae06..e3b7ce1b0b 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 d1ece8ba12..a1a6c6c187 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 b1d23866a0..470ed7a71c 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 final class MediaMetadata implements Bundleable { @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 @@ public final class MediaMetadata implements Bundleable { 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 final class MediaMetadata implements Bundleable { 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 final class MediaMetadata implements Bundleable { 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 final class MediaMetadata implements Bundleable { @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 final class MediaMetadata implements Bundleable { @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 @@ public final class MediaMetadata implements Bundleable { 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 @@ public final class MediaMetadata implements Bundleable { 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 final class MediaMetadata implements Bundleable { && 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 final class MediaMetadata implements Bundleable { trackNumber, totalTrackCount, folderType, + isBrowsable, isPlayable, recordingYear, recordingMonth, @@ -1095,6 +1139,7 @@ public final class MediaMetadata implements Bundleable { FIELD_COMPILATION, FIELD_STATION, FIELD_MEDIA_TYPE, + FIELD_IS_BROWSABLE, FIELD_EXTRAS, }) private @interface FieldNumber {} @@ -1131,6 +1176,7 @@ public final class MediaMetadata implements Bundleable { 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 final class MediaMetadata implements Bundleable { 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 @@ public final class MediaMetadata implements Bundleable { 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 @@ public final class MediaMetadata implements Bundleable { 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 4d66cd922a..f3a7418fc7 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 class MediaMetadataTest { 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 class MediaMetadataTest { 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 @@ public class MediaMetadataTest { .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 2984dbc604..471d0200eb 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 final class LibraryResult implements Bundleable { /** * 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 final class LibraryResult implements Bundleable { * #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 @@ public final class LibraryResult implements Bundleable { 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 742c31e879..fc924af3d9 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 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; 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 8dda126ce4..5da473a821 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 bb6b8ebfa9..48442529ff 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 abstract class MediaLibraryService extends MediaSessionService { * 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 c805129dd4..919d552178 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } - 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } 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 e1ab29d946..a4d7afc33c 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 class LibraryResultTest { @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 45c7171ccb..5884f6c6ba 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 static java.util.concurrent.TimeUnit.MILLISECONDS; 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 class MediaBrowserListenerTest extends MediaControllerListenerTest { 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 043b654f5c..873d2a4441 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 class MediaBrowserServiceCompatCallbackWithMediaBrowserTest { 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 4180c3a357..850cf31860 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 @@ public class MediaSessionServiceNotificationTest { .setTitle("Test Song Name") .setArtist("Test Artist Name") .setArtworkData(artworkData) - .setFolderType(MediaMetadata.FOLDER_TYPE_NONE) .setIsPlayable(true) .build(); } @@ -147,7 +146,6 @@ public class MediaSessionServiceNotificationTest { 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 2a6af52728..bd82b76e2a 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 final class MediaTestUtils { 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 final class MediaTestUtils { 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 final class MediaTestUtils { 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 d460bee20b..f5a2da634e 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 class MockMediaLibraryService extends MediaLibraryService { 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 class MockMediaLibraryService extends MediaLibraryService { 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 @@ public class MockMediaLibraryService extends MediaLibraryService { 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 @@ public class MockMediaLibraryService extends MediaLibraryService { 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 ef9c4e5a3f..32f2c01b9e 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 final class RobolectricUtil { */ 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 2fc70006a3..44464e39b9 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 @@ import java.util.List; * 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 @@ public interface Player { /** * 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 @@ public interface Player { *

    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 731dca5630..2a2a3ffc45 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 abstract class SimpleBasePlayer extends BasePlayer { @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 78e529ae3a..0ab3bab541 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; *

    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 final class ListenerSet { 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 @@ public final class 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 final class ListenerSet { /** * 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 final class ListenerSet { * *

    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 final class ListenerSet { * @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 final class ListenerSet { /** 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 final class ListenerSet { * @param event The event. */ public void queueEvent(int eventFlag, Event event) { + verifyCurrentThread(); CopyOnWriteArraySet> listenerSnapshot = new CopyOnWriteArraySet<>(listeners); queuedEvents.add( () -> { @@ -196,6 +220,7 @@ public final class ListenerSet { /** Notifies listeners of events previously enqueued with {@link #queueEvent(int, Event)}. */ public void flushEvents() { + verifyCurrentThread(); if (queuedEvents.isEmpty()) { return; } @@ -234,11 +259,27 @@ public final class ListenerSet { *

    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 @@ public final class ListenerSet { 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 eae251688e..e58db58847 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 @@ import java.util.List; * 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 interface ExoPlayer extends Player { /** * 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 interface ExoPlayer extends Player { /** * 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 interface ExoPlayer extends Player { @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 d896bc7654..5f27dc546a 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.PlayerMessage.Target; 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 @@ import java.util.concurrent.TimeoutException; @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 @@ import java.util.concurrent.TimeoutException; @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 @@ import java.util.concurrent.TimeoutException; @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 @@ import java.util.concurrent.TimeoutException; 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 74062ab3e3..5044d8fa56 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 class DefaultAnalyticsCollector implements AnalyticsCollector { 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 8aafe98324..3fa73c69fc 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 final class ExoPlayerTest { @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 5affe5c695..496e6ea946 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 class MediaController implements Player { @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 class MediaController implements Player { @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 cc2567c0aa..20aca9ee0f 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.CommonConstants.DEFAULT_TEST_N 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.SurfaceView; 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.After; 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 @@ import org.junit.runner.RunWith; 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 class MediaControllerSurfaceTest { } @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 class MediaControllerSurfaceTest { }); 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 class MediaControllerSurfaceTest { }); 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 class MediaControllerSurfaceTest { }); 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 class MediaControllerSurfaceTest { }); 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 class MediaControllerSurfaceTest { }); 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 class MediaControllerSurfaceTest { }); 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 e3b7ce1b0b..4daafc1236 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 57b81b3fa5..b7c1443816 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 final class CastPlayer extends BasePlayer { // 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 74b144baa3..b0f31e5d21 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 @@ public abstract class BasePlayer implements Player { @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 abstract class BasePlayer implements Player { @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 abstract class BasePlayer implements Player { 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 abstract class BasePlayer implements Player { @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 abstract class BasePlayer implements Player { 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 abstract class BasePlayer implements Player { : 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 2a2a3ffc45..512b9d9311 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 android.view.TextureView; 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 abstract class SimpleBasePlayer extends BasePlayer { } @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 0000000000..4f3c677f66 --- /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 5f27dc546a..4e6ebf0c32 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 @@ import java.util.concurrent.TimeoutException; } @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 @@ import java.util.concurrent.TimeoutException; } } - 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 c5150e5c7f..5676ce7554 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 class SimpleExoPlayer extends BasePlayer 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 8638745a56..5845cdaceb 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 class StubPlayer extends BasePlayer { } @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 512b9d9311..0d6f24d98e 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 abstract class SimpleBasePlayer extends BasePlayer { .setTotalBufferedDurationMs(PositionSupplier.ZERO) .setContentBufferedPositionMs(state.contentPositionMsSupplier) .setAdBufferedPositionMs(state.adPositionMsSupplier) + .setIsLoading(false) .build()); } @@ -2231,6 +2232,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { .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 5c53e5e27b..498fb60068 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 class SimpleBasePlayerTest { .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 class SimpleBasePlayerTest { .buildUpon() .setPlaybackState(Player.STATE_IDLE) .setRepeatMode(Player.REPEAT_MODE_ALL) + .setIsLoading(false) .build(); SettableFuture future = SettableFuture.create(); SimpleBasePlayer player = @@ -2124,9 +2126,12 @@ public class SimpleBasePlayerTest { // 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 class SimpleBasePlayerTest { .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 @@ public class SimpleBasePlayerTest { 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 bb712e2472..8e08993f1a 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 @@ public final class Format implements Bundleable { + 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 7770a8c276..68f3a82882 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 final class MediaItem implements Bundleable { 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 final class MediaItem implements Bundleable { @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 final class MediaItem implements Bundleable { 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 final class MediaItem implements Bundleable { 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 final class MediaItem implements Bundleable { @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 final class MediaItem implements Bundleable { 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 final class MediaItem implements Bundleable { @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 f861a701ef..30b5853e5f 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 class MediaItemTest { } @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 class MediaItemTest { 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 class MediaItemTest { 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 class MediaItemTest { 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 4daafc1236..91e690245e 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 0d6f24d98e..c32260fc23 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { // 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { PositionInfo positionInfo = getPositionInfo( newState, - /* useDiscontinuityPosition= */ state.hasPositionDiscontinuity, + /* useDiscontinuityPosition= */ newState.hasPositionDiscontinuity, window, period); listeners.queueEvent( @@ -2928,9 +2937,9 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 498fb60068..b6b65b62c8 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 @@ public class SimpleBasePlayerTest { 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 | 218 ++++++++++-------- .../media3/common/SimpleBasePlayerTest.java | 218 +++++++++++++++--- 2 files changed, 309 insertions(+), 127 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 c32260fc23..aa677d85c8 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.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; +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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { *

      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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { *

      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 abstract class SimpleBasePlayer extends BasePlayer { *

      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 abstract class SimpleBasePlayer extends BasePlayer { *

      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 abstract class SimpleBasePlayer extends BasePlayer { * 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { } } 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { && 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { .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 abstract class SimpleBasePlayer extends BasePlayer { .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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 getCurrentPeriodIndexInternal(State state, Timeline.Window window) { - if (state.currentPeriodIndex != C.INDEX_UNSET) { - return state.currentPeriodIndex; - } - if (state.timeline.isEmpty()) { + private static int getCurrentMediaItemIndexInternal(State state) { + if (state.currentMediaItemIndex != C.INDEX_UNSET) { return state.currentMediaItemIndex; } - return state.timeline.getWindow(state.currentMediaItemIndex, window).firstPeriodIndex; + 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 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 currentMediaItemIndex; + } + 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { // 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 b6b65b62c8..2b2d6fb4ac 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 class SimpleBasePlayerTest { .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 class SimpleBasePlayerTest { .setPlaylist(playlist) .setPlaylistMetadata(playlistMetadata) .setCurrentMediaItemIndex(1) - .setCurrentPeriodIndex(1) .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2) .setContentPositionMs(contentPositionSupplier) .setAdPositionMs(adPositionSupplier) @@ -315,7 +313,6 @@ public class SimpleBasePlayerTest { 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,7 +359,32 @@ public class SimpleBasePlayerTest { } @Test - public void stateBuilderBuild_currentWindowIndexExceedsPlaylistLength_throwsException() { + 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_currentMediaItemIndexSetForEmptyPlaylist_doesNotThrow() { + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist(ImmutableList.of()) + .setCurrentMediaItemIndex(20) + .build(); + + assertThat(state.currentMediaItemIndex).isEqualTo(20); + } + + @Test + public void stateBuilderBuild_currentMediaItemIndexExceedsPlaylistLength_throwsException() { assertThrows( IllegalArgumentException.class, () -> @@ -376,37 +398,6 @@ public class SimpleBasePlayerTest { .build()); } - @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()); - } - - @Test - public void stateBuilderBuild_currentPeriodIndexInOtherMediaItem_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(0) - .setCurrentPeriodIndex(1) - .build()); - } - @Test public void stateBuilderBuild_currentAdGroupIndexExceedsAdGroupCount_throwsException() { assertThrows( @@ -453,6 +444,16 @@ public class SimpleBasePlayerTest { .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 class SimpleBasePlayerTest { 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 class SimpleBasePlayerTest { .setPlaylist(playlist) .setPlaylistMetadata(playlistMetadata) .setCurrentMediaItemIndex(1) - .setCurrentPeriodIndex(1) .setContentPositionMs(contentPositionSupplier) .setContentBufferedPositionMs(contentBufferedPositionSupplier) .setTotalBufferedDurationMs(totalBufferedPositionSupplier) @@ -1049,6 +1070,131 @@ public class SimpleBasePlayerTest { 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 470ed7a71c..9f6b0f2035 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 final class MediaMetadata implements Bundleable { @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 f3a7418fc7..bde20bc603 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 class MediaMetadataTest { } @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 91e690245e..3b4ca8610d 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 30d94ef39e..b1669556d2 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.content.res.Resources; 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.util.SparseLongArray; 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 final class Util { 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 @@ public final class Util { 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 67197ade22..b0be016e63 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_POSITION_DISCONTINUITY; 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 class LegacyPlayerControlView extends FrameLayout { 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 8c56f78298..461dbd2dd0 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_TIMELINE_CHANGED; 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.FrameLayout; 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 class PlayerControlView extends FrameLayout { 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 class PlayerControlView extends FrameLayout { 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 class PlayerControlView extends FrameLayout { 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 @@ public class PlayerControlView extends FrameLayout { 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 998bd845ba..6731d040a3 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 @@ package androidx.media3.ui; 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 class PlayerView extends FrameLayout implements AdViewProvider { 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 @@ public class PlayerView extends FrameLayout implements AdViewProvider { } @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 43dc1aed11..679df19aae 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 abstract class Timeline 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 abstract class Timeline 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 abstract class Timeline implements Bundleable { 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 b2f005c47d..8ad14b9cda 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 { } private static final int PERIOD_COUNT_UNSET = -1; - private static final MediaItem EMPTY_MEDIA_ITEM = + private static final MediaItem PLACEHOLDER_MEDIA_ITEM = new MediaItem.Builder().setMediaId("MergingMediaSource").build(); private final boolean adjustPeriodTimeOffsets; @@ -163,7 +163,7 @@ public final class MergingMediaSource extends CompositeMediaSource { @Override public MediaItem getMediaItem() { - return mediaSources.length > 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 b7c1443816..8d2a0cbde1 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.TrackSelectionParameters; 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 final class CastPlayer extends BasePlayer { @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 final class CastPlayer extends BasePlayer { @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 final class CastPlayer extends BasePlayer { @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 final class CastPlayer extends BasePlayer { 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 44464e39b9..be64212d21 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { * 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 @@ public interface Player { /** * 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 4e6ebf0c32..525b2df9c4 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 @@ package androidx.media3.exoplayer; 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.Tracks; 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 @@ import java.util.concurrent.TimeoutException; @Override public void addMediaItems(int index, List mediaItems) { verifyApplicationThread(); - index = min(index, mediaSourceHolderSnapshots.size()); addMediaSources(index, createMediaSources(mediaItems)); } @@ -648,7 +647,8 @@ import java.util.concurrent.TimeoutException; @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 @@ import java.util.concurrent.TimeoutException; @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 @@ import java.util.concurrent.TimeoutException; @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 @@ import java.util.concurrent.TimeoutException; @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 @@ import java.util.concurrent.TimeoutException; } 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 @@ import java.util.concurrent.TimeoutException; 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 3fa73c69fc..fb2ae47b59 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.AdPlaybackState; 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 final class ExoPlayerTest { } @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 6560cea856..e3a2ca4a33 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; if (!isPlayerCommandAvailable(Player.COMMAND_SEEK_TO_MEDIA_ITEM)) { return; } + checkArgument(mediaItemIndex >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_SEEK_TO_MEDIA_ITEM, @@ -499,6 +500,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; if (!isPlayerCommandAvailable(Player.COMMAND_SEEK_TO_MEDIA_ITEM)) { return; } + checkArgument(mediaItemIndex >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_SEEK_TO_MEDIA_ITEM, @@ -938,6 +940,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { return; } + checkArgument(index >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_CHANGE_MEDIA_ITEMS, @@ -969,6 +972,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { return; } + checkArgument(index >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_CHANGE_MEDIA_ITEMS, @@ -1038,6 +1042,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { return; } + checkArgument(index >= 0); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_CHANGE_MEDIA_ITEMS, @@ -1051,6 +1056,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { return; } + checkArgument(fromIndex >= 0 && toIndex >= fromIndex); dispatchRemoteSessionTaskWithPlayerCommand( Player.COMMAND_CHANGE_MEDIA_ITEMS, @@ -1073,19 +1079,17 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 2c670fa855..ff8e3c7c9a 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.annotation.Nullable; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void addMediaItems(int index, List mediaItems) { + checkArgument(index >= 0); if (mediaItems.isEmpty()) { return; } @@ -732,9 +731,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @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 7e7f18dc17..e923f21d5a 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.media3.test.session.common.TestUtils; 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 class MediaControllerStateMaskingTest { 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 e10dd5ae96..4fb1de36af 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.media3.test.session.common.MainLooperTestRule; 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 class MediaControllerStateMaskingWithMediaSessionCompatTest { 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 437062c570..1bd45fc24a 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 class MediaCodecVideoRenderer extends MediaCodecRenderer { } 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 d01fb6eee3..4e075c18bd 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; @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 @@ import org.checkerframework.checker.initialization.qual.Initialized; /* 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; protected void setSessionPositionUpdateDelayMsOnHandler(long updateDelayMs) { verifyApplicationThread(); sessionPositionUpdateDelayMs = updateDelayMs; - - applicationHandler.removeCallbacks(this::notifyPeriodicSessionPositionInfoChangesOnHandler); - applicationHandler.postDelayed( - this::notifyPeriodicSessionPositionInfoChangesOnHandler, updateDelayMs); + schedulePeriodicSessionPositionInfoChanges(); } @Nullable @@ -718,9 +717,15 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onIsPlayingChanged(seq, isPlaying)); + session.schedulePeriodicSessionPositionInfoChanges(); } @Override @@ -877,6 +883,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; /* 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 3c13e756ce..a7b5dbfc6c 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.KEY_AVAI 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 class MediaControllerTest { @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.setSessionPositionUpdateDelayMs(10L); + remoteSession.getMockPlayer().setBufferedPosition(testBufferedPosition); - - remoteSession.setSessionPositionUpdateDelayMs(0L); - PollingCheck.waitFor( TIMEOUT_MS, () -> { @@ -998,6 +1006,31 @@ public class MediaControllerTest { }); } + @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 3b4ca8610d..f0c4f88892 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 aa677d85c8..6ab13a7a37 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.castNonNull; 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.Futures; 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 abstract class SimpleBasePlayer extends BasePlayer { @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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { if (!pendingOperations.isEmpty() || released) { return; } - updateStateAndInformListeners(getState()); + updateStateAndInformListeners( + getState(), /* seeked= */ false, /* isRepeatingCurrentItem= */ false); } /** @@ -2650,6 +2751,26 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { } 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { State previousState, State newState, int positionDiscontinuityReason, + boolean isRepeatingCurrentItem, Timeline.Window window) { Timeline previousTimeline = previousState.timeline; Timeline newTimeline = newState.timeline; @@ -3353,6 +3602,10 @@ public abstract class SimpleBasePlayer extends BasePlayer { .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 @@ public abstract class SimpleBasePlayer extends BasePlayer { && 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 2b2d6fb4ac..a78d2a9c58 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.anyLong; 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 com.google.common.util.concurrent.SettableFuture; 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 @@ public class SimpleBasePlayerTest { /* 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 @@ public class SimpleBasePlayerTest { 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 @@ public class SimpleBasePlayerTest { 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 be64212d21..089bb3c0ae 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 @@ public interface Player { * 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 84a8c9eb75..51bc101b0d 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 final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab } @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 2bf05e7a27..d03f24e0bf 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 class MediaSession { /* 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 2ee3be9c96..ffe84bb11e 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.annotation.Nullable; 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 final class MediaBrowser extends MediaController { private Bundle connectionHints; private Listener listener; private Looper applicationLooper; + private @MonotonicNonNull BitmapLoader bitmapLoader; /** * Creates a builder for {@link MediaBrowser}. @@ -121,6 +123,21 @@ public final class MediaBrowser extends MediaController { 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 final class MediaBrowser extends MediaController { */ 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 @@ public final class MediaBrowser extends MediaController { 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 @@ public final class MediaBrowser extends MediaController { 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 fc924af3d9..8b3fb24eef 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 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; 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 496e6ea946..e9855421d6 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.Executor; 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 class MediaController implements Player { private Bundle connectionHints; private Listener listener; private Looper applicationLooper; + private @MonotonicNonNull BitmapLoader bitmapLoader; /** * Creates a builder for {@link MediaController}. @@ -261,6 +263,21 @@ public class MediaController implements Player { 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 class MediaController implements Player { 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 @@ public class MediaController implements Player { 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 @@ public class MediaController implements Player { 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 @@ public class MediaController implements Player { 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 ff8e3c7c9a..489997beb8 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 static java.lang.Math.min; 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 com.google.common.util.concurrent.SettableFuture; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; this.instance = instance; controllerCompatCallback = new ControllerCompatCallback(applicationLooper); this.token = token; + this.bitmapLoader = bitmapLoader; } /* package */ MediaController getInstance() { @@ -716,11 +723,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } // 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 919d552178..11735dd6ac 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 static java.util.concurrent.TimeUnit.MILLISECONDS; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 82e4008c4a..b16847b8a8 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.RepeatMode; 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.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 java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -327,7 +329,7 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { @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 class MediaSessionCompatCallbackWithMediaControllerTest { 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 class MediaSessionCompatCallbackWithMediaControllerTest { 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 089bb3c0ae..d974a10a44 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 @@ import java.util.List; */ 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 @@ public interface Player { /** * 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 interface Player { } /** - * 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 interface Player { /** Creates an instance. */ @UnstableApi + @SuppressWarnings("deprecation") // Setting deprecated windowIndex field public PositionInfo( @Nullable Object windowUid, int mediaItemIndex, @@ -349,7 +350,7 @@ public interface Player { } /** - * A set of {@link Command commands}. + * A set of {@linkplain Command commands}. * *

      Instances are immutable. */ @@ -433,9 +434,9 @@ public interface Player { } /** - * 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 interface Player { /** * 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 interface Player { } /** - * 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 interface Player { } /** - * 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 @@ public interface Player { *

      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 @@ public interface Player { *

      {@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 @@ public interface Player { * 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 @@ public interface Player { *

      {@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 @@ public interface Player { 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 @@ public interface Player { /** * 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 @@ public interface Player { 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 @@ public interface Player { 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 @@ public interface Player { */ @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 @@ public interface Player { * @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 @@ public interface Player { * @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 @@ public interface Player { 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 @@ public interface Player { 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 @@ public interface Player { 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 @@ public interface Player { 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 @@ public interface Player { /** * 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 @@ public interface Player { * * @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 @@ public interface Player { 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 @@ public interface Player { * 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 @@ public interface Player { /** * 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 @@ public interface Player { @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 @@ public interface Player { /** * 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 d974a10a44..b3f024192a 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 @@ public interface Player { *

      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 @@ public interface Player { 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { */ 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 @@ public interface Player { * *

        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 @@ public interface Player { * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { * *

        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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { * 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 @@ public interface Player { * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { *

        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 @@ public interface Player { * 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 @@ public interface Player { *

      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 @@ public interface Player { * 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 @@ public interface Player { * 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 @@ public interface Player { *

    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 @@ public interface Player { * *

    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 @@ public interface Player { * 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 @@ public interface Player { * .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 @@ public interface Player { * 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 @@ public interface Player { /** * 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 @@ public interface Player { * 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 @@ public interface Player { *

    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 @@ public interface Player { *

    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 @@ public interface Player { /** * 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 @@ public interface Player { */ 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 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { * 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 @@ public interface Player { * 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 @@ public interface Player { *

    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 @@ public interface Player { * 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 @@ public interface Player { *

    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 @@ public interface Player { * 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 @@ public interface Player { *

    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 @@ public interface Player { * 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 @@ public interface Player { @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 @@ public interface Player { * *

    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 6ab13a7a37..842f63912b 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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 abstract class SimpleBasePlayer extends BasePlayer { 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @ForOverride protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_PLAY_PAUSE"); } /** @@ -2795,7 +2818,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @ForOverride protected ListenableFuture handlePrepare() { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_PREPARE"); } /** @@ -2808,7 +2831,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @ForOverride protected ListenableFuture handleStop() { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_STOP"); } /** @@ -2820,7 +2843,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { // 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 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @ForOverride protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEnabled) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_SET_SHUFFLE_MODE"); } /** @@ -2862,7 +2885,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @ForOverride protected ListenableFuture handleIncreaseDeviceVolume() { - throw new IllegalStateException(); + throw new IllegalStateException( + "Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME"); } /** @@ -2946,7 +2972,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @ForOverride protected ListenableFuture handleDecreaseDeviceVolume() { - throw new IllegalStateException(); + throw new IllegalStateException( + "Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME"); } /** @@ -2960,7 +2987,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @ForOverride protected ListenableFuture handleSetDeviceMuted(boolean muted) { - throw new IllegalStateException(); + throw new IllegalStateException( + "Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME"); } /** @@ -2975,7 +3003,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @ForOverride protected ListenableFuture handleSetVideoOutput(Object videoOutput) { - throw new IllegalStateException(); + throw new IllegalStateException("Missing implementation to handle COMMAND_SET_VIDEO_SURFACE"); } /** @@ -2992,7 +3020,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { */ @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 @@ public abstract class SimpleBasePlayer extends BasePlayer { @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 f0c4f88892..7e9f8fd2b5 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 6731d040a3..b91d14cb5f 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 class PlayerView extends FrameLayout implements AdViewProvider { /** * 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 class PlayerView extends FrameLayout implements AdViewProvider { @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 class PlayerView extends FrameLayout implements AdViewProvider { 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 f80ac4dfa3..0000000000 --- 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 7e9f8fd2b5..14685340d9 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 cc8d1ef0e7..446fe90a29 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 bde20bc603..d2810ddd1b 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 class MediaMetadataTest { } @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 8efa8f218d..ec9f23199d 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 final class AdPlaybackState implements Bundleable { @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 final class AdPlaybackState implements Bundleable { 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 @@ public final class AdPlaybackState implements Bundleable { } } 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 d398cd5b0f..29d6383971 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 com.google.common.truth.Truth.assertThat; 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 class AdPlaybackStateTest { } @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 14685340d9..24760d16f9 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 0a5ad9a21d..c412079eb5 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 76fc35d287..401d73a8e6 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 24760d16f9..bbcbf00eba 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 150c852a91..2f328e1c15 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.content.Context; 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 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 29d6383971..6a07dce3dc 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 class AdPlaybackStateTest { 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 30b5853e5f..3f557ca8d2 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 class MediaItemTest { } @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 class MediaItemTest { } @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 class MediaItemTest { } @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 d2810ddd1b..904c55ee15 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 class MediaMetadataTest { 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 679df19aae..d95b27f2bc 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 abstract class Timeline implements Bundleable { 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 @@ public abstract class Timeline implements Bundleable { @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 abstract class Timeline implements Bundleable { *

    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 @@ public abstract class Timeline implements Bundleable { 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 6844330e14..111652b38b 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 @@ package androidx.media3.common; 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 class TimelineTest { /* 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 class TimelineTest { 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 class TimelineTest { /* 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 bbcbf00eba..62c3236332 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 4543d32819..5e290bc67d 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.GaplessInfoHolder; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 54bac6c44c..c3c8992476 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 @@ public class DefaultTrackSelector extends MappingTrackSelector { // 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 class DefaultTrackSelector extends MappingTrackSelector { 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 class DefaultTrackSelector extends MappingTrackSelector { 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 | 235 ++++++--------- .../androidx/media3/common/HeartRating.java | 35 +-- .../androidx/media3/common/MediaItem.java | 185 ++++-------- .../androidx/media3/common/MediaMetadata.java | 279 ++++++++---------- .../media3/common/PercentageRating.java | 29 +- .../media3/common/PlaybackException.java | 45 +-- .../media3/common/PlaybackParameters.java | 29 +- .../java/androidx/media3/common/Player.java | 80 ++--- .../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 | 67 +---- .../androidx/media3/common/VideoSize.java | 48 +-- .../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 | 263 ++++++----------- .../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, 1100 insertions(+), 2139 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 ec9f23199d..5bc8f9d0a9 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 @@ public final class AdPlaybackState implements Bundleable { // 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 final class AdPlaybackState implements Bundleable { // 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 @@ public final class AdPlaybackState implements Bundleable { contentResumeOffsetUs, isServerSideInserted); } - - private static String keyForField(@AdGroup.FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } /** @@ -1121,21 +1101,10 @@ public final class AdPlaybackState implements Bundleable { // 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 final class AdPlaybackState implements Bundleable { 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 final class AdPlaybackState implements Bundleable { 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 @@ public final class AdPlaybackState implements Bundleable { } } 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 11a8ef15bd..6406baf87a 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 final class AudioAttributes implements Bundleable { // 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 final class AudioAttributes implements Bundleable { 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 aae29250d1..034ada4fe8 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 final class ColorInfo implements Bundleable { // 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 2daeb92ef2..c75fcb7cc9 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 android.os.Bundle; 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 final class DeviceInfo implements Bundleable { // 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 final class DeviceInfo implements Bundleable { 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 8e08993f1a..450585d1f1 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 final class Format implements Bundleable { } // 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 final class Format implements Bundleable { @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 @@ public final class Format implements Bundleable { 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 @@ public final class Format implements Bundleable { } 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 08a6b405f3..22ca4bec1b 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 final class HeartRating extends Rating { 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 final class HeartRating extends Rating { @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 68f3a82882..4db28ca61f 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 @@ package androidx.media3.common; 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.ImmutableList; 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 final class MediaItem implements Bundleable { // 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 final class MediaItem implements Bundleable { 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 final class MediaItem implements Bundleable { // 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 final class MediaItem implements Bundleable { 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 final class MediaItem implements Bundleable { // 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 final class MediaItem implements Bundleable { 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 final class MediaItem implements Bundleable { } // 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 final class MediaItem implements Bundleable { 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 final class MediaItem implements Bundleable { @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 @@ public final class MediaItem implements Bundleable { 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 9f6b0f2035..822932377f 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 final class MediaMetadata implements Bundleable { // 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 final class MediaMetadata implements Bundleable { 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))); + .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(keyForField(FIELD_USER_RATING))) { - @Nullable Bundle fieldBundle = bundle.getBundle(keyForField(FIELD_USER_RATING)); + 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 afc20a1687..8504f3a6ce 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 final class PercentageRating extends Rating { 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 final class PercentageRating extends Rating { @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 2bcc674631..f9aa597856 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 class PlaybackException extends Exception implements Bundleable { @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 class PlaybackException extends Exception implements Bundleable { // 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 class PlaybackException extends Exception implements Bundleable { @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 @@ public class PlaybackException extends Exception implements Bundleable { @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 84881a55ce..df63b7d111 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 final class PlaybackParameters implements Bundleable { // 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 final class PlaybackParameters implements Bundleable { @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 b3f024192a..c9e7d4d360 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 interface Player { } // 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 interface Player { @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 interface Player { @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 @@ public interface Player { adGroupIndex, adIndexInAdGroup); } - - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } } /** @@ -581,13 +560,7 @@ public interface Player { // 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 interface Player { 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 interface Player { @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 @@ public interface Player { } 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 f0df87c434..0d0cd18533 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 static java.lang.annotation.ElementType.TYPE_USE; 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 @@ public abstract class Rating implements Bundleable { 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 2c38f7cb75..70aba78bfd 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 final class StarRating extends Rating { 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 final class StarRating extends Rating { @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 cd4ad73473..b6e0cb0687 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 final class ThumbRating extends Rating { 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 final class ThumbRating extends Rating { @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 d95b27f2bc..8e37968a0c 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.checkArgument; 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 androidx.media3.common.util.Util; 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 abstract class Timeline implements Bundleable { // 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 abstract class Timeline implements Bundleable { 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 abstract class Timeline implements Bundleable { @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 @@ public abstract class Timeline implements Bundleable { 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 abstract class Timeline implements Bundleable { // 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 abstract class Timeline implements Bundleable { 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 abstract class Timeline implements Bundleable { @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 @@ public abstract class Timeline implements Bundleable { 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 abstract class Timeline implements Bundleable { // 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 abstract class Timeline implements Bundleable { } 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 abstract class Timeline implements Bundleable { 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 @@ public abstract class Timeline implements Bundleable { 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 ce934111d5..f13d3dca4a 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 final class TrackGroup implements Bundleable { } // 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 final class TrackGroup implements Bundleable { 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 final class TrackGroup implements Bundleable { @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 a673e95bd8..c40e88b654 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.max; 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 final class TrackSelectionOverride implements Bundleable { @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 final class TrackSelectionOverride implements Bundleable { @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 1c2f7a633a..b65bc9400a 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 @@ public class TrackSelectionParameters implements Bundleable { @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 @@ public class TrackSelectionParameters implements Bundleable { 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 class TrackSelectionParameters implements Bundleable { // 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 class TrackSelectionParameters implements Bundleable { 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 class TrackSelectionParameters implements Bundleable { @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 6da0a9204c..28d2e89a97 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 @@ 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.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 final class Tracks implements Bundleable { } // 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 final class Tracks implements Bundleable { 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 final class Tracks implements Bundleable { } // 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 final class Tracks implements Bundleable { @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 9c25c257ae..fee94edbcd 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 final class VideoSize implements Bundleable { } // 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 475a29f9d3..cb50d1005f 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.annotation.Nullable; 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 final class Cue implements Bundleable { // 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 final class Cue implements Bundleable { 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 df11b6fda8..a77a75c66c 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 final class CueGroup implements Bundleable { // 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 b1669556d2..464db2648d 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 final class Util { : 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 3f557ca8d2..4df2c9d0b7 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 class MediaItemTest { @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 class MediaItemTest { 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 19d69529ca..82a958ce16 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 @@ public final class ExoPlaybackException extends PlaybackException { 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 @@ public final class ExoPlaybackException extends PlaybackException { @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 @@ public final class ExoPlaybackException extends PlaybackException { @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 0bc5a014f6..befc158d25 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.TrackGroup; 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 final class TrackGroupArray implements Bundleable { // 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 final class TrackGroupArray implements Bundleable { 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 @@ public final class TrackGroupArray implements Bundleable { } } } - - 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 c3c8992476..50a0ab216d 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 @@ public class DefaultTrackSelector extends MappingTrackSelector { // 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 @@ public class DefaultTrackSelector extends MappingTrackSelector { 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 class DefaultTrackSelector extends MappingTrackSelector { // 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 class DefaultTrackSelector extends MappingTrackSelector { // 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 @@ public class DefaultTrackSelector extends MappingTrackSelector { 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 class DefaultTrackSelector extends MappingTrackSelector { // 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 class DefaultTrackSelector extends MappingTrackSelector { @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 2f328e1c15..70fccc7655 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.splitAdGroup; 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.Handler; 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.ImmutableList; 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 final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou // 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 final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou @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 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou } 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 b8632b7617..989cd4a57c 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 @@ package androidx.media3.session; 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 @@ public final class CommandButton implements Bundleable { // 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 final class CommandButton implements Bundleable { @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 @@ public final class CommandButton implements Bundleable { : 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 @@ public final class CommandButton implements Bundleable { .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 6b09d056a5..037baff628 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 @@ package androidx.media3.session; 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 @@ import java.lang.annotation.Target; // 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 @@ import java.lang.annotation.Target; 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 afb57b623f..113848eda0 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 @@ import java.lang.annotation.Target; // 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 @@ import java.lang.annotation.Target; /* 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 @@ import java.lang.annotation.Target; 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 @@ import java.lang.annotation.Target; 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 471d0200eb..1d123d1107 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.MediaItem; 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 @@ public final class LibraryResult implements Bundleable { // 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 @@ public final class LibraryResult implements Bundleable { @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 @@ public final class LibraryResult implements Bundleable { */ 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 @@ public final class LibraryResult implements Bundleable { 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 48442529ff..6a96d47a7d 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.checkArgument; 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.content.Intent; 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.MediaItem; 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 abstract class MediaLibraryService extends MediaSessionService { // 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 abstract class MediaLibraryService extends MediaSessionService { @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 79c780c36e..dfb94e8a3d 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.MEDIA_ITEM_TRANSITION_REASON_REPEAT; 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.VideoSize; 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 @@ import java.lang.annotation.Target; // 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 @@ import java.lang.annotation.Target; // 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 @@ import java.lang.annotation.Target; boolean excludeTracks) { Bundle bundle = new Bundle(); if (playerError != null) { - bundle.putBundle(keyForField(FIELD_PLAYBACK_ERROR), playerError.toBundle()); + bundle.putBundle(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.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(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_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 @@ import java.lang.annotation.Target; 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 @@ import java.lang.annotation.Target; 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 35ef391826..c514af10c8 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.annotation.Nullable; 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 final class SessionCommand implements Bundleable { } // 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 final class SessionCommand implements Bundleable { 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 d255bfa970..ff8592dc31 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 @@ package androidx.media3.session; 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 @@ public final class SessionCommands implements Bundleable { // 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 final class SessionCommands implements Bundleable { 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 final class SessionCommands implements Bundleable { 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 final class SessionCommands implements Bundleable { } 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 f2464b5af2..f8960d2a87 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 @@ import java.lang.annotation.Target; // 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 @@ import java.lang.annotation.Target; 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 @@ import java.lang.annotation.Target; 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 f4ca56cf39..ebd389ba5d 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.IntDef; 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 @@ public final class SessionResult implements Bundleable { // 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 final class SessionResult implements Bundleable { @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 7a25ccc1c9..624e108d7b 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.Bundleable; 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 @@ public final class SessionToken implements Bundleable { } 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 @@ public final class SessionToken implements Bundleable { // 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 @@ public final class SessionToken implements Bundleable { 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 final class SessionToken implements Bundleable { 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 1c2e869b53..98250bab74 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 @@ package androidx.media3.session; 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 @@ import java.lang.annotation.Target; // 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 @@ import java.lang.annotation.Target; 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 @@ import java.lang.annotation.Target; 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 da43c9fbd6..a7edc7073c 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_BROWSER_SERVICE_LEGACY; 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 @@ import java.lang.annotation.Target; // 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 @@ import java.lang.annotation.Target; 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 | 470 +++++++++--------- 1 file changed, 243 insertions(+), 227 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 04f73c76ee..924b9d3ac0 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 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList */ 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 "GM": - return new int[] {4, 3, 3, 4, 2, 2}; - case "GN": - return new int[] {4, 3, 4, 2, 2, 2}; - case "GQ": - return new int[] {4, 2, 1, 4, 2, 2}; - case "GT": - return new int[] {2, 3, 2, 2, 2, 2}; + 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 "EG": + case "GH": + return new int[] {3, 3, 3, 3, 2, 2}; + case "GM": + return new int[] {4, 3, 2, 4, 2, 2}; + case "GN": + 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, 4, 3, 3, 2, 2}; + case "GT": + 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}; - case "KH": - return new int[] {2, 1, 4, 2, 2, 2}; - case "CF": - case "KI": - return new int[] {4, 2, 4, 2, 2, 2}; - case "FK": + return new int[] {0, 3, 3, 3, 4, 4}; case "KE": - case "KP": - return new int[] {3, 2, 2, 2, 2, 2}; + return new int[] {3, 2, 2, 1, 2, 2}; + case "KH": + return new int[] {1, 0, 4, 2, 2, 2}; + case "CU": + case "KI": + 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 "GR": - case "HR": - case "SI": - return new int[] {1, 0, 0, 0, 1, 2}; case "BG": - case "MT": - case "SK": + case "ES": + case "GR": + case "SI": 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 62c3236332..4816c82c74 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 70fccc7655..831bf183be 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.UnstableApi; 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 final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou @Nullable private AdErrorEvent.AdErrorListener adErrorListener; private State state; private ImmutableList companionAdSlots; + private boolean focusSkipButtonWhenAvailable; /** * Creates an instance. @@ -205,6 +207,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou this.adViewProvider = adViewProvider; companionAdSlots = ImmutableList.of(); state = new State(ImmutableMap.of()); + focusSkipButtonWhenAvailable = true; } /** @@ -274,6 +277,22 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou } } - private final ImaUtil.ServerSideAdInsertionConfiguration configuration; + private final ServerSideAdInsertionConfiguration configuration; private final Context context; private final Map mediaSourceResources; @@ -363,7 +383,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou @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 final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou StreamManagerLoadable streamManagerLoadable = new StreamManagerLoadable( sdkAdsLoader, + adsLoader.configuration, streamRequest, streamPlayer, applicationAdErrorListener, @@ -932,6 +953,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou /** 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 final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 9c24e62009..2e900e7c6d 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 @@ import java.util.Set; @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 @@ import java.util.Set; @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 | 60 +++++++++---- .../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, 206 insertions(+), 55 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4816c82c74..7ec50c0674 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 2215b07071..069534ffad 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.ListenableFuture; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; .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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } updateQueue(timeline); - // Duration might be unknown at onMediaItemTransition and become available afterward. updateMetadataIfChanged(); } @@ -1146,22 +1155,30 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; .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(); + 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 (ObjectsCompat.equals(currentMediaItemForMetadataUpdate, currentMediaItem) - && durationMsForMetadataUpdate == durationMs) { + 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; setMetadata( sessionCompat, MediaUtils.convertToMediaMetadataCompat( - currentMediaItem, durationMs, result)); + newMediaMetadata, + newMediaId, + newMediaUri, + newDurationMs, + /* artworkBitmap= */ result)); sessionImpl.onNotificationRefreshRequired(); } @@ -1210,7 +1231,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } 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 11735dd6ac..716f08a29d 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 5220083112..c7b50fa114 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 77d06b7244..71543ae0fe 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 class MediaControllerCompatCallbackWithMediaSessionTest { .setBufferedPosition(testBufferingPosition) .setPlaybackParameters(new PlaybackParameters(testSpeed)) .setTimeline(testTimeline) + .setMediaMetadata(testMediaItems.get(testItemIndex).mediaMetadata) .setPlaylistMetadata(testPlaylistMetadata) .setCurrentMediaItemIndex(testItemIndex) .setShuffleModeEnabled(testShuffleModeEnabled) @@ -370,6 +371,7 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { .setDuration(testDurationMs) .setPlaybackParameters(playbackParameters) .setTimeline(testTimeline) + .setMediaMetadata(testMediaItems.get(testItemIndex).mediaMetadata) .setPlaylistMetadata(testPlaylistMetadata) .setCurrentMediaItemIndex(testItemIndex) .setShuffleModeEnabled(testShuffleModeEnabled) @@ -979,53 +981,68 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { } @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 class MediaControllerCompatCallbackWithMediaSessionTest { .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 b735477ce7..e07afb4422 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { 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 class MediaControllerWithMediaSessionCompatTest { @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 class MediaControllerWithMediaSessionCompatTest { @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 class MediaControllerWithMediaSessionCompatTest { 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 59209c334a..9511124a5b 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 static java.util.concurrent.TimeUnit.SECONDS; 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 final class MediaUtilsTest { } 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 final class MediaUtilsTest { 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 ce0c68eb09..d48253eed7 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 class MediaSessionProviderService extends Service { }); } + @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 7d5cadfa8d..ca2840480d 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 class RemoteMediaSession { 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 b0fd6bc36a..c3acb2a83d 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * *

    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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * *

      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 7ec50c0674..cb41231c1a 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 401d73a8e6..21d07e4ee5 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 fc7144dc91..ef01b148ca 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.checkArgument; 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.ExpandableListView.OnChildClickListener; 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 class SampleChooserActivity extends AppCompatActivity 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 @@ public class SampleChooserActivity extends AppCompatActivity 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 class SampleChooserActivity extends AppCompatActivity 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 49441ef7da..ce9c90d0c2 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 f2df8effff..9b6e63be00 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 java.util.List; 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 cb41231c1a..69a5f177a3 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 831bf183be..959d873cf8 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 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou 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 6ae6968d93..27c0cc4ece 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 @@ import java.util.concurrent.TimeoutException; private int totalNotificationCount; @Nullable private MediaNotification mediaNotification; + private boolean startedInForeground; public MediaNotificationManager( MediaSessionService mediaSessionService, @@ -80,6 +81,7 @@ import java.util.concurrent.TimeoutException; startSelfIntent = new Intent(mediaSessionService, mediaSessionService.getClass()); controllerMap = new HashMap<>(); customLayoutMap = new HashMap<>(); + startedInForeground = false; } public void addSession(MediaSession session) { @@ -163,9 +165,14 @@ import java.util.concurrent.TimeoutException; } } - 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 @@ import java.util.concurrent.TimeoutException; 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 @@ import java.util.concurrent.TimeoutException; 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 @@ import java.util.concurrent.TimeoutException; 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 @@ import java.util.concurrent.TimeoutException; } } - 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 @@ import java.util.concurrent.TimeoutException; } public void onConnected() { - if (shouldShowNotification(session.getPlayer())) { - mediaSessionService.onUpdateNotification(session); + if (shouldShowNotification(session)) { + mediaSessionService.onUpdateNotificationInternal( + session, /* startInForegroundWhenPaused= */ false); } } @@ -283,7 +280,8 @@ import java.util.concurrent.TimeoutException; 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 @@ import java.util.concurrent.TimeoutException; Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_MEDIA_METADATA_CHANGED, Player.EVENT_TIMELINE_CHANGED)) { - mediaSessionService.onUpdateNotification(session); + mediaSessionService.onUpdateNotificationInternal( + session, /* startInForegroundWhenPaused= */ false); } } @@ -304,10 +303,35 @@ import java.util.concurrent.TimeoutException; 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) private static class Api24 { 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 d03f24e0bf..5fead90f84 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 class MediaSession { } /** 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 @@ public class MediaSession { * @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 4e075c18bd..ec03a8525e 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; } } - /* 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 069534ffad..3c25022e9d 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 0e8d21cca4..a93a6cf8a7 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.checkNotNull; 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.Looper; 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ 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 abstract class MediaSessionService extends Service { // 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 abstract class MediaSessionService extends Service { } MediaNotificationManager notificationManager = getMediaNotificationManager(); postOrRun(mainHandler, () -> notificationManager.removeSession(session)); - session.setListener(null); + session.clearListener(); } /** @@ -282,6 +310,22 @@ public abstract class MediaSessionService extends Service { } } + /** 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 abstract class MediaSessionService extends Service { *

        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 abstract class MediaSessionService extends Service { * @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 @@ public abstract class MediaSessionService extends Service { } } + /* 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 @@ public abstract class MediaSessionService extends Service { } } + @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 abstract class MediaSessionService extends Service { } } } + + @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 b13b4d61fb..866e92d80e 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 @@ import java.util.concurrent.ExecutionException; 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 e9855421d6..529be85dcc 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 class MediaController implements Player { @Deprecated @Override public void stop(boolean reset) { - throw new UnsupportedOperationException(); + stop(); + if (reset) { + clearMediaItems(); + } } /** @@ -1174,7 +1177,7 @@ public class MediaController implements Player { @Deprecated @Override public boolean isCurrentWindowDynamic() { - throw new UnsupportedOperationException(); + return isCurrentMediaItemDynamic(); } @Override @@ -1191,7 +1194,7 @@ public class MediaController implements Player { @Deprecated @Override public boolean isCurrentWindowLive() { - throw new UnsupportedOperationException(); + return isCurrentMediaItemLive(); } @Override @@ -1208,7 +1211,7 @@ public class MediaController implements Player { @Deprecated @Override public boolean isCurrentWindowSeekable() { - throw new UnsupportedOperationException(); + return isCurrentMediaItemSeekable(); } @Override @@ -1260,7 +1263,7 @@ public class MediaController implements Player { @Deprecated @Override public int getCurrentWindowIndex() { - throw new UnsupportedOperationException(); + return getCurrentMediaItemIndex(); } @Override @@ -1276,7 +1279,7 @@ public class MediaController implements Player { @Deprecated @Override public int getPreviousWindowIndex() { - throw new UnsupportedOperationException(); + return getPreviousMediaItemIndex(); } /** @@ -1299,7 +1302,7 @@ public class MediaController implements Player { @Deprecated @Override public int getNextWindowIndex() { - throw new UnsupportedOperationException(); + return getNextMediaItemIndex(); } /** @@ -1322,7 +1325,7 @@ public class MediaController implements Player { @Deprecated @Override public boolean hasPrevious() { - throw new UnsupportedOperationException(); + return hasPreviousMediaItem(); } /** @@ -1332,7 +1335,7 @@ public class MediaController implements Player { @Deprecated @Override public boolean hasNext() { - throw new UnsupportedOperationException(); + return hasNextMediaItem(); } /** @@ -1342,7 +1345,7 @@ public class MediaController implements Player { @Deprecated @Override public boolean hasPreviousWindow() { - throw new UnsupportedOperationException(); + return hasPreviousMediaItem(); } /** @@ -1352,7 +1355,7 @@ public class MediaController implements Player { @Deprecated @Override public boolean hasNextWindow() { - throw new UnsupportedOperationException(); + return hasNextMediaItem(); } @Override @@ -1374,7 +1377,7 @@ public class MediaController implements Player { @Deprecated @Override public void previous() { - throw new UnsupportedOperationException(); + seekToPreviousMediaItem(); } /** @@ -1384,7 +1387,7 @@ public class MediaController implements Player { @Deprecated @Override public void next() { - throw new UnsupportedOperationException(); + seekToNextMediaItem(); } /** @@ -1394,7 +1397,7 @@ public class MediaController implements Player { @Deprecated @Override public void seekToPreviousWindow() { - throw new UnsupportedOperationException(); + seekToPreviousMediaItem(); } /** @@ -1420,7 +1423,7 @@ public class MediaController implements Player { @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 e3a2ca4a33..c16a8a61d8 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.Collections; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; private ListenableFuture dispatchRemoteSessionTaskWithSessionCommandInternal( @SessionCommand.CommandCode int commandCode, - SessionCommand sessionCommand, + @Nullable SessionCommand sessionCommand, RemoteSessionTask task) { return dispatchRemoteSessionTask( sessionCommand != null @@ -339,7 +351,9 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_BACK, (iSession, seq) -> iSession.seekBack(controllerStub, seq)); + (iSession, seq) -> iSession.seekBack(controllerStub, seq)); seekToInternalByOffset(-getSeekBackIncrement()); } @@ -543,7 +517,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_FORWARD, (iSession, seq) -> iSession.seekForward(controllerStub, seq)); + (iSession, seq) -> iSession.seekForward(controllerStub, seq)); seekToInternalByOffset(getSeekForwardIncrement()); } @@ -560,7 +534,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_PLAY_PAUSE, (iSession, seq) -> iSession.setPlayWhenReady(controllerStub, seq, playWhenReady)); setPlayWhenReady( @@ -697,7 +670,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_SPEED_AND_PITCH, (iSession, seq) -> iSession.setPlaybackParameters(controllerStub, seq, playbackParameters.toBundle())); @@ -723,7 +695,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_SPEED_AND_PITCH, (iSession, seq) -> iSession.setPlaybackSpeed(controllerStub, seq, speed)); if (playerInfo.playbackParameters.speed != speed) { @@ -746,24 +717,15 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_MEDIA_ITEM, (iSession, seq) -> iSession.setMediaItem(controllerStub, seq, mediaItem.toBundle())); setMediaItemsInternal( @@ -802,7 +763,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_MEDIA_ITEM, (iSession, seq) -> iSession.setMediaItemWithStartPosition( controllerStub, seq, mediaItem.toBundle(), startPositionMs)); @@ -821,7 +781,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_MEDIA_ITEM, (iSession, seq) -> iSession.setMediaItemWithResetPosition( controllerStub, seq, mediaItem.toBundle(), resetPosition)); @@ -840,7 +799,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.setMediaItems( controllerStub, @@ -861,7 +819,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.setMediaItemsWithResetPosition( controllerStub, @@ -883,7 +840,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.setMediaItemsWithStartIndex( controllerStub, @@ -903,7 +859,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_MEDIA_ITEMS_METADATA, (iSession, seq) -> iSession.setPlaylistMetadata(controllerStub, seq, playlistMetadata.toBundle())); @@ -928,7 +883,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.addMediaItem(controllerStub, seq, mediaItem.toBundle())); addMediaItemsInternal( @@ -943,7 +897,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; checkArgument(index >= 0); dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.addMediaItemWithIndex(controllerStub, seq, index, mediaItem.toBundle())); @@ -957,7 +910,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.addMediaItems( controllerStub, @@ -975,7 +927,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; checkArgument(index >= 0); dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.addMediaItemsWithIndex( controllerStub, @@ -1045,7 +996,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.clearMediaItems(controllerStub, seq)); removeMediaItemsInternal(/* fromIndex= */ 0, /* toIndex= */ Integer.MAX_VALUE); @@ -1224,7 +1172,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; checkArgument(currentIndex >= 0 && newIndex >= 0); dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_CHANGE_MEDIA_ITEMS, (iSession, seq) -> iSession.moveMediaItem(controllerStub, seq, currentIndex, newIndex)); moveMediaItemsInternal( @@ -1239,7 +1186,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, (iSession, seq) -> iSession.seekToPreviousMediaItem(controllerStub, seq)); if (getPreviousMediaItemIndex() != C.INDEX_UNSET) { @@ -1312,7 +1257,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, (iSession, seq) -> iSession.seekToNextMediaItem(controllerStub, seq)); if (getNextMediaItemIndex() != C.INDEX_UNSET) { @@ -1327,7 +1271,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SEEK_TO_PREVIOUS, (iSession, seq) -> iSession.seekToPrevious(controllerStub, seq)); Timeline timeline = getCurrentTimeline(); @@ -1359,7 +1302,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_VOLUME, (iSession, seq) -> iSession.setVolume(controllerStub, seq, volume)); if (playerInfo.volume != volume) { @@ -1486,7 +1416,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_DEVICE_VOLUME, (iSession, seq) -> iSession.setDeviceVolume(controllerStub, seq, volume)); if (playerInfo.deviceVolume != volume) { @@ -1506,7 +1435,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_ADJUST_DEVICE_VOLUME, (iSession, seq) -> iSession.increaseDeviceVolume(controllerStub, seq)); int newDeviceVolume = playerInfo.deviceVolume + 1; @@ -1526,7 +1454,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_ADJUST_DEVICE_VOLUME, (iSession, seq) -> iSession.decreaseDeviceVolume(controllerStub, seq)); int newDeviceVolume = playerInfo.deviceVolume - 1; @@ -1546,7 +1473,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_DEVICE_VOLUME, (iSession, seq) -> iSession.setDeviceMuted(controllerStub, seq, muted)); if (playerInfo.deviceMuted != muted) { @@ -1575,7 +1501,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; } clearSurfacesAndCallbacks(); - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null); + /* surface= */ dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, null)); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } @@ -1599,7 +1526,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; @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 @@ import org.checkerframework.checker.nullness.qual.NonNull; @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 @@ import org.checkerframework.checker.nullness.qual.NonNull; } dispatchRemoteSessionTaskWithPlayerCommand( - Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, (iSession, seq) -> iSession.setTrackSelectionParameters(controllerStub, seq, parameters.toBundle())); @@ -2157,30 +2088,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; return; } videoSurface = null; - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(/* surface= */ null); + /* surface= */ dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, null)); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } @@ -3019,7 +2928,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; return; } videoSurface = new Surface(surfaceTexture); - dispatchRemoteSetVideoSurfaceTaskAndWaitForFuture(videoSurface); + dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture( + (iSession, seq) -> iSession.setVideoSurface(controllerStub, seq, videoSurface)); maybeNotifySurfaceSizeChanged(width, height); } @@ -3037,7 +2947,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 ec03a8525e..7ad0de53e3 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 3c25022e9d..e130763ea2 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_FORWARD; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; || 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } 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 a9eab3f1a3..97a85e3ffb 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.Bundle; 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 android.view.TextureView; 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.PlaybackParameters; 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 @@ import java.util.List; super.play(); } + public void playIfCommandAvailable() { + if (isCommandAvailable(COMMAND_PLAY_PAUSE)) { + play(); + } + } + @Override public void pause() { verifyApplicationThread(); @@ -145,6 +155,12 @@ import java.util.List; super.prepare(); } + public void prepareIfCommandAvailable() { + if (isCommandAvailable(COMMAND_PREPARE)) { + prepare(); + } + } + @Override public void stop() { verifyApplicationThread(); @@ -163,6 +179,18 @@ import java.util.List; 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 @@ import java.util.List; 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 @@ import java.util.List; 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 @@ import java.util.List; 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 @@ import java.util.List; 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 @@ import java.util.List; return super.getVolume(); } + public float getVolumeWithCommandCheck() { + return isCommandAvailable(COMMAND_GET_VOLUME) ? getVolume() : 0; + } + @Override public void setVolume(float volume) { verifyApplicationThread(); @@ -643,6 +700,10 @@ import java.util.List; 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 @@ import java.util.List; 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 @@ import java.util.List; 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 @@ import java.util.List; 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 @@ import java.util.List; || 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 @@ import java.util.List; } } 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 @@ import java.util.List; postOrRun( handler, () -> { + if (!isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)) { + return; + } switch (direction) { case AudioManager.ADJUST_RAISE: increaseDeviceVolume(); @@ -879,7 +1034,7 @@ import java.util.List; setDeviceMuted(false); break; case AudioManager.ADJUST_TOGGLE_MUTE: - setDeviceMuted(!isDeviceMuted()); + setDeviceMuted(!isDeviceMutedWithCommandCheck()); break; default: Log.w( @@ -898,6 +1053,9 @@ import java.util.List; *

        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 @@ import java.util.List; *

        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 @@ import java.util.List; 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 85e26f4cc3..430e14c61c 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 6fd12bc37e..504c48ed63 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 @@ package androidx.media3.session; 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 class MediaSessionCallbackWithMediaControllerCompatTest { } @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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 class MediaSessionCallbackWithMediaControllerCompatTest { } @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 class MediaSessionCallbackWithMediaControllerCompatTest { } @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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 a6a1f2327f..245f944972 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 class MediaSessionKeyEventTest { 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 8d82012e32..d19743b6ee 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 static java.util.concurrent.TimeUnit.MILLISECONDS; 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 class MediaSessionPermissionTest { 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 c16a8a61d8..8cca5c4d87 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; @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 866e92d80e..aa51cb519c 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 @@ import java.util.concurrent.ExecutionException; 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 d19743b6ee..38492052ef 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 class MediaSessionPermissionTest { @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 959d873cf8..e029b74578 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 final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou /** * 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 16ca1a25a5..b5ba86ab22 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 727772e190..0add882c72 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 97a85e3ffb..bf5f2756c9 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 @@ import java.util.List; 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 @@ import java.util.List; 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 @@ import java.util.List; 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 0000000000..7f50a47455 --- /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 461dbd2dd0..f5aa0dca5d 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 @@ public class PlayerControlView extends FrameLayout { ((ImageView) playPauseButton) .setImageDrawable(getDrawable(getContext(), resources, drawableRes)); playPauseButton.setContentDescription(resources.getString(stringRes)); + + boolean enablePlayPause = shouldEnablePlayPauseButton(); + updateButton(enablePlayPause, playPauseButton); } } @@ -1497,6 +1500,10 @@ public class PlayerControlView extends FrameLayout { } } + 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 8e37968a0c..1d7706f907 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 abstract class Timeline implements Bundleable { 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 abstract class Timeline implements Bundleable { 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 abstract class Timeline implements Bundleable { *

        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 abstract class Timeline implements Bundleable { 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 111652b38b..716e16ec3c 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 class TimelineTest { 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 113848eda0..c681ab4420 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 @@ import androidx.media3.common.util.Util; 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 5fead90f84..d6bcc84d3c 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 @@ public class MediaSession { 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 7ad0de53e3..11b6f37b15 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; // 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 aa51cb519c..1cbb8106d5 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 @@ import java.util.concurrent.ExecutionException; 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 716f08a29d..0d61696904 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * 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 dfb94e8a3d..f4bd254eed 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 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; // 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 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; 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 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; 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 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; @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 9bd0503ab0..b8ff74a679 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 @@ public class MatroskaExtractor implements Extractor { } /** - * 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 @@ public class MatroskaExtractor implements Extractor { 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 c9e7d4d360..17183a7f96 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 interface Player { @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 interface Player { @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 @@ public interface Player { *

        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 @@ public interface Player { *

        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 d6bcc84d3c..7f09c4280b 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 @@ public class MediaSession { 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 11b6f37b15..9e7f201d79 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; } } + 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; } } 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 e130763ea2..d49b5b1666 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @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 1cbb8106d5..3a36b80368 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 @@ import java.util.concurrent.ExecutionException; @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 f4bd254eed..56207efa9d 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 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; 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 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; 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 bf5f2756c9..42c391f9c4 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 @@ import java.util.List; *

        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 @@ import java.util.List; *

        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 f8960d2a87..a4d537f0cc 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 @@ import com.google.common.base.Objects; @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 @@ import com.google.common.base.Objects; 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 @@ import com.google.common.base.Objects; 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 7e8738f9d9..fdc4e2e4c0 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 class PlayerInfoTest { 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 17183a7f96..9015699e32 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 @@ public interface Player { 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 @@ public interface Player { *
      • {@link #getPreviousMediaItemIndex()} *
      • {@link #hasPreviousMediaItem()} *
      • {@link #hasNextMediaItem()} - *
      • {@link #getCurrentAdGroupIndex()} - *
      • {@link #getCurrentAdIndexInAdGroup()} *
      */ int COMMAND_GET_TIMELINE = 17; @@ -2692,18 +2708,27 @@ public interface Player { /** * 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 @@ public interface Player { /** * 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 @@ public interface Player { * 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 @@ public interface Player { * 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 @@ public interface Player { * *

      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 @@ public interface Player { * 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 @@ public interface Player { * 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 @@ public interface Player { * 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 @@ public interface Player { * 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 @@ public interface Player { * 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 1d7706f907..d470b37b52 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 abstract class Timeline implements Bundleable { 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 9e7f201d79..180030adf1 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; 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 3a36b80368..6ae74ccbbf 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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; return; } queueSessionTaskWithPlayerCommand( - caller, sequenceNumber, COMMAND_STOP, sendSessionResultSuccess(Player::stop)); + caller, sequenceNumber, COMMAND_STOP, sendSessionResultSuccess(player -> player.stop())); } @Override @@ -655,7 +675,7 @@ import java.util.concurrent.ExecutionException; caller, sequenceNumber, COMMAND_SEEK_TO_DEFAULT_POSITION, - sendSessionResultSuccess(Player::seekToDefaultPosition)); + sendSessionResultSuccess(player -> player.seekToDefaultPosition())); } @Override @@ -668,7 +688,10 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), - Player::setMediaItems))); + (playerWrapper, controller, mediaItems) -> + playerWrapper.setMediaItems(mediaItems)))); } @Override @@ -870,7 +897,7 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList), - Player::setMediaItems))); + (playerWrapper, controller, mediaItems) -> + playerWrapper.setMediaItems(mediaItems)))); } @Override @@ -956,7 +985,8 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList), - (player, mediaItems) -> + (player, controller, mediaItems) -> player.setMediaItems(mediaItems, startIndex, startPositionMs)))); } @@ -1033,7 +1063,8 @@ import java.util.concurrent.ExecutionException; handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), - Player::addMediaItems))); + (playerWrapper, controller, mediaItems) -> + playerWrapper.addMediaItems(mediaItems)))); } @Override @@ -1057,7 +1088,9 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; handleMediaItemsWhenReady( (sessionImpl, controller, sequenceNum) -> sessionImpl.onAddMediaItemsOnHandler(controller, mediaItems), - Player::addMediaItems))); + (playerWrapper, controller, items) -> playerWrapper.addMediaItems(items)))); } @Override @@ -1114,7 +1147,9 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; } 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 56207efa9d..ddc212cd53 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 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; 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 42c391f9c4..c4922f4b4f 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 @@ package androidx.media3.session; 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 @@ import java.util.List; } 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 @@ import java.util.List; 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 fdc4e2e4c0..32ea6e18a5 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 class PlayerInfoTest { /* 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 class PlayerInfoTest { 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 13f7d64d4e..0beb95bef4 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 class MediaControllerListenerTest { } @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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { @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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { 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 class MediaControllerListenerTest { 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 076643c2a2..2098f6ca29 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.media3.test.session.common.TestUtils; 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 class MediaSessionPlayerTest { 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 class MediaSessionPlayerTest { 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 class MediaSessionPlayerTest { 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 class MediaSessionPlayerTest { 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 class MediaSessionPlayerTest { 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 class MediaSessionPlayerTest { 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 23e30bf4b8..4d33c9efe3 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 class MockPlayer implements Player { @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 class MockPlayer implements Player { @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 class MockPlayer implements Player { @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 8cca5c4d87..8f224cb1ee 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; @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 @@ import org.checkerframework.checker.nullness.qual.NonNull; ? null : new SessionServiceConnection(connectionHints); flushCommandQueueHandler = new FlushCommandQueueHandler(applicationLooper); - lastReturnedCurrentPositionMs = C.TIME_UNSET; + currentPositionMs = C.TIME_UNSET; lastSetPlayWhenReadyCalledTimeMs = C.TIME_UNSET; } @@ -582,32 +582,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; @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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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 a7b5dbfc6c..0bf910b686 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 class MediaControllerTest { 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 c3acb2a83d..6b2368df7d 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 class DefaultMediaNotificationProvider implements MediaNotification.Provi 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 fe7616bce3..e696979c6c 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_C 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 class DefaultMediaNotificationProviderTest { 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 @@ public class DefaultMediaNotificationProviderTest { } 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 9015699e32..a33775d522 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 @@ public interface Player { } /** - * 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 @@ public interface Player { *

      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 @@ public interface Player { 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 @@ public interface Player { * 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 @@ public interface Player { @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 @@ public interface Player { 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 @@ public interface Player { 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 @@ public interface Player { 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 @@ public interface Player { 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 @@ public interface Player { 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 @@ public interface Player { 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 @@ public interface Player { 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 @@ public interface Player { 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 @@ public interface Player { /** * 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 69a5f177a3..47170725be 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 6b2368df7d..b6c487fcd2 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.FutureCallback; 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 class DefaultMediaNotificationProvider implements MediaNotification.Provi addNotificationActions( mediaSession, getMediaButtons( + mediaSession, player.getAvailableCommands(), customLayout, /* showPauseButton= */ player.getPlayWhenReady() @@ -418,6 +418,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi * 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 class DefaultMediaNotificationProvider implements MediaNotification.Provi * 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 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi 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 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi */ 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 e696979c6c..cd159ea0be 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 class DefaultMediaNotificationProviderTest { 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 class DefaultMediaNotificationProviderTest { 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 class DefaultMediaNotificationProviderTest { 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 class DefaultMediaNotificationProviderTest { 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 class DefaultMediaNotificationProviderTest { 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 class DefaultMediaNotificationProviderTest { 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 47170725be..5ceebf14d9 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 7f09c4280b..9037f9aae8 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.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; +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 @@ public class MediaSession { /** * 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 @@ public class MediaSession { * 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 @@ public class MediaSession { 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 180030adf1..9d32853015 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.Log; 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 @@ import org.checkerframework.checker.initialization.qual.Initialized; "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 d49b5b1666..0c467ac81e 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.Log; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 6ae74ccbbf..ffd6e78621 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.annotation.Nullable; 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.LibraryParams; 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 @@ import java.util.concurrent.ExecutionException; }; } + 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 @@ import java.util.concurrent.ExecutionException; @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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; @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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; 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 0d61696904..6f3233b520 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; : 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 1891ee001d..37ea204459 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 static java.util.concurrent.TimeUnit.MILLISECONDS; 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 class MediaSessionCallbackTest { 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 class MediaSessionCallbackTest { 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 class MediaSessionCallbackTest { } @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 class MediaSessionCallbackTest { 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 504c48ed63..fcdbe1ec5a 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 class MediaSessionCallbackWithMediaControllerCompatTest { } @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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 class MediaSessionCallbackWithMediaControllerCompatTest { } @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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 class MediaSessionCallbackWithMediaControllerCompatTest { } @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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 class MediaSessionCallbackWithMediaControllerCompatTest { } @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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 class MediaSessionCallbackWithMediaControllerCompatTest { } @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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 class MediaSessionCallbackWithMediaControllerCompatTest { } @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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 class MediaSessionCallbackWithMediaControllerCompatTest { 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 2098f6ca29..6ebf428658 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 class MediaSessionPlayerTest { 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 class MediaSessionPlayerTest { 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 class MediaSessionPlayerTest { 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 class MediaSessionPlayerTest { 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 class MediaSessionPlayerTest { 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 class MediaSessionPlayerTest { // 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 class MediaSessionPlayerTest { 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 9037f9aae8..f3045d3cda 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 @@ public class MediaSession { * 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 @@ public class MediaSession { * @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 class MediaSession { /** * 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 0c467ac81e..1e9caba13b 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 ffd6e78621..3c0b27a775 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 @@ import java.util.concurrent.ExecutionException; () -> { if (!sessionImpl.isReleased()) { mediaItemPlayerTask.run( - sessionImpl.getPlayerWrapper(), controller, mediaItemsWithStartPosition); + sessionImpl.getPlayerWrapper(), mediaItemsWithStartPosition); } }, new SessionResult(SessionResult.RESULT_SUCCESS))); @@ -909,28 +909,7 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; 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 @@ import java.util.concurrent.ExecutionException; (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 @@ import java.util.concurrent.ExecutionException; } 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 6f3233b520..fc983aa537 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; : 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); + 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(); - } - } - - 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(); + 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 2e900e7c6d..bd19af60f2 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 @@ import java.util.Set; } /** 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 489997beb8..fc58fbcbc5 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.SurfaceView; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // 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 fc983aa537..95f3e3bbf5 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 android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_Q 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 android.util.Pair; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } /** - * 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 fe90b5bd04..5a6e05df29 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 @@ public class TestUtils { 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 9511124a5b..9b0864857a 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 @@ 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.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.support.v4.media.session.PlaybackStateCompat; 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 final class MediaUtilsTest { } @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(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)) + .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_whenSessionIsNotReady_disallowsShuffle() { - long sessionFlags = FLAG_HANDLES_QUEUE_COMMANDS; + public void convertToPlayerCommands_withJustPlayAction_playPauseCommandNotAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder().setActions(PlaybackStateCompat.ACTION_PLAY).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= */ 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( + 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_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( + 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 6b2b0fcd76..f7c9b01f5f 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 f7c9b01f5f..4b93f3806b 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 5c83902d04..85b5905d67 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 final class DefaultMediaDescriptionAdapter implements MediaDescriptionAda @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 final class DefaultMediaDescriptionAdapter implements MediaDescriptionAda @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 final class DefaultMediaDescriptionAdapter implements MediaDescriptionAda @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 f5aa0dca5d..ec24bba35d 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.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 static androidx.media3.common.util.Util.msToUs; import android.annotation.SuppressLint; import android.content.Context; @@ -798,7 +810,7 @@ public class PlayerControlView extends FrameLayout { */ 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 @@ public class PlayerControlView extends FrameLayout { } @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 @@ public class PlayerControlView extends FrameLayout { @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 @@ public class PlayerControlView extends FrameLayout { 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 @@ public class PlayerControlView extends FrameLayout { 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 @@ public class PlayerControlView extends FrameLayout { } 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 @@ public class PlayerControlView extends FrameLayout { @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 @@ public class PlayerControlView extends FrameLayout { } 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 @@ public class PlayerControlView extends FrameLayout { } 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 @@ public class PlayerControlView extends FrameLayout { 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 class PlayerControlView extends FrameLayout { } 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 class PlayerControlView extends FrameLayout { 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 @@ public class PlayerControlView extends FrameLayout { } 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 @@ public class PlayerControlView extends FrameLayout { 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 @@ public class PlayerControlView extends FrameLayout { } /** - * 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 class PlayerControlView extends FrameLayout { } 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 class PlayerControlView extends FrameLayout { 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 class PlayerControlView extends FrameLayout { 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 class PlayerControlView extends FrameLayout { 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 30e610ed14..f420489d61 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 @@ public class PlayerNotificationManager { @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 @@ public class PlayerNotificationManager { // 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 class PlayerNotificationManager { } 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 b91d14cb5f..702ec7efdb 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 class PlayerView extends FrameLayout implements AdViewProvider { @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 class PlayerView extends FrameLayout implements AdViewProvider { @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 @@ public class PlayerView extends FrameLayout implements AdViewProvider { } 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 @@ public class PlayerView extends FrameLayout implements AdViewProvider { } 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 @@ public class PlayerView extends FrameLayout implements AdViewProvider { 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 @@ public class PlayerView extends FrameLayout implements AdViewProvider { } @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 class PlayerView extends FrameLayout implements AdViewProvider { // 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 6cacef3ceb..9455b89504 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 final class 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 final class 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 895fcd22d8..3f295789b0 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 @@ import org.junit.runner.RunWith; 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 class DefaultMediaDescriptionAdapterTest { 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 class DefaultMediaDescriptionAdapterTest { 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 ec24bba35d..d5168952d9 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 @@ public class PlayerControlView extends FrameLayout { 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 @@ public class PlayerControlView extends FrameLayout { private void updateTrackLists() { initTrackSelectionAdapter(); updateButton(textTrackSelectionAdapter.getItemCount() > 0, subtitleButton); + updateSettingsButton(); } private void initTrackSelectionAdapter() { @@ -1301,6 +1305,11 @@ public class PlayerControlView extends FrameLayout { 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 @@ public class PlayerControlView extends FrameLayout { } 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 @@ public class PlayerControlView extends FrameLayout { * @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 @@ public class PlayerControlView extends FrameLayout { @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 class PlayerControlView extends FrameLayout { 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 class PlayerControlView extends FrameLayout { @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 class PlayerControlView extends FrameLayout { 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 c71b5cffd0..1cf6a3734f 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 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 740e468c23..2ef9252c64 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 final class MergingMediaPeriodTest { 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 5ceebf14d9..cdd6711554 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 1e9caba13b..b7aa79e8cf 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Handler handler) { sessionImpl = session; Context context = sessionImpl.getContext(); + appPackageName = context.getPackageName(); sessionManager = MediaSessionManager.getSessionManager(context); controllerLegacyCbForBroadcast = new ControllerLegacyCbForBroadcast(); connectionTimeoutHandler = @@ -225,7 +227,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 245f944972..a4733f6449 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 class MediaSessionKeyEventTest { 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 cdd6711554..5d021ece39 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 800c601059..4ccca487c8 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 class FragmentedMp4Extractor implements Extractor { } /** - * 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 0f902e441a..4bcb712c34 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 d747be40c5..c03c03e6c0 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 027e7eb633..e33b92c7bc 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 db94e2636e..a079fe334e 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 b5ba86ab22..192499d4e1 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 | 215 ++++++++------ .../common/IRemoteMediaSessionCompat.aidl | 1 + ...aControllerWithMediaSessionCompatTest.java | 263 ++++++++++++++++++ .../MediaSessionCompatProviderService.java | 57 +++- .../session/RemoteMediaSessionCompat.java | 4 + 7 files changed, 466 insertions(+), 82 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5d021ece39..9734daf577 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 fc58fbcbc5..cd0a0a1fca 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; + " 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (hasMediaMetadataCompat) { MediaItem mediaItem = MediaUtils.convertToMediaItem( - currentTimeline.getMediaItemAt(currentMediaItemIndex).mediaId, + checkNotNull(currentTimeline.getMediaItemAt(currentMediaItemIndex)).mediaId, newLegacyPlayerInfo.mediaMetadataCompat, ratingType); currentTimeline = @@ -1999,7 +2001,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 adaf65d707..a7dc94c511 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.base.Objects; 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,66 +50,29 @@ import java.util.Map; 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; - private QueueTimeline( - ImmutableList mediaItems, - Map unmodifiableMediaItemToQueueIdMap, - @Nullable MediaItem fakeMediaItem) { - this.mediaItems = mediaItems; - this.unmodifiableMediaItemToQueueIdMap = unmodifiableMediaItemToQueueIdMap; - this.fakeMediaItem = fakeMediaItem; - } - + /** Creates a new instance. */ public QueueTimeline(QueueTimeline queueTimeline) { this.mediaItems = queueTimeline.mediaItems; - this.unmodifiableMediaItemToQueueIdMap = queueTimeline.unmodifiableMediaItemToQueueIdMap; + this.mediaItemToQueueIdMap = queueTimeline.mediaItemToQueueIdMap; this.fakeMediaItem = queueTimeline.fakeMediaItem; } - public QueueTimeline copyWithFakeMediaItem(@Nullable MediaItem fakeMediaItem) { - return new QueueTimeline(mediaItems, unmodifiableMediaItemToQueueIdMap, fakeMediaItem); - } - - public QueueTimeline copyWithNewMediaItem(int replaceIndex, MediaItem newMediaItem) { - ImmutableList.Builder newMediaItemsBuilder = new ImmutableList.Builder<>(); - newMediaItemsBuilder.addAll(mediaItems.subList(0, replaceIndex)); - newMediaItemsBuilder.add(newMediaItem); - newMediaItemsBuilder.addAll(mediaItems.subList(replaceIndex + 1, mediaItems.size())); - return new QueueTimeline( - newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem); - } - - 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); - } - - 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); - } - - 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, - fakeMediaItem); + private QueueTimeline( + ImmutableList mediaItems, + ImmutableMap mediaItemToQueueIdMap, + @Nullable MediaItem fakeMediaItem) { + this.mediaItems = mediaItems; + this.mediaItemToQueueIdMap = mediaItemToQueueIdMap; + this.fakeMediaItem = 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<>(); - IdentityHashMap mediaItemToQueueIdMap = new IdentityHashMap<>(); + ImmutableMap.Builder mediaItemToQueueIdMap = new ImmutableMap.Builder<>(); for (int i = 0; i < queue.size(); i++) { QueueItem queueItem = queue.get(i); MediaItem mediaItem = MediaUtils.convertToMediaItem(queueItem); @@ -115,20 +80,122 @@ import java.util.Map; mediaItemToQueueIdMap.put(mediaItem, queueItem.getQueueId()); } return new QueueTimeline( - mediaItemsBuilder.build(), - Collections.unmodifiableMap(mediaItemToQueueIdMap), - /* fakeMediaItem= */ null); + 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) { - @Nullable MediaItem mediaItem = mediaItems.get(mediaItemIndex); - if (mediaItem == null) { - return QueueItem.UNKNOWN_ID; - } - Long queueId = unmodifiableMediaItemToQueueIdMap.get(mediaItem); + 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, 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(), 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(), 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(), 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(), + mediaItemToQueueIdMap, + fakeMediaItem); + } + + /** + * 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(); + } + int mediaItemIndex = mediaItems.indexOf(mediaItem); + return mediaItemIndex == -1 ? C.INDEX_UNSET : mediaItemIndex; + } + @Nullable public MediaItem getMediaItemAt(int mediaItemIndex) { if (mediaItemIndex >= 0 && mediaItemIndex < mediaItems.size()) { @@ -137,14 +204,6 @@ import java.util.Map; 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 @@ import java.util.Map; 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 3fe24ac8b9..196306d789 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 e07afb4422..748fbf9fd4 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 class MediaControllerWithMediaSessionCompatTest { 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 3fac9431e1..91346dffa6 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 @@ import java.util.concurrent.Executor; @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 class MediaSessionCompatProviderService extends Service { () -> { 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 class MediaSessionCompatProviderService extends Service { } @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 da94920c59..887d939728 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 class RemoteMediaSessionCompat { 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 51bc101b0d..ff675b5e54 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 final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab } @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 | 16 +++-- .../test/assets/media/mpd/sample_mpd_images | 5 +- 7 files changed, 143 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9734daf577..a85995fac9 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 450585d1f1..b34bead66e 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 @@ import java.util.UUID; *

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

          Fields relevant to image formats

          + * + *
            + *
          • {@link #tileCountHorizontal} + *
          • {@link #tileCountVertical} + *
          */ public final class Format implements Bundleable { @@ -165,6 +172,11 @@ public final class Format implements Bundleable { 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 final class Format implements Bundleable { 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 @@ public final class Format implements Bundleable { 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 final class Format implements Bundleable { 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 final class Format implements Bundleable { /** 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 @@ public final class Format implements Bundleable { 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 final class Format implements Bundleable { 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 final class Format implements Bundleable { && 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 final class Format implements Bundleable { 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 final class Format implements Bundleable { 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 @@ public final class Format implements Bundleable { // 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 ab656935ff..e15d6fb5d1 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 @@ public final class FormatTest { .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 d9603d742f..00a96572a8 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 @@ public final class DashMediaSource extends BaseMediaSource { 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 @@ public final class DashMediaSource extends BaseMediaSource { 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 c5006ad7b7..0e0bb927b9 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 @@ public class DashManifestParser extends DefaultHandler ? 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 @@ public class DashManifestParser extends DefaultHandler roleFlags |= parseRoleFlagsFromAccessibilityDescriptors(accessibilityDescriptors); roleFlags |= parseRoleFlagsFromProperties(essentialProperties); roleFlags |= parseRoleFlagsFromProperties(supplementalProperties); + @Nullable Pair tileCounts = parseTileCountFromProperties(essentialProperties); Format.Builder formatBuilder = new Format.Builder() @@ -820,7 +823,9 @@ public class DashManifestParser extends DefaultHandler .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 @@ public class DashManifestParser extends DefaultHandler 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 04d53b1841..29510717d7 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 class DashManifestParserTest { ApplicationProvider.getApplicationContext(), SAMPLE_MPD_IMAGES)); AdaptationSet adaptationSet = manifest.getPeriod(0).adaptationSets.get(0); - Format format = adaptationSet.representations.get(0).format; + Format format0 = adaptationSet.representations.get(0).format; + Format format1 = adaptationSet.representations.get(1).format; - assertThat(format.sampleMimeType).isEqualTo("image/jpeg"); - assertThat(format.width).isEqualTo(320); - assertThat(format.height).isEqualTo(180); + 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 981a29a23a..7d0779e957 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 a85995fac9..f2d0c6587e 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 0000000000..7aacffc6f7 --- /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 0000000000..14d4e94306 --- /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 b7aa79e8cf..13cf696db0 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private volatile long connectionTimeoutMs; @Nullable private FutureCallback pendingBitmapLoadCallback; + private int sessionFlags; public MediaSessionLegacyStub( MediaSessionImpl session, @@ -161,8 +162,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; sessionCompat.setSessionActivity(sessionActivity); } - sessionCompat.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS); - @SuppressWarnings("nullness:assignment") @Initialized MediaSessionLegacyStub thisRef = this; @@ -254,6 +253,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; 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 7f50a47455..a70c8abb40 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 class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest 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 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest 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 class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest } }; } + + 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 f2d0c6587e..44c3c864f3 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 44c3c864f3..32b921e94b 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 32b921e94b..38c631388e 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 38ed1ba728..005d4e2e68 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 9f50f679ba..d0b375b92d 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 ac9b80f1d6..39884d57c7 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 603392d369..ad013f4cd3 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;