From d7234a6a4e6a52e1e2f1016ab29e6326188013cb Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Fri, 11 Apr 2025 09:32:59 -0700 Subject: [PATCH] Fix issue where ProgressiveMediaPeriod fails assertPrepared In `PreloadMediaSource` we accesses the `MediaPeriod.getBufferedPositionUs()` when we receive `onContinueLoadingRequested()` from the period. For `ProgressiveMediaPeriod` which requires loading a portion of media file to get it prepared, there is a chance that it needs more than one round of `onContinueLoadingRequested()` -> `continueLoading()` to complete preparation, for example, when the `setContinueLoadingIntervalBytes()` is small enough to not include the full information for preparation. Thus we should avoid `MediaPeriod.getBufferedPositionUs()` being called before period is prepared, as it will fail at `assertPrepared`. Issue: androidx/media#2315 PiperOrigin-RevId: 746490375 (cherry picked from commit 344f249511ad67a42d8328e79102f02bbbec5201) --- RELEASENOTES.md | 127 +----------------- .../source/preload/PreloadMediaSource.java | 23 ++-- .../preload/PreloadMediaSourceTest.java | 115 ++++------------ 3 files changed, 41 insertions(+), 224 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 009e88b361..1b48ecf25c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,131 +1,14 @@ # Release notes +* ExoPlayer: + * Fix issue where `ProgressiveMediaPeriod` throws an + `IllegalStateException` as `PreloadMediaSource` attempts to call its + `getBufferedDurationUs()` before it is prepared + ([#2315](https://github.com/androidx/media/issues/2315)). * Cast extension: * Add support for playlist metadata ([#2235](https://github.com/androidx/media/pull/2235)). -### Unreleased changes - -* Common Library: - * Add `PlaybackParameters.withPitch(float)` method for easily copying a - `PlaybackParameters` with a new `pitch` value - ([#2257](https://github.com/androidx/media/issues/2257)). -* ExoPlayer: - * Fix sending `CmcdData` in manifest requests for DASH, HLS, and - SmoothStreaming ([#2253](https://github.com/androidx/media/pull/2253)). -<<<<<<< HEAD -======= - * Fix issue where media item transition fails due to recoverable renderer - error during initialization of the next media item - ([#2229](https://github.com/androidx/media/issues/2229)). - * Add `ExoPlayer.setScrubbingModeEnabled(boolean)` method. This optimizes - the player for many frequent seeks (for example, from a user dragging a - scrubber bar around). - * `AdPlaybackState.withAdDurationsUs(long[][])` can be used after ad - groups have been removed. The user still needs to pass in an array of - durations for removed ad groups which can be empty or null - ([#2267](https://github.com/androidx/media/issues/2267)). ->>>>>>> d133300627 (Make AdPlaybackState.withAdDurationsUs work with removed ad groups) -* Transformer: -* Track Selection: -* Extractors: - * MP3: Use duration and data size from unseekable Xing, VBRI and similar - variable bitrate metadata when falling back to constant bitrate seeking - due to `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING(_ALWAYS)` - ([#2194](https://github.com/androidx/media/issues/2194)). - * MP4: Parse `alternate_group` from the `tkhd` box and expose it as an - `Mp4AlternateGroupData` entry in each track's `Format.metadata` - ([#2242](https://github.com/androidx/media/issues/2242)). -* DataSource: -* Audio: - * Allow constant power upmixing/downmixing in DefaultAudioMixer. - * Fix offload issue where position might get stuck when playing a playlist - of short content - ([#1920](https://github.com/androidx/media/issues/1920)). -* Video: - * Add experimental `ExoPlayer` API to include the - `MediaCodec.BUFFER_FLAG_DECODE_ONLY` flag when queuing decode-only input - buffers. This flag will signal the decoder to skip the decode-only - buffers thereby resulting in faster seeking. Enable it with - `DefaultRenderersFactory.experimentalSetEnableMediaCodecBufferDecodeOnlyFlag`. -* Text: -* Metadata: -* Image: -* DataSource: -* DRM: -* Effect: - * Add `Presentation.createForShortSide(int)` that creates a `Presentation` - that ensures the shortest side always matches the given value, - regardless of input orientation. -* Muxers: - * `writeSampleData()` API now uses muxer specific `BufferInfo` class - instead of `MediaCodec.BufferInfo`. -* IMA extension: -* Session: - * Lower aggregation timeout for platform `MediaSession` callbacks from 500 - to 100 milliseconds and add an experimental setter to allow apps to - configure this value. - * Fix issue where notifications reappear after they have been dismissed by - the user ([#2302](https://github.com/androidx/media/issues/2302)). - * Fix a bug where the `PlayerWrapper` returned a single-item timeline when - the wrapped player is actually empty. This happened when the wrapped - player doesn't have `COMMAND_GET_TIMELINE` available while - `COMMAND_GET_CURRENT_MEDIA_ITEM` is available and the wrapped player is - empty ([#2320](https://github.com/androidx/media/issues/2320)). - * Fix a bug where calling - `MediaSessionService.setMediaNotificationProvider` is silently ignored - after other interactions with the service like - `setForegroundServiceTimeoutMs` - ([#2305](https://github.com/androidx/media/issues/2305)). -* UI: - * Enable `PlayerSurface` to work with `ExoPlayer.setVideoEffects` and - `CompositionPlayer`. - * Fix bug where `PlayerSurface` can't be recomposed with a new `Player`. -* Downloads: - * Add partial download support for progressive streams. Apps can prepare a - progressive stream with `DownloadHelper`, and request a - `DownloadRequest` from the helper with specifying the time-based media - start and end positions that the download should cover. The returned - `DownloadRequest` carries the resolved byte range, with which a - `ProgressiveDownloader` can be created and download the content - correspondingly. -* OkHttp extension: -* Cronet extension: -* RTMP extension: -* HLS extension: - * Fix issue where chunk duration wasn't set in `CmcdData` for HLS media, - causing an assertion failure when processing encrypted media segments - ([#2312](https://github.com/androidx/media/issues/2312)). -* DASH extension: -* Smooth Streaming extension: -* RTSP extension: - * Add support for URI with RTSPT scheme as a way to configure the RTSP - session to use TCP - ([#1484](https://github.com/androidx/media/issues/1484)). -* Decoder extensions (FFmpeg, VP9, AV1, etc.): -* MIDI extension: -* Leanback extension: -* Cast extension: -* Test Utilities: -* Demo app: - * Add `PlaybackSpeedPopUpButton` Composable UI element to be part of - `ExtraControls` in `demo-compose`. -* Remove deprecated symbols: - * Removed deprecated `SegmentDownloader` constructor - `SegmentDownloader(MediaItem, Parser, CacheDataSource.Factory, - Executor)` and the corresponding constructors in its subclasses - `DashDownloader`, `HlsDownloader` and `SsDownloader`. - * Removed deprecated `Player.hasNext()`, `Player.hasNextWindow()`. Use - `Player.hasNextMediaItem()` instead. - * Removed deprecated `Player.next()`. Use `Player.seekToNextMediaItem()` - instead. - * Removed deprecated `Player.seekToPreviousWindow()`. Use - `Player.seekToPreviousMediaItem()` instead. - * Removed deprecated `Player.seekToNextWindow()`. Use - `Player.seekToNextMediaItem()` instead. - * Removed deprecated `BaseAudioProcessor` in `exoplayer` module. Use - `BaseAudioProcessor` under `common` module. - ## 1.6 ### 1.6.0 (2025-03-26) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java index fc3d3870ca..37efbb82ee 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java @@ -505,17 +505,18 @@ public final class PreloadMediaSource extends WrappingMediaSource { return; } PreloadMediaPeriod preloadMediaPeriod = (PreloadMediaPeriod) mediaPeriod; - long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); - if (prepared && bufferedPositionUs == C.TIME_END_OF_SOURCE) { - preloadControl.onLoadedToTheEndOfSource(PreloadMediaSource.this); - stopPreloading(); - return; - } - if (prepared - && !preloadControl.onContinueLoadingRequested( - PreloadMediaSource.this, bufferedPositionUs - periodStartPositionUs)) { - stopPreloading(); - return; + if (prepared) { + long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); + if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { + preloadControl.onLoadedToTheEndOfSource(PreloadMediaSource.this); + stopPreloading(); + return; + } + if (!preloadControl.onContinueLoadingRequested( + PreloadMediaSource.this, bufferedPositionUs - periodStartPositionUs)) { + stopPreloading(); + return; + } } preloadMediaPeriod.continueLoading( new LoadingInfo.Builder().setPlaybackPositionUs(periodStartPositionUs).build()); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java index 5e6db67123..639f7844f1 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java @@ -90,12 +90,13 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class PreloadMediaSourceTest { - private static final int LOADING_CHECK_INTERVAL_BYTES = 10 * 1024; + private static final int LOADING_CHECK_INTERVAL_BYTES = 32; private static final int TARGET_PRELOAD_DURATION_US = 10000; private Allocator allocator; private BandwidthMeter bandwidthMeter; private RenderersFactory renderersFactory; + private MediaItem mediaItem; @Before public void setUp() { @@ -112,6 +113,10 @@ public final class PreloadMediaSourceTest { SystemClock.DEFAULT.createHandler(handler.getLooper(), /* callback= */ null), audioListener) }; + mediaItem = + new MediaItem.Builder() + .setUri(Uri.parse("asset://android_asset/media/mp4/long_1080p_lowbitrate.mp4")) + .build(); } @Test @@ -146,11 +151,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); runMainLooperUntil(() -> preloadMediaSourceReference.get() != null); @@ -191,11 +192,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); runMainLooperUntil(() -> preloadMediaSourceReference.get() != null); @@ -235,11 +232,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); shadowOf(Looper.getMainLooper()).idle(); @@ -266,11 +259,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); AtomicReference externalCallerMediaSourceReference = new AtomicReference<>(); MediaSource.MediaSourceCaller externalCaller = @@ -315,11 +304,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); runMainLooperUntil(() -> preloadMediaSourceReference.get() != null); @@ -388,11 +373,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); runMainLooperUntil(() -> preloadExceptionReference.get() != null); @@ -472,11 +453,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); runMainLooperUntil(() -> preloadExceptionReference.get() != null); @@ -583,11 +560,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); runMainLooperUntil(() -> preloadExceptionReference.get() != null); @@ -615,11 +588,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); FakeMediaSource wrappedMediaSource = mediaSourceFactory.getLastCreatedSource(); wrappedMediaSource.setAllowPreparation(false); preloadMediaSource.preload(/* startPositionUs= */ 0L); @@ -653,11 +622,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); shadowOf(Looper.getMainLooper()).idle(); @@ -727,11 +692,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); shadowOf(Looper.getMainLooper()).idle(); @@ -808,11 +769,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); shadowOf(Looper.getMainLooper()).idle(); @@ -876,11 +833,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); shadowOf(Looper.getMainLooper()).idle(); @@ -923,11 +876,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); AtomicBoolean externalCallerSourceInfoRefreshedCalled = new AtomicBoolean(); MediaSource.MediaSourceCaller externalCaller = (source, timeline) -> externalCallerSourceInfoRefreshedCalled.set(true); @@ -976,11 +925,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); AtomicBoolean externalCallerSourceInfoRefreshedCalled = new AtomicBoolean(); MediaSource.MediaSourceCaller externalCaller = (source, timeline) -> externalCallerSourceInfoRefreshedCalled.set(true); @@ -1031,11 +976,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); AtomicBoolean externalCaller1SourceInfoRefreshedCalled = new AtomicBoolean(); AtomicBoolean externalCaller2SourceInfoRefreshedCalled = new AtomicBoolean(); MediaSource.MediaSourceCaller externalCaller1 = @@ -1090,11 +1031,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); preloadMediaSource.preload(/* startPositionUs= */ 0L); shadowOf(Looper.getMainLooper()).idle(); preloadMediaSource.releasePreloadMediaSource(); @@ -1140,11 +1077,7 @@ public final class PreloadMediaSourceTest { getRendererCapabilities(renderersFactory), allocator, Util.getCurrentOrMainLooper()); - PreloadMediaSource preloadMediaSource = - preloadMediaSourceFactory.createMediaSource( - new MediaItem.Builder() - .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")) - .build()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); AtomicBoolean externalCallerSourceInfoRefreshedCalled = new AtomicBoolean(); MediaSource.MediaSourceCaller externalCaller = (source, timeline) -> externalCallerSourceInfoRefreshedCalled.set(true);