diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 7d3515dd86..5eb4ae65ff 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -19,6 +19,7 @@ body: options: - Media3 main branch - Media3 pre-release (alpha, beta or RC not in this list) + - Media3 1.8.0 - Media3 1.7.1 (same as 1.6.1) - Media3 1.7.0 (do not use) - Media3 1.6.1 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 29e47b87b6..4634f5b63a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,10 +1,27 @@ # Release notes -### Unreleased changes +## 1.8 + +### 1.8.0 (2025-07-30) + +This release includes the following changes since the +[1.7.1 release](#171-2025-05-16): * Common Library: * Add support for replacing the player in `ForwardingSimpleBasePlayer`. * ExoPlayer: + * Add getter for shuffle mode to the `ExoPlayer` interface + ([#2522](https://github.com/androidx/media/pull/2522)). + * More clearly throw an exception if `DefaultAudioSink` is accessed from + multiple threads. If this happens due to a call to + `RendererCapabilities.getFormatSupport` outside of the player, make sure + to call this method on the same thread as ExoPlayer's playback thread or + use a different instance than the one used for playback + ([#1191](https://github.com/androidx/media/issues/1191)). + * Fix bug where non-stereo audio formats on TVs may be marked as + unsupported by `DefaultTrackSelector`. + * Ensure the last frame is correctly rendered when using MediaCodec's + `DECODE_ONLY` flag (which is enabled by default in scrubbing mode). * Add support for using the virtual device ID from the `Context` passed to `ExoPlayer.Builder`. * Enable dynamic scheduling by default in scrubbing mode. @@ -20,155 +37,6 @@ * Fix bug where internal scheduling delayed last frame when seeking to the end while paused. For now, the bug fix only takes effect if `ExoPlayer.Builder.experimentalSetDynamicSchedulingEnabled` is enabled. -* Transformer: - * Add `CodecDbLite` that enables chipset specific optimizations of video - encoding settings. - * Add `setEnableCodecDbLite` flag to the `DefaultEncoderFactory` to enable - CodecDB Lite settings optimization. By default, this flag is set to - false. -* Track Selection: -* Extractors: - * Add support for seeking in fragmented MP4 with multiple `sidx` atoms. - This behavior can be enabled using the `FLAG_MERGE_FRAGMENTED_SIDX` flag - on `FragmentedMp4Extractor` - ([#9373](https://github.com/google/ExoPlayer/issues/9373)). - * Ignore empty seek tables in FLAC files (including those containing only - placeholder seek points), and fall back to binary search seeking if the - duration of the file is known - ([#2327]()https://github.com/androidx/media/issues/2327). - * Fix parsing of H.265 SEI units to fully skip unrecognized SEI types - ([#2456]()https://github.com/androidx/media/issues/2456). - * Update `WavExtractor` to use the header extension's SubFormat data for - the audio format when parsing a `WAVE_FORMAT_EXTENSIBLE` type file. - * MP4: Add support for `ipcm` and `fpcm` boxes defining raw PCM audio - tracks (64-bit floating point PCM is not supported). - * MP4: Handle the rotation part of `tkhd` transformation matrices that - both rotate and reflect the video. This ensures that reflected videos - taken by the iPhone front facing camera display the right way up, but - incorrectly reflected in the y-axis - ([#2012]()https://github.com/androidx/media/issues/2012). -* DataSource: -* Audio: - * Add support for all linear PCM sample formats in - `ChannelMappingAudioProcessor` and `TrimmingAudioProcessor`. - * Add support for audio gaps in `CompositionPlayer`. - * Remove spurious call to `BaseAudioProcessor#flush()` from - `BaseAudioProcessor#reset()`. -* Video: - * Improve smooth video frame release at startup when audio samples don't - start at exactly the requested position. - * Extend detached surface workaround to "realme" devices - ([#2059](https://github.com/androidx/media/issues/2059)). -* Text: - * Fix a playback stall when a subtitle segment initially fails to load and - later loads successfully, followed by several empty subtitle segments - ([#2517](https://github.com/androidx/media/issues/2517)). -* Metadata: - * Added support for retrieving media duration and `Timeline` to - `MetadataRetriever` and migrated it to an instance-based, - `AutoCloseable` API. Use the new `Builder` to create an instance for a - `MediaItem`, then call `retrieveTrackGroups()`, `retrieveTimeline()`, - and `retrieveDurationUs()` to get `ListenableFuture`s for the metadata. - The previous static methods are now deprecated - ([#2462](https://github.com/androidx/media/issues/2462)). -* Image: - * Limit decoded bitmaps to the display size in - `BitmapFactoryImageDecoder`, to avoid an app crashing with `Canvas: - trying to draw too large bitmap.` from `PlayerView` when trying to - display very large (e.g. 50MP) images. - * Change the signature of - `DefaultRenderersFactory.getImageDecoderFactory()` to take a `Context` - parameter. - * Align the max bitmap output size used in `CompositionPlayer` with that - already used in `Transformer` (meaning `CompositionPlayer` does not - consider the display size when decoding bitmaps, unlike `ExoPlayer`). -* DataSource: -* DRM: -* Effect: -* Muxers: - * Fix a bug where correct sample flags were not set for audio samples in - fragmented MP4. -* IMA extension: -* Session: - * Fix bug where calling `setSessionExtras` from the main thread when - running the player from a different application thread then the main - thread caused an `IllegalStateException` - ([#2265](https://github.com/androidx/media/pull/2265)). - * Don't automatically show a notification if a player is set up with media - items without preparing or playing them - ([#2423]()https://github.com/androidx/media/issues/2423). This behavior - is configurable via - `MediaSessionService.setShowNotificationForIdlePlayer`. - * Add custom `PlaybackException` for all or selected controllers. - * Fix bug where seeking in a live stream on a `MediaController` can cause - an `IllegalArgumentException`. - * For live streams, stop publishing a playback position and the ability to - seek in the current item for platform media controllers, to avoid - position artefacts in the Android Auto UI (and other controllers using - this information from the platform media session) - ([#1758](https://github.com/androidx/media/issues/1758)). -* UI: -* Downloads: -* OkHttp extension: -* Cronet extension: - * Add automatic cookie handling - ([#5975](https://github.com/google/ExoPlayer/issues/5975)). -* RTMP extension: -* HLS extension: - * Fix playlist parsing to accept `\f` (form feed) in quoted string - attribute values - ([#2420](https://github.com/androidx/media/issues/2420)). - * Support updating interstitials with the same ID - ([#2427](https://github.com/androidx/media/pull/2427)). - * Fix bug where playlist load errors are sometimes not propagated once a - live stream runs out of segments to load - ([#2401]()https://github.com/androidx/media/issues/2401). - * Fix bug where track selection changes after loading low-latency parts - and preload hints can cause playback to get stuck or freeze - ([#2299](https://github.com/androidx/media/issues/2299)). - * Group subtitle renditions by NAME tag, similar to how audio renditions - are grouped already - ([#1666](https://github.com/androidx/media/issues/1666)). -* DASH extension: - * Fix bug where shortening a DASH period duration can throw an exception - when samples beyond the new duration have already been read by the - rendering pipeline - ([#2440](https://github.com/androidx/media/issues/2440)). - * Fix bug where redirect wasn't followed when using CMCD query parameters - ([#2475](https://github.com/androidx/media/issues/2475)). -* Smooth Streaming extension: -* RTSP extension: - * Fix `RtspClient` to use the location uri as provided when processing an - HTTP 302 response - ([#2398](https://github.com/androidx/media/issues/2398)). -* Decoder extensions (FFmpeg, VP9, AV1, etc.): - * Fix bug where - `DefaultTrackSelector.setAllowInvalidateSelectionsOnRendererCapabilitiesChange` - has no effect for audio decoder extensions - ([#2258](https://github.com/androidx/media/issues/2258)). -* MIDI extension: -* Leanback extension: -* Cast extension: - * Add support for `setVolume()`, and `getVolume()` - ([#2279](https://github.com/androidx/media/pull/2279)). - * Prevent CastPlayer from entering STATE_BUFFERING while the timeline is - empty. -* Test Utilities: - * Add `advance(player).untilPositionAtLeast` and `untilMediaItemIndex` to - `TestPlayerRunHelper` in order to advance the player until a specified - position is reached. In most cases, these methods are more reliable than - the existing `untilPosition` and `untilStartOfMediaItem` methods. - * Move `FakeDownloader` to `test-utils-robolectric` module for reuse in - other tests. -* Remove deprecated symbols: - -## 1.8 - -### 1.8.0-alpha01 (2025-05-19) - -This release includes the following changes since [1.7.1](#171-2025-05-16): - -* ExoPlayer: * Add `ExoPlayer.setScrubbingModeEnabled(boolean)` method. This optimizes the player for many frequent seeks (for example, from a user dragging a scrubber bar around). The behavior of scrubbing mode can be customized @@ -192,6 +60,11 @@ This release includes the following changes since [1.7.1](#171-2025-05-16): * Add support of preloading from specified position in `DefaultPreloadManager`. * Transformer: + * Add `CodecDbLite` that enables chipset specific optimizations of video + encoding settings. + * Add `setEnableCodecDbLite` flag to the `DefaultEncoderFactory` to enable + CodecDB Lite settings optimization. By default, this flag is set to + false. * Filling an initial gap (added via `addGap()`) with silent audio now requires explicitly setting `experimentalSetForceAudioTrack(true)` in `EditedMediaItemSequence.Builder`. If the gap is in the middle of the @@ -208,11 +81,41 @@ This release includes the following changes since [1.7.1](#171-2025-05-16): custom `VideoCompositorSettings` to arrange sequences into a 2x2 or PiP layout. * Extractors: + * Parse metadata from fragmented MP4 files + ([#2084](https://github.com/androidx/media/issues/2084)). + * JPEG: Support motion photos that don't have an Exif segment at the start + ([#2552](https://github.com/androidx/media/issues/2552)). + * Add support for seeking in fragmented MP4 with multiple `sidx` atoms. + This behavior can be enabled using the `FLAG_MERGE_FRAGMENTED_SIDX` flag + on `FragmentedMp4Extractor` + ([#9373](https://github.com/google/ExoPlayer/issues/9373)). + * Ignore empty seek tables in FLAC files (including those containing only + placeholder seek points), and fall back to binary search seeking if the + duration of the file is known + ([#2327](https://github.com/androidx/media/issues/2327)). + * Fix parsing of H.265 SEI units to fully skip unrecognized SEI types + ([#2456](https://github.com/androidx/media/issues/2456)). + * Update `WavExtractor` to use the header extension's SubFormat data for + the audio format when parsing a `WAVE_FORMAT_EXTENSIBLE` type file. + * MP4: Add support for `ipcm` and `fpcm` boxes defining raw PCM audio + tracks (64-bit floating point PCM is not supported). + * MP4: Handle the rotation part of `tkhd` transformation matrices that + both rotate and reflect the video. This ensures that reflected videos + taken by the iPhone front facing camera display the right way up, but + incorrectly reflected in the y-axis + ([#2012](https://github.com/androidx/media/issues/2012)). * 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)). * Audio: + * Fix bug where `AnalyticsListener.onAudioPositionAdvancing` is not called + when the audio playback is started very close to the end of the media. + * Add support for all linear PCM sample formats in + `ChannelMappingAudioProcessor` and `TrimmingAudioProcessor`. + * Add support for audio gaps in `CompositionPlayer`. + * Remove spurious call to `BaseAudioProcessor#flush()` from + `BaseAudioProcessor#reset()`. * Allow constant power upmixing/downmixing in DefaultAudioMixer. * Make `ChannelMappingAudioProcessor`, `TrimmingAudioProcessor` and `ToFloatPcmAudioProcessor` public @@ -230,6 +133,12 @@ This release includes the following changes since [1.7.1](#171-2025-05-16): * Fix recovery to multichannel audio after fallback to stereo audio on some devices ([#2258](https://github.com/androidx/media/issues/2258)). * Video: + * Extend detached surface workaround to "lenovo" and "motorola" devices + ([#2059](https://github.com/androidx/media/issues/2059)). + * Improve smooth video frame release at startup when audio samples don't + start at exactly the requested position. + * Extend detached surface workaround to "realme" devices + ([#2059](https://github.com/androidx/media/issues/2059)). * 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 @@ -240,6 +149,11 @@ This release includes the following changes since [1.7.1](#171-2025-05-16): * Fix VP9 Widevine playback errors on some devices ([#2408](https://github.com/androidx/media/issues/2408)). * Text: + * Add support for VobSub tracks in MP4 files + ([#2510](https://github.com/androidx/media/issues/2510)). + * Fix a playback stall when a subtitle segment initially fails to load and + later loads successfully, followed by several empty subtitle segments + ([#2517](https://github.com/androidx/media/issues/2517)). * Fix SSA and SubRip to display an in-progress cue when enabling subtitles ([#2309](https://github.com/androidx/media/issues/2309)). * Fix playback getting stuck when switching from a stream with a subtitle @@ -257,11 +171,37 @@ This release includes the following changes since [1.7.1](#171-2025-05-16): files which is used to define the z-order of cues when more than one is shown on screen at the same time ([#2124](https://github.com/androidx/media/issues/2124)). +* Metadata: + * Added support for retrieving media duration and `Timeline` to + `MetadataRetriever` and migrated it to an instance-based, + `AutoCloseable` API. Use the new `Builder` to create an instance for a + `MediaItem`, then call `retrieveTrackGroups()`, `retrieveTimeline()`, + and `retrieveDurationUs()` to get `ListenableFuture`s for the metadata. + The previous static methods are now deprecated + ([#2462](https://github.com/androidx/media/issues/2462)). +* Image: + * Limit decoded bitmaps to the display size in + `BitmapFactoryImageDecoder`, to avoid an app crashing with `Canvas: + trying to draw too large bitmap.` from `PlayerView` when trying to + display very large (e.g. 50MP) images. + * Change the signature of + `DefaultRenderersFactory.getImageDecoderFactory()` to take a `Context` + parameter. + * Align the max bitmap output size used in `CompositionPlayer` with that + already used in `Transformer` (meaning `CompositionPlayer` does not + consider the display size when decoding bitmaps, unlike `ExoPlayer`). +* DRM: + * Add new overload of `OfflineLicenseHelper.newWidevineInstance` accepting + a `MediaItem.DrmConfiguration` so that HTTP request headers can be + applied correctly + ([#2169](https://github.com/androidx/media/issues/2169)). * Effect: * Add `Presentation.createForShortSide(int)` that creates a `Presentation` that ensures the shortest side always matches the given value, regardless of input orientation. * Muxers: + * Fix a bug where correct sample flags were not set for audio samples in + fragmented MP4. * `writeSampleData()` API now uses muxer specific `BufferInfo` class instead of `MediaCodec.BufferInfo`. * Add `Muxer.Factory#supportsWritingNegativeTimestampsInEditList` which @@ -274,17 +214,50 @@ This release includes the following changes since [1.7.1](#171-2025-05-16): `MediaPeriodQueue` anymore ([#2215](https://github.com/androidx/media/issues/2215)). * Session: + * Fix bug where connections from third-party non-privileged Media3 + controllers are ignored. + * Remove check for available commands when sending custom commands to a + legacy `MediaBrowserServiceCompat`. This is in parity with the behavior + of legacy controllers/browsers when connected to a legacy app. + * Fix a bug that causes a player's first playback error to be incorrectly + treated as a persistent custom exception. This prevents the application + from recovering. + * Fix bug where some controller changes that are not handled by the + session may cause `IllegalStateExceptions`. + * Fix bug where controller actions that are not handled by the session may + leave the controller in an invalid state. + * Fix StrictMode unsafe launch violation warning + ([#2330](https://github.com/androidx/media/pull/2330)). + * Fix bug where calling `setSessionExtras` from the main thread when + running the player from a different application thread then the main + thread caused an `IllegalStateException` + ([#2265](https://github.com/androidx/media/pull/2265)). + * Don't automatically show a notification if a player is set up with media + items without preparing or playing them + ([#2423]()https://github.com/androidx/media/issues/2423). This behavior + is configurable via + `MediaSessionService.setShowNotificationForIdlePlayer`. + * Add custom `PlaybackException` for all or selected controllers. + * Fix bug where seeking in a live stream on a `MediaController` can cause + an `IllegalArgumentException`. + * For live streams, stop publishing a playback position and the ability to + seek in the current item for platform media controllers, to avoid + position artefacts in the Android Auto UI (and other controllers using + this information from the platform media session) + ([#1758](https://github.com/androidx/media/issues/1758)). * Fix a bug where passing null into `getLibraryRoot` of a `MediaBrowser` connected to a legacy `MediaBrowserServiceCompat` produced a `NullPointerException`. - * Fix a bug where where sending custom actions, a search result or a - getItem request crashed the legacy session app with a - `ClassNotFoundException`. + * Fix a bug where sending custom actions, a search result or a getItem + request crashed the legacy session app with a `ClassNotFoundException`. * Fix a bug where `MediaItem.LocalConfiguration.uri` was shared to the platform sessions's `MediaMetadata`. To intentionally share a URI to allow controllers to re-request the media, set `MediaItem.RequestMetadata.mediaUri` instead. * UI: + * Fix bug where `PlayerSurface` inside re-usable components like + `LazyColumn` didn't work correctly + ([#2493](https://github.com/androidx/media/issues/2493)). * Fix a Compose bug which resulted in a gap between setting the initial button states and observing the change in state (e.g. icon shapes or being enabled). Any changes made to the Player outside of the @@ -318,17 +291,73 @@ This release includes the following changes since [1.7.1](#171-2025-05-16): carries the resolved time range, with which a concrete `SegmentDownloader` can be created and download the content correspondingly. +* Cronet extension: + * Add automatic cookie handling + ([#5975](https://github.com/google/ExoPlayer/issues/5975)). * HLS extension: + * Fix bug where `HlsSampleStreamWrapper` attempts to seek inside buffer + when there are no chunks available in the buffer + [#2598](https://github.com/androidx/media/issues/2598). + * Fix bug where track selection changes after loading low-latency parts + and preload hints can cause playback to get stuck or freeze + ([#2299](https://github.com/androidx/media/issues/2299)). + * Prevent excessive reloads by waiting for half the target duration when + `CAN-BLOCK-RELOAD=YES` is not honored by the server + ([#2317](https://github.com/androidx/media/pull/2317)). + * Fix bug where playback was stalled when starting an interstitials stream + before a mid roll and asset list resolution was attempted for the wrong + ad ([#2558](https://github.com/androidx/media/issues/2558)). + * Fix playlist parsing to accept `\f` (form feed) in quoted string + attribute values + ([#2420](https://github.com/androidx/media/issues/2420)). + * Support updating interstitials with the same ID + ([#2427](https://github.com/androidx/media/pull/2427)). + * Fix bug where playlist load errors are sometimes not propagated once a + live stream runs out of segments to load + ([#2401]()https://github.com/androidx/media/issues/2401). + * Group subtitle renditions by NAME tag, similar to how audio renditions + are grouped already + ([#1666](https://github.com/androidx/media/issues/1666)). * Support X-ASSET-LIST and live streams with `HlsInterstitialsAdsLoader`. +* DASH extension: + * Fix issue where trick-play adaptation set is merged with its main + adaptation set to form an invalid `TrackGroup` + ([#2148](https://github.com/androidx/media/issues/2148)). + * Fix bug where shortening a DASH period duration can throw an exception + when samples beyond the new duration have already been read by the + rendering pipeline + ([#2440](https://github.com/androidx/media/issues/2440)). + * Fix bug where redirect wasn't followed when using CMCD query parameters + ([#2475](https://github.com/androidx/media/issues/2475)). * RTSP extension: + * Add support for RTP Aggregation Packet for H265 in accordance with RFC + 7798#4.4.2 ([#2413](https://github.com/androidx/media/pull/2413)). + * Fix `RtspClient` to use the location uri as provided when processing an + HTTP 302 response + ([#2398](https://github.com/androidx/media/issues/2398)). * Add parsing support for SessionDescriptions containing lines with trailing whitespace characters ([#2357](https://github.com/androidx/media/issues/2357)). +* Decoder extensions (FFmpeg, VP9, AV1, etc.): + * Fix bug where + `DefaultTrackSelector.setAllowInvalidateSelectionsOnRendererCapabilitiesChange` + has no effect for audio decoder extensions + ([#2258](https://github.com/androidx/media/issues/2258)). * Cast extension: + * Add support for `setVolume()`, and `getVolume()` + ([#2279](https://github.com/androidx/media/pull/2279)). + * Prevent CastPlayer from entering STATE_BUFFERING while the timeline is + empty. * Add support for `getDeviceVolume()`, `setDeviceVolume()`, `getDeviceMuted()`, and `setDeviceMuted()` ([#2089](https://github.com/androidx/media/issues/2089)). * Test Utilities: + * Add `advance(player).untilPositionAtLeast` and `untilMediaItemIndex` to + `TestPlayerRunHelper` in order to advance the player until a specified + position is reached. In most cases, these methods are more reliable than + the existing `untilPosition` and `untilStartOfMediaItem` methods. + * Move `FakeDownloader` to `test-utils-robolectric` module for reuse in + other tests. * Removed `transformer.TestUtil.addAudioDecoders(String...)`, `transformer.TestUtil.addAudioEncoders(String...)`, and `transformer.TestUtil.addAudioEncoders(ShadowMediaCodec.CodecConfig, @@ -356,6 +385,22 @@ This release includes the following changes since [1.7.1](#171-2025-05-16): MediaCodecSelector, long, boolean, @Nullable Handler, @Nullable VideoRendererEventListener, int, float, @Nullable VideoSinkProvider)`. +### 1.8.0-rc02 (2025-07-24) + +Use the 1.8.0 [stable version](#180-2025-07-30). + +### 1.8.0-rc01 (2025-07-16) + +Use the 1.8.0 [stable version](#180-2025-07-30). + +### 1.8.0-beta01 (2025-06-24) + +Use the 1.8.0 [stable version](#180-2025-07-30). + +### 1.8.0-alpha01 (2025-05-19) + +Use the 1.8.0 [stable version](#180-2025-07-30). + ## 1.7 ### 1.7.1 (2025-05-16) diff --git a/constants.gradle b/constants.gradle index 60c7c20beb..4105ba9ca9 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.8.0-alpha01' - releaseVersionCode = 1_008_000_0_01 + releaseVersion = '1.8.0' + releaseVersionCode = 1_008_000_3_00 minSdkVersion = 21 // See https://developer.android.com/training/cars/media/automotive-os#automotive-module automotiveMinSdkVersion = 28 diff --git a/core_settings.gradle b/core_settings.gradle index 78818ce532..6058097b79 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -62,8 +62,6 @@ include modulePrefix + 'lib-datasource' project(modulePrefix + 'lib-datasource').projectDir = new File(rootDir, 'libraries/datasource') include modulePrefix + 'lib-datasource-cronet' project(modulePrefix + 'lib-datasource-cronet').projectDir = new File(rootDir, 'libraries/datasource_cronet') -include modulePrefix + 'lib-datasource-httpengine' -project(modulePrefix + 'lib-datasource-httpengine').projectDir = new File(rootDir, 'libraries/datasource_httpengine') include modulePrefix + 'lib-datasource-rtmp' project(modulePrefix + 'lib-datasource-rtmp').projectDir = new File(rootDir, 'libraries/datasource_rtmp') include modulePrefix + 'lib-datasource-okhttp' @@ -73,8 +71,6 @@ include modulePrefix + 'lib-decoder' project(modulePrefix + 'lib-decoder').projectDir = new File(rootDir, 'libraries/decoder') include modulePrefix + 'lib-decoder-av1' project(modulePrefix + 'lib-decoder-av1').projectDir = new File(rootDir, 'libraries/decoder_av1') -include modulePrefix + 'lib-decoder-dav1d' -project(modulePrefix + 'lib-decoder-dav1d').projectDir = new File(rootDir, 'libraries/decoder_dav1d') include modulePrefix + 'lib-decoder-ffmpeg' project(modulePrefix + 'lib-decoder-ffmpeg').projectDir = new File(rootDir, 'libraries/decoder_ffmpeg') include modulePrefix + 'lib-decoder-flac' diff --git a/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewViewModel.kt b/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewViewModel.kt index 76493ff2df..d61be5acfd 100644 --- a/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewViewModel.kt +++ b/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewViewModel.kt @@ -286,10 +286,7 @@ class CompositionPreviewViewModel(application: Application, val compositionLayou else -> 1 // Sequence } // TODO(b/417365294): Improve how sequences are built - val videoSequenceBuilders = - MutableList(numSequences) { _ -> - EditedMediaItemSequence.Builder() - } + val videoSequenceBuilders = MutableList(numSequences) { EditedMediaItemSequence.Builder() } val videoSequences = mutableListOf() for (sequenceIndex in 0 until numSequences) { var hasItem = false diff --git a/demos/composition/src/main/java/androidx/media3/demo/composition/ui/UiComponents.kt b/demos/composition/src/main/java/androidx/media3/demo/composition/ui/UiComponents.kt index cefb0276ba..e1b04fb7ad 100644 --- a/demos/composition/src/main/java/androidx/media3/demo/composition/ui/UiComponents.kt +++ b/demos/composition/src/main/java/androidx/media3/demo/composition/ui/UiComponents.kt @@ -29,8 +29,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.pointerInput diff --git a/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java b/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java index 3e513b3d85..efa264b975 100644 --- a/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java +++ b/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java @@ -32,6 +32,7 @@ import androidx.media3.common.Player; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.GlUtil.GlException; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DefaultDataSource; @@ -77,10 +78,16 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { Context context = getApplicationContext(); boolean requestSecureSurface = getIntent().hasExtra(DRM_SCHEME_EXTRA); - if (requestSecureSurface && !GlUtil.isProtectedContentExtensionSupported(context)) { - Toast.makeText( - context, R.string.error_protected_content_extension_not_supported, Toast.LENGTH_LONG) - .show(); + try { + if (requestSecureSurface && !GlUtil.isProtectedContentExtensionSupported(context)) { + Toast.makeText( + context, + R.string.error_protected_content_extension_not_supported, + Toast.LENGTH_LONG) + .show(); + } + } catch (GlException e) { + Toast.makeText(context, R.string.gl_error_occurred, Toast.LENGTH_LONG).show(); } VideoProcessingGLSurfaceView videoProcessingGLSurfaceView = diff --git a/demos/gl/src/main/res/values/strings.xml b/demos/gl/src/main/res/values/strings.xml index 7e9e5d9961..9d20711f67 100644 --- a/demos/gl/src/main/res/values/strings.xml +++ b/demos/gl/src/main/res/values/strings.xml @@ -18,5 +18,6 @@ ExoPlayer GL demo The GL protected content extension is not supported. + GL error occurred. diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 22829405d9..8b8daf420f 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -83,7 +83,6 @@ dependencies { implementation project(modulePrefix + 'lib-datasource-cronet') implementation project(modulePrefix + 'lib-exoplayer-ima') withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-av1') - withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-dav1d') withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-ffmpeg') withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-flac') withDecoderExtensionsImplementation project(modulePrefix + 'lib-decoder-opus') diff --git a/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java b/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java index ef8e10ed2b..f01df16de7 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java @@ -409,10 +409,8 @@ public void execute() { () -> { OfflineLicenseHelper offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance( - drmConfiguration.licenseUri.toString(), - drmConfiguration.forceDefaultLicenseUri, + drmConfiguration, dataSourceFactory, - drmConfiguration.licenseRequestHeaders, new DrmSessionEventListener.EventDispatcher()); try { keySetId = offlineLicenseHelper.downloadLicense(format); 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 4788496f81..970af96bec 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 @@ -116,7 +116,7 @@ class MainActivity : AppCompatActivity() { override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, - grantResults: IntArray + grantResults: IntArray, ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (grantResults.isEmpty()) { @@ -134,7 +134,7 @@ class MainActivity : AppCompatActivity() { browserFuture = MediaBrowser.Builder( this, - SessionToken(this, ComponentName(this, PlaybackService::class.java)) + SessionToken(this, ComponentName(this, PlaybackService::class.java)), ) .buildAsync() browserFuture.addListener({ pushRoot() }, ContextCompat.getMainExecutor(this)) @@ -153,7 +153,7 @@ class MainActivity : AppCompatActivity() { mediaItem.mediaId, /* page= */ 0, /* pageSize= */ Int.MAX_VALUE, - /* params= */ null + /* params= */ null, ) subItemMediaList.clear() @@ -164,7 +164,7 @@ class MainActivity : AppCompatActivity() { subItemMediaList.addAll(children) mediaListAdapter.notifyDataSetChanged() }, - ContextCompat.getMainExecutor(this) + ContextCompat.getMainExecutor(this), ) } @@ -175,7 +175,7 @@ class MainActivity : AppCompatActivity() { private fun popPathStack() { treePathStack.removeLast() - if (treePathStack.size == 0) { + if (treePathStack.isEmpty()) { finish() return } @@ -197,14 +197,14 @@ class MainActivity : AppCompatActivity() { val root: MediaItem = result.value!! pushPathStack(root) }, - ContextCompat.getMainExecutor(this) + ContextCompat.getMainExecutor(this), ) } private class FolderMediaItemArrayAdapter( context: Context, viewID: Int, - mediaItemList: List + mediaItemList: List, ) : ArrayAdapter(context, viewID, mediaItemList) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val mediaItem = getItem(position)!! diff --git a/demos/session_service/src/main/java/androidx/media3/demo/session/DemoPlaybackService.kt b/demos/session_service/src/main/java/androidx/media3/demo/session/DemoPlaybackService.kt index 37f2f9546e..24bbac7f90 100644 --- a/demos/session_service/src/main/java/androidx/media3/demo/session/DemoPlaybackService.kt +++ b/demos/session_service/src/main/java/androidx/media3/demo/session/DemoPlaybackService.kt @@ -161,12 +161,11 @@ open class DemoPlaybackService : MediaLibraryService() { // The media session always supports skip, except at the start and end of the playlist. // Reserve the space for the skip action in these cases to avoid custom actions jumping // around when the user skips. - mediaLibrarySession.setSessionExtras( + mediaLibrarySession.sessionExtras = bundleOf( MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV to true, MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT to true, ) - ) } } diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaItemDatabase.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaItemDatabase.kt index 04d4170b9a..19577d6fe2 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaItemDatabase.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaItemDatabase.kt @@ -29,7 +29,7 @@ class MediaItemDatabase { ) fun get(index: Int): MediaItem { - val uri = mediaUris.get(index.mod(mediaUris.size)) + val uri = mediaUris[index.mod(mediaUris.size)] return MediaItem.Builder().setUri(uri).setMediaId(index.toString()).build() } } diff --git a/docsamples/build.gradle b/docsamples/build.gradle new file mode 100644 index 0000000000..2ad014b92c --- /dev/null +++ b/docsamples/build.gradle @@ -0,0 +1,35 @@ +// Copyright 2023 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. +apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" + +apply plugin: 'kotlin-android' + +android { + namespace 'androidx.media3.docsamples' + + kotlinOptions { + jvmTarget = '1.8' + } + + defaultConfig { + minSdkVersion 23 + } +} + +dependencies { + implementation project(modulePrefix + 'lib-exoplayer') + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion + implementation 'androidx.activity:activity-compose:1.9.0' +} diff --git a/libraries/decoder_dav1d/src/main/AndroidManifest.xml b/docsamples/src/main/AndroidManifest.xml similarity index 86% rename from libraries/decoder_dav1d/src/main/AndroidManifest.xml rename to docsamples/src/main/AndroidManifest.xml index 36f2089f0f..32eaa0b95f 100644 --- a/libraries/decoder_dav1d/src/main/AndroidManifest.xml +++ b/docsamples/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - - - + diff --git a/docsamples/src/main/java/androidx/media3/docsamples/PreloadManagerKotlinSnippets.kt b/docsamples/src/main/java/androidx/media3/docsamples/PreloadManagerKotlinSnippets.kt new file mode 100644 index 0000000000..9f257b0d72 --- /dev/null +++ b/docsamples/src/main/java/androidx/media3/docsamples/PreloadManagerKotlinSnippets.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2025 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.docsamples + +import android.os.Bundle +import androidx.annotation.OptIn +import androidx.appcompat.app.AppCompatActivity +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.preload.DefaultPreloadManager +import androidx.media3.exoplayer.source.preload.TargetPreloadStatusControl +import java.lang.Math.abs + +// constants to make the code snippets work +const val currentPlayingIndex = 10 + +@UnstableApi +// [START android_defaultpreloadmanager_MyTargetPreloadStatusControl] +class MyTargetPreloadStatusControl(currentPlayingIndex: Int = C.INDEX_UNSET) : + TargetPreloadStatusControl { + + override fun getTargetPreloadStatus(index: Int): DefaultPreloadManager.PreloadStatus? { + if (index - currentPlayingIndex == 1) { // next track + // return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED and + // suggest loading 3000ms from the default start position + return DefaultPreloadManager.PreloadStatus.specifiedRangeLoaded(3000L) + } else if (index - currentPlayingIndex == -1) { // previous track + // return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED and + // suggest loading 3000ms from the default start position + return DefaultPreloadManager.PreloadStatus.specifiedRangeLoaded(3000L) + } else if (abs(index - currentPlayingIndex) == 2) { + // return a PreloadStatus that is labelled by STAGE_TRACKS_SELECTED + return DefaultPreloadManager.PreloadStatus.TRACKS_SELECTED + } else if (abs(index - currentPlayingIndex) <= 4) { + // return a PreloadStatus that is labelled by STAGE_SOURCE_PREPARED + return DefaultPreloadManager.PreloadStatus.SOURCE_PREPARED + } + return null + } +} + +// [END android_defaultpreloadmanager_MyTargetPreloadStatusControl] + +class PreloadManagerSnippetsKotlin { + + class PreloadSnippetsActivity : AppCompatActivity() { + private val context = this + + @OptIn(UnstableApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // [START android_defaultpreloadmanager_createPLM] + val targetPreloadStatusControl = MyTargetPreloadStatusControl() + val preloadManagerBuilder = DefaultPreloadManager.Builder(context, targetPreloadStatusControl) + val preloadManager = preloadManagerBuilder.build() + // [END android_defaultpreloadmanager_createPLM] + + val player = preloadManagerBuilder.buildExoPlayer() + + // [START android_defaultpreloadmanager_addMedia] + val initialMediaItems = pullMediaItemsFromService(count = 20) + for (index in 0 until initialMediaItems.size) { + preloadManager.add(initialMediaItems.get(index), /* rankingData= */ index) + } + // items aren't actually loaded yet! need to call invalidate() after this + // [END android_defaultpreloadmanager_addMedia] + + // [START android_defaultpreloadmanager_invalidate] + preloadManager.invalidate() + // [END android_defaultpreloadmanager_invalidate] + } + + @OptIn(UnstableApi::class) + private fun fetchMedia( + preloadManager: DefaultPreloadManager, + mediaItem: MediaItem, + player: ExoPlayer, + currentIndex: Int, + ) { + // [START android_defaultpreloadmanager_getAndPlayMedia] + // When a media item is about to display on the screen + val mediaSource = preloadManager.getMediaSource(mediaItem) + if (mediaSource != null) { + player.setMediaSource(mediaSource) + } else { + // If mediaSource is null, that mediaItem hasn't been added to the preload manager + // yet. So, send it directly to the player when it's about to play + player.setMediaItem(mediaItem) + } + player.prepare() + + // When the media item is displaying at the center of the screen + player.play() + preloadManager.setCurrentPlayingIndex(currentIndex) + + // Need to call invalidate() to update the priorities + preloadManager.invalidate() + // [END android_defaultpreloadmanager_getAndPlayMedia] + } + + @OptIn(UnstableApi::class) + private fun removeMedia(mediaItem: MediaItem, preloadManager: DefaultPreloadManager) { + // [START android_defaultpreloadmanager_removeItem] + preloadManager.remove(mediaItem) + // [END android_defaultpreloadmanager_removeItem] + } + + @OptIn(UnstableApi::class) + private fun releasePLM(preloadManager: DefaultPreloadManager) { + // [START android_defaultpreloadmanager_releasePLM] + preloadManager.release() + // [END android_defaultpreloadmanager_releasePLM] + } + + // no-op methods to support the code snippets + private fun pullMediaItemsFromService(count: Int): List { + return listOf() + } + } +} diff --git a/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/package-info.java b/docsamples/src/main/java/androidx/media3/docsamples/package-info.java similarity index 87% rename from libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/package-info.java rename to docsamples/src/main/java/androidx/media3/docsamples/package-info.java index 30c7ab0503..015be68b17 100644 --- a/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/package-info.java +++ b/docsamples/src/main/java/androidx/media3/docsamples/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Android Open Source Project + * Copyright 2023 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. @@ -14,6 +14,6 @@ * limitations under the License. */ @NonNullApi -package androidx.media3.decoder.dav1d; +package androidx.media3.docsamples; import androidx.media3.common.util.NonNullApi; 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 4b89517e09..75da592c9d 100644 --- a/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java +++ b/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java @@ -54,6 +54,7 @@ private AudioAttributesV21(AudioAttributes audioAttributes) { } if (SDK_INT >= 32) { Api32.setSpatializationBehavior(builder, audioAttributes.spatializationBehavior); + Api32.setIsContentSpatialized(builder, audioAttributes.isContentSpatialized); } this.audioAttributes = builder.build(); } @@ -74,6 +75,7 @@ public static final class Builder { private @C.AudioUsage int usage; private @C.AudioAllowedCapturePolicy int allowedCapturePolicy; private @C.SpatializationBehavior int spatializationBehavior; + private boolean isContentSpatialized; /** * Creates a new builder for {@link AudioAttributes}. @@ -87,6 +89,7 @@ public Builder() { usage = C.USAGE_MEDIA; allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL; spatializationBehavior = C.SPATIALIZATION_BEHAVIOR_AUTO; + isContentSpatialized = false; } /** See {@link android.media.AudioAttributes.Builder#setContentType(int)} */ @@ -124,10 +127,23 @@ public Builder setSpatializationBehavior(@C.SpatializationBehavior int spatializ return this; } + /** See {@link android.media.AudioAttributes.Builder#setIsContentSpatialized(boolean)}. */ + @CanIgnoreReturnValue + @UnstableApi + public Builder setIsContentSpatialized(boolean isContentSpatialized) { + this.isContentSpatialized = isContentSpatialized; + return this; + } + /** Creates an {@link AudioAttributes} instance from this builder. */ public AudioAttributes build() { return new AudioAttributes( - contentType, flags, usage, allowedCapturePolicy, spatializationBehavior); + contentType, + flags, + usage, + allowedCapturePolicy, + spatializationBehavior, + isContentSpatialized); } } @@ -146,6 +162,9 @@ public AudioAttributes build() { /** The {@link C.SpatializationBehavior}. */ public final @C.SpatializationBehavior int spatializationBehavior; + /** Whether the content is spatialized. */ + @UnstableApi public final boolean isContentSpatialized; + @Nullable private AudioAttributesV21 audioAttributesV21; private AudioAttributes( @@ -153,12 +172,14 @@ private AudioAttributes( @C.AudioFlags int flags, @C.AudioUsage int usage, @C.AudioAllowedCapturePolicy int allowedCapturePolicy, - @C.SpatializationBehavior int spatializationBehavior) { + @C.SpatializationBehavior int spatializationBehavior, + boolean isContentSpatialized) { this.contentType = contentType; this.flags = flags; this.usage = usage; this.allowedCapturePolicy = allowedCapturePolicy; this.spatializationBehavior = spatializationBehavior; + this.isContentSpatialized = isContentSpatialized; } /** @@ -224,7 +245,8 @@ public boolean equals(@Nullable Object obj) { && this.flags == other.flags && this.usage == other.usage && this.allowedCapturePolicy == other.allowedCapturePolicy - && this.spatializationBehavior == other.spatializationBehavior; + && this.spatializationBehavior == other.spatializationBehavior + && this.isContentSpatialized == other.isContentSpatialized; } @Override @@ -235,6 +257,7 @@ public int hashCode() { result = 31 * result + usage; result = 31 * result + allowedCapturePolicy; result = 31 * result + spatializationBehavior; + result = 31 * result + (isContentSpatialized ? 1 : 0); return result; } @@ -243,6 +266,7 @@ public int hashCode() { 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); + private static final String FIELD_IS_CONTENT_SPATIALIZED = Util.intToStringMaxRadix(5); @UnstableApi public Bundle toBundle() { @@ -252,6 +276,7 @@ public Bundle toBundle() { bundle.putInt(FIELD_USAGE, usage); bundle.putInt(FIELD_ALLOWED_CAPTURE_POLICY, allowedCapturePolicy); bundle.putInt(FIELD_SPATIALIZATION_BEHAVIOR, spatializationBehavior); + bundle.putBoolean(FIELD_IS_CONTENT_SPATIALIZED, isContentSpatialized); return bundle; } @@ -274,9 +299,11 @@ public static AudioAttributes fromBundle(Bundle bundle) { if (bundle.containsKey(FIELD_SPATIALIZATION_BEHAVIOR)) { builder.setSpatializationBehavior(bundle.getInt(FIELD_SPATIALIZATION_BEHAVIOR)); } + if (bundle.containsKey(FIELD_IS_CONTENT_SPATIALIZED)) { + builder.setIsContentSpatialized(bundle.getBoolean(FIELD_IS_CONTENT_SPATIALIZED)); + } return builder.build(); } - ; @RequiresApi(29) private static final class Api29 { @@ -296,5 +323,10 @@ public static void setSpatializationBehavior( @C.SpatializationBehavior int spatializationBehavior) { builder.setSpatializationBehavior(spatializationBehavior); } + + public static void setIsContentSpatialized( + android.media.AudioAttributes.Builder builder, boolean isContentSpatialized) { + builder.setIsContentSpatialized(isContentSpatialized); + } } } 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 411c0e3451..590a60edce 100644 --- a/libraries/common/src/main/java/androidx/media3/common/C.java +++ b/libraries/common/src/main/java/androidx/media3/common/C.java @@ -1669,17 +1669,24 @@ private C() {} @UnstableApi public static final int FORMAT_UNSUPPORTED_DRM = 0b010; /** - * Formats with the same top-level type are generally supported, but not this format or any other - * format with the same MIME type because the sub-type is not supported. + * Formats with the same type of media (e.g. video, audio, image or text) are generally supported, + * but not this format. * - *

Example: The player supports audio and the format's MIME type matches audio/[subtype], but - * there does not exist a suitable decoder for [subtype]. + *

Example: The player supports audio and the format's {@linkplain MimeTypes#isAudio(String) + * MIME type is for audio}, but there does not exist a suitable decoder for this format's MIME + * type. + * + * @see MimeTypes#isAudio(String) + * @see MimeTypes#isVideo(String) + * @see MimeTypes#isImage(String) + * @see MimeTypes#isText(String) */ @UnstableApi public static final int FORMAT_UNSUPPORTED_SUBTYPE = 0b001; /** - * The format is unsupported, because no formats with the same top-level type are supported or - * there is only specialized support for different MIME types of the same top-level type. + * The format is unsupported, because no formats with the same type of media (e.g. video, audio, + * image or text) are supported or there is only specialized support for different MIME types of + * the same type. * *

Example 1: The player is a general purpose audio player, but the format has a video MIME * type. 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 d294816b6a..3deab89560 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.0-beta01". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "1.8.0-alpha01"; + public static final String VERSION = "1.8.0"; /** 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.8.0-alpha01"; + public static final String VERSION_SLASHY = "AndroidXMedia3/1.8.0"; /** * 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_008_000_0_01; + public static final int VERSION_INT = 1_008_000_3_00; /** Whether the library was compiled with {@link Assertions} checks enabled. */ public static final boolean ASSERTIONS_ENABLED = true; 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 aac3054179..aac7f99e33 100644 --- a/libraries/common/src/main/java/androidx/media3/common/PlaybackParameters.java +++ b/libraries/common/src/main/java/androidx/media3/common/PlaybackParameters.java @@ -143,5 +143,4 @@ public static PlaybackParameters fromBundle(Bundle bundle) { float pitch = bundle.getFloat(FIELD_PITCH, /* defaultValue= */ 1f); return new PlaybackParameters(speed, pitch); } - ; } 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 9f7f8ee279..90e8d56a4f 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -1894,7 +1894,7 @@ private Timeline.Period getPeriod( uid, windowIndex, /* durationUs= */ positionInFirstPeriodUs + durationUs, - /* positionInWindowUs= */ 0, + /* positionInWindowUs= */ -positionInFirstPeriodUs, AdPlaybackState.NONE, isPlaceholder); } else { diff --git a/libraries/common/src/main/java/androidx/media3/common/util/BackgroundThreadStateHandler.java b/libraries/common/src/main/java/androidx/media3/common/util/BackgroundThreadStateHandler.java index 647e28c837..5d288d3fd0 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/BackgroundThreadStateHandler.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/BackgroundThreadStateHandler.java @@ -115,11 +115,11 @@ public void updateStateAsync( Function placeholderState, Function backgroundStateUpdate) { checkState(Looper.myLooper() == foregroundHandler.getLooper()); pendingOperations++; - backgroundHandler.post( + runInBackground( () -> { backgroundState = backgroundStateUpdate.apply(backgroundState); T newState = backgroundState; - foregroundHandler.post( + runInForeground( () -> { if (--pendingOperations == 0) { updateStateInForeground(newState); @@ -139,7 +139,7 @@ public void updateStateAsync( */ public void setStateInBackground(T newState) { backgroundState = newState; - foregroundHandler.post( + runInForeground( () -> { if (pendingOperations == 0) { updateStateInForeground(newState); @@ -159,9 +159,21 @@ public void setStateInBackground(T newState) { * @param runnable The {@link Runnable} to be called on the background thread. */ public void runInBackground(Runnable runnable) { + if (!backgroundHandler.getLooper().getThread().isAlive()) { + // Avoid sending messages on dead thread. + return; + } backgroundHandler.post(runnable); } + private void runInForeground(Runnable runnable) { + if (!foregroundHandler.getLooper().getThread().isAlive()) { + // Avoid sending messages on dead thread. + return; + } + foregroundHandler.post(runnable); + } + private void updateStateInForeground(T newState) { T oldState = foregroundState; foregroundState = newState; diff --git a/libraries/common/src/main/java/androidx/media3/common/util/CodecSpecificDataUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/CodecSpecificDataUtil.java index 3f75edcd53..62c17527af 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/CodecSpecificDataUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/CodecSpecificDataUtil.java @@ -64,6 +64,8 @@ public final class CodecSpecificDataUtil { private static final String CODEC_ID_MP4A = "mp4a"; // AC-4 private static final String CODEC_ID_AC4 = "ac-4"; + // IAMF + private static final String CODEC_ID_IAMF = "iamf"; private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$"); @@ -96,6 +98,47 @@ public static List buildCea708InitializationData(boolean isWideAspectRat return Collections.singletonList(isWideAspectRatio ? new byte[] {1} : new byte[] {0}); } + /** + * Builds an RFC 6381 IAMF codec string from the given {@code initializationData}. + * + *

The format is defined by the IAMF Codec Parameters String + * specification. + */ + public static String buildIamfCodecString(byte[] initializationData) { + ParsableByteArray parsableByteArray = new ParsableByteArray(initializationData); + parsableByteArray.skipLeb128(); // configOBUs_size + // IASequenceHeaderOBU + parsableByteArray.skipBytes(4); // ia_code (4 bytes) + int primaryProfile = parsableByteArray.readUnsignedByte(); // primary_profile (1 byte) + int additionalProfile = parsableByteArray.readUnsignedByte(); // additional_profile (1 byte) + + // OBUHeader + // obu_type (5 bits) + obu_redundant_copy (1 bit) + obu_trimming_status_flag (1 bit) + + // obu_extension_flag (1 bit) + parsableByteArray.skipBytes(1); + parsableByteArray.skipLeb128(); // obu_size + + // CodecConfigOBU + parsableByteArray.skipLeb128(); // codec_config_id + String codecId = parsableByteArray.readString(4); // codec_id (4 bytes) + + if (codecId.equals("mp4a")) { + parsableByteArray.skipLeb128(); // num_samples_per_frame + parsableByteArray.skipBytes(2); // audio_roll_distance (2 bytes) + + ParsableBitArray decoderConfigBitArray = new ParsableBitArray(); + decoderConfigBitArray.reset(parsableByteArray); + int audioObjectType = decoderConfigBitArray.readBits(5); + // If audioObjectType is an escape value, then we need to read 6 bits. + if (audioObjectType == 0x1F) { + audioObjectType = 32 + decoderConfigBitArray.readBits(6); + } + codecId += ".40." + audioObjectType; + } + return Util.formatInvariant("iamf.%03X.%03X.%s", primaryProfile, additionalProfile, codecId); + } + /** * Returns whether the CEA-708 closed caption service with the given initialization data is * formatted for displays with 16:9 aspect ratio. @@ -354,6 +397,8 @@ public static Pair getCodecProfileAndLevel(Format format) { return getAacCodecProfileAndLevel(format.codecs, parts); case CODEC_ID_AC4: return getAc4CodecProfileAndLevel(format.codecs, parts); + case CODEC_ID_IAMF: + return getIamfCodecProfileAndLevel(format.codecs, parts); default: return null; } @@ -802,6 +847,49 @@ private static Pair getAc4CodecProfileAndLevel(String codec, S return new Pair<>(profile, level); } + @Nullable + private static Pair getIamfCodecProfileAndLevel(String codec, String[] parts) { + if (parts.length < 4) { + Log.w(TAG, "Ignoring malformed IAMF codec string: " + codec); + return null; + } + + int primaryProfileValue; + try { + primaryProfileValue = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed primary profile in IAMF codec string: " + parts[1], e); + return null; + } + + int profileBitmask = 0x1 << (16 + primaryProfileValue); + int versionBitmask = 0x1 << 24; + int auxiliaryProfileValue = 0; + + switch (parts[3]) { + case "Opus": + auxiliaryProfileValue = 0x1; // Bit 0 + break; + case "mp4a": + auxiliaryProfileValue = 0x1 << 1; // Bit 1 + break; + case "fLaC": + auxiliaryProfileValue = 0x1 << 2; // Bit 2 + break; + case "ipcm": + auxiliaryProfileValue = 0x1 << 3; // Bit 3 + break; + default: + Log.w(TAG, "Ignoring unknown codec identifier for IAMF auxiliary profile: " + parts[3]); + return null; + } + // IAMF profiles are defined as the combination of (listed from LSB to MSB): + // - audio codec (2 bytes) + // - profile (1 byte, offset 16) + // - specification version (1 byte, offset 24) + return new Pair<>(versionBitmask | profileBitmask | auxiliaryProfileValue, /* level= */ 0); + } + private static int avcProfileNumberToConst(int profileNumber) { switch (profileNumber) { case 66: diff --git a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java index e2f69e13eb..86daa514f8 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java @@ -182,7 +182,7 @@ public static float[] createVertexBuffer(List vertexList) { * *

If {@code true}, the device supports a protected output path for DRM content when using GL. */ - public static boolean isProtectedContentExtensionSupported(Context context) { + public static boolean isProtectedContentExtensionSupported(Context context) throws GlException { if (SDK_INT < 24) { return false; } @@ -211,7 +211,7 @@ public static boolean isProtectedContentExtensionSupported(Context context) { * surfaces in a call to {@link EGL14#eglMakeCurrent(EGLDisplay, EGLSurface, EGLSurface, * EGLContext)}. */ - public static boolean isSurfacelessContextExtensionSupported() { + public static boolean isSurfacelessContextExtensionSupported() throws GlException { return isExtensionSupported(EXTENSION_SURFACELESS_CONTEXT); } @@ -242,7 +242,8 @@ public static boolean isYuvTargetExtensionSupported() { } /** Returns whether the given {@link C.ColorTransfer} is supported. */ - public static boolean isColorTransferSupported(@C.ColorTransfer int colorTransfer) { + public static boolean isColorTransferSupported(@C.ColorTransfer int colorTransfer) + throws GlException { if (colorTransfer == C.COLOR_TRANSFER_ST2084) { return GlUtil.isBt2020PqExtensionSupported(); } else if (colorTransfer == C.COLOR_TRANSFER_HLG) { @@ -252,14 +253,14 @@ public static boolean isColorTransferSupported(@C.ColorTransfer int colorTransfe } /** Returns whether {@link #EXTENSION_COLORSPACE_BT2020_PQ} is supported. */ - public static boolean isBt2020PqExtensionSupported() { + public static boolean isBt2020PqExtensionSupported() throws GlException { // On API<33, the system cannot display PQ content correctly regardless of whether BT2020 PQ // GL extension is supported. Context: http://b/252537203#comment5. return SDK_INT >= 33 && isExtensionSupported(EXTENSION_COLORSPACE_BT2020_PQ); } /** Returns whether {@link #EXTENSION_COLORSPACE_BT2020_HLG} is supported. */ - public static boolean isBt2020HlgExtensionSupported() { + public static boolean isBt2020HlgExtensionSupported() throws GlException { return isExtensionSupported(EXTENSION_COLORSPACE_BT2020_HLG); } @@ -1081,8 +1082,8 @@ private static EGLConfig getEglConfig(EGLDisplay eglDisplay, int[] attributes) return eglConfigs[0]; } - private static boolean isExtensionSupported(String extensionName) { - EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + private static boolean isExtensionSupported(String extensionName) throws GlException { + EGLDisplay display = getDefaultEglDisplay(); @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); return eglExtensions != null && eglExtensions.contains(extensionName); } diff --git a/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java index 40289d9e2f..50fd16408d 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java @@ -181,7 +181,11 @@ public static MediaFormat createMediaFormatFromFormat(Format format) { maybeSetPixelAspectRatio(result, format.pixelWidthHeightRatio); if (format.id != null) { - result.setInteger(MediaFormat.KEY_TRACK_ID, Integer.parseInt(format.id)); + try { + result.setInteger(MediaFormat.KEY_TRACK_ID, Integer.parseInt(format.id)); + } catch (NumberFormatException e) { + // Ignore, format.id is not always an integer, but KEY_TRACK_ID expects an int. + } } return result; } 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 d1b2cf00c2..1cd686f45b 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 @@ -659,7 +659,8 @@ public long readUtf8EncodedLong() { } /** - * Reads a little endian long of variable length. + * Reads an unsigned variable-length LEB128 + * value into a long. * * @throws IllegalStateException if the byte to be read is over the limit of the parsable byte * array @@ -683,7 +684,8 @@ public long readUnsignedLeb128ToLong() { } /** - * Reads a little endian integer of variable length. + * Reads an unsigned variable-length LEB128 + * value into an int. * * @throws IllegalArgumentException if the read value is greater than {@link Integer#MAX_VALUE} or * less than {@link Integer#MIN_VALUE} @@ -693,6 +695,11 @@ public int readUnsignedLeb128ToInt() { return Ints.checkedCast(readUnsignedLeb128ToLong()); } + /** Skips a variable-length LEB128 value. */ + public void skipLeb128() { + while ((readUnsignedByte() & 0x80) != 0) {} + } + /** * 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. 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 f503fb690a..2ec0574d72 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 @@ -190,7 +190,7 @@ public final class Util { private static final String TAG = "Util"; private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( - "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" + "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt ]" + "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?" + "([Zz]|((\\+|\\-)(\\d?\\d):?(\\d\\d)))?"); private static final Pattern XS_DURATION_PATTERN = @@ -2401,6 +2401,7 @@ public static int getApiLevelThatAudioFormatIntroducedAudioEncoding(int encoding return 28; case C.ENCODING_OPUS: return 30; + case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_32BIT: return 31; case C.ENCODING_DTS_UHD_P2: diff --git a/libraries/common/src/test/java/androidx/media3/common/AudioAttributesTest.java b/libraries/common/src/test/java/androidx/media3/common/AudioAttributesTest.java index 38144bdaca..13e5bbd505 100644 --- a/libraries/common/src/test/java/androidx/media3/common/AudioAttributesTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/AudioAttributesTest.java @@ -34,6 +34,7 @@ public void roundTripViaBundle_yieldsEqualInstance() { .setUsage(C.USAGE_ALARM) .setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_SYSTEM) .setSpatializationBehavior(C.SPATIALIZATION_BEHAVIOR_NEVER) + .setIsContentSpatialized(true) .build(); assertThat(AudioAttributes.fromBundle(audioAttributes.toBundle())).isEqualTo(audioAttributes); 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 7fd5b97bb2..cda440568a 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java @@ -27,7 +27,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -209,7 +208,7 @@ public void builderSetDrmSessionForClearPeriods_setsAudioAndVideoTracks() { .setUri(URI_STRING) .setDrmUuid(C.WIDEVINE_UUID) .setDrmLicenseUri(licenseUri) - .setDrmSessionForClearTypes(Arrays.asList(C.TRACK_TYPE_AUDIO)) + .setDrmSessionForClearTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO)) .setDrmSessionForClearPeriods(true) .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 363eb45290..608022d14b 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -1199,6 +1199,93 @@ protected State getState() { assertThat(player.getCurrentMediaItemIndex()).isEqualTo(4); } + @Test + public void getTimeline_withoutExplicitPeriodData_returnsCorrectValues() { + Commands commands = new Commands.Builder().addAll(Player.COMMAND_GET_TIMELINE).build(); + Object mediaItemUid = new Object(); + 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 playlist = + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid) + .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) + .build()); + State state = new State.Builder().setAvailableCommands(commands).setPlaylist(playlist).build(); + + Player player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + }; + 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(mediaItemUid); + 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(mediaItemUid); + assertThat(period.adPlaybackState).isEqualTo(AdPlaybackState.NONE); + } + @Test public void getCurrentMediaItemIndex_withUnsetIndexInState_returnsDefaultIndex() { State state = new State.Builder().setCurrentMediaItemIndex(C.INDEX_UNSET).build(); diff --git a/libraries/common/src/test/java/androidx/media3/common/util/CodecSpecificDataUtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/CodecSpecificDataUtilTest.java index 4708f05d21..3d32764641 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/CodecSpecificDataUtilTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/CodecSpecificDataUtilTest.java @@ -198,6 +198,70 @@ public void getCodecProfileAndLevel_handlesMvHevcCodecString() { MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel4); } + @Test + public void getCodecProfileAndLevel_handlesIamfCodecString_forSimpleProfileOpus() { + // TODO(b/426125651): Replace iamf simple profile value with + // MediaCodecInfo.CodecProfileLevel.IAMFProfileSimpleOpus + assertCodecProfileAndLevelForCodecsString( + MimeTypes.AUDIO_IAMF, "iamf.000.000.Opus", /* Simple profile= */ 0x1010001, 0); + } + + @Test + public void getCodecProfileAndLevel_handlesIamfCodecString_forSimpleProfileAac() { + // TODO(b/426125651): Replace iamf simple profile value with + // MediaCodecInfo.CodecProfileLevel.IAMFProfileSimpleAac + assertCodecProfileAndLevelForCodecsString( + MimeTypes.AUDIO_IAMF, "iamf.000.000.mp4a.40.2", /* Simple profile= */ 0x1010002, 0); + } + + @Test + public void getCodecProfileAndLevel_handlesIamfCodecString_forSimpleProfileFlac() { + // TODO(b/426125651): Replace iamf simple profile value with + // MediaCodecInfo.CodecProfileLevel.IAMFProfileSimpleFlac + assertCodecProfileAndLevelForCodecsString( + MimeTypes.AUDIO_IAMF, "iamf.000.000.fLaC", /* Simple profile= */ 0x1010004, 0); + } + + @Test + public void getCodecProfileAndLevel_handlesIamfCodecString_forSimpleProfileIpcm() { + // TODO(b/426125651): Replace iamf simple profile value with + // MediaCodecInfo.CodecProfileLevel.IAMFProfileSimpleIpcm + assertCodecProfileAndLevelForCodecsString( + MimeTypes.AUDIO_IAMF, "iamf.000.000.ipcm", /* Simple profile= */ 0x1010008, 0); + } + + @Test + public void getCodecProfileAndLevel_handlesIamfCodecString_forBaseProfileOpus() { + // TODO(b/426125651): Replace iamf base profile value with + // MediaCodecInfo.CodecProfileLevel.IAMFProfileBaseOpus + assertCodecProfileAndLevelForCodecsString( + MimeTypes.AUDIO_IAMF, "iamf.001.000.Opus", /* Base profile= */ 0x1020001, 0); + } + + @Test + public void getCodecProfileAndLevel_handlesIamfCodecString_forBaseProfileAac() { + // TODO(b/426125651): Replace iamf base profile value with + // MediaCodecInfo.CodecProfileLevel.IAMFProfileBaseAac + assertCodecProfileAndLevelForCodecsString( + MimeTypes.AUDIO_IAMF, "iamf.001.000.mp4a.40.2", /* Base profile= */ 0x1020002, 0); + } + + @Test + public void getCodecProfileAndLevel_handlesIamfCodecString_forBaseProfileFlac() { + // TODO(b/426125651): Replace iamf base profile value with + // MediaCodecInfo.CodecProfileLevel.IAMFProfileBaseFlac + assertCodecProfileAndLevelForCodecsString( + MimeTypes.AUDIO_IAMF, "iamf.001.000.fLaC", /* Base profile= */ 0x1020004, 0); + } + + @Test + public void getCodecProfileAndLevel_handlesIamfCodecString_forBaseProfileIpcm() { + // TODO(b/426125651): Replace iamf base profile value with + // MediaCodecInfo.CodecProfileLevel.IAMFProfileBaseIpcm + assertCodecProfileAndLevelForCodecsString( + MimeTypes.AUDIO_IAMF, "iamf.001.000.ipcm", /* Base profile= */ 0x1020008, 0); + } + private static void assertCodecProfileAndLevelForCodecsString( String sampleMimeType, String codecs, int profile, int level) { Format format = diff --git a/libraries/common/src/test/java/androidx/media3/common/util/MediaFormatUtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/MediaFormatUtilTest.java index 7bd10d7ff4..54fb69fff7 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/MediaFormatUtilTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/MediaFormatUtilTest.java @@ -240,4 +240,13 @@ public void createMediaFormatFromFormat_withCustomPcmEncoding_setsCustomPcmEncod .isEqualTo(C.ENCODING_PCM_16BIT_BIG_ENDIAN); assertThat(mediaFormat.containsKey(MediaFormat.KEY_PCM_ENCODING)).isFalse(); } + + @Test + public void createMediaFormatFromFormat_withNonIntegerFormatId_doesNotSetTrackId() { + Format format = new Format.Builder().setId("1/256").build(); + + MediaFormat mediaFormat = MediaFormatUtil.createMediaFormatFromFormat(format); + + assertThat(mediaFormat.containsKey(MediaFormat.KEY_TRACK_ID)).isFalse(); + } } 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 22bea475ee..90af5e8078 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 @@ -17,7 +17,6 @@ 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 java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_16; import static java.nio.charset.StandardCharsets.UTF_16BE; @@ -466,7 +465,7 @@ 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.readString(data.length, US_ASCII)).isEqualTo("test"); assertThat(testArray.getPosition()).isEqualTo(data.length); } diff --git a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java index 9a3096b336..0a913a7648 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java @@ -1623,10 +1623,7 @@ public void contentEquals_oneNullSparseArrayAndOneNonNullSparseArray_returnsFals } @Test - @Config( - minSdk = - Config.OLDEST_SDK) // Specifies the minimum SDK to enforce the test to run with all API - // levels. + @Config(minSdk = 21) // Specifies the minimum SDK to enforce the test to run with all API levels. public void contentEquals_sparseArraysWithEqualContent_returnsTrue() { SparseArray sparseArray1 = new SparseArray<>(); sparseArray1.put(1, 2); @@ -1639,10 +1636,7 @@ public void contentEquals_sparseArraysWithEqualContent_returnsTrue() { } @Test - @Config( - minSdk = - Config.OLDEST_SDK) // Specifies the minimum SDK to enforce the test to run with all API - // levels. + @Config(minSdk = 21) // Specifies the minimum SDK to enforce the test to run with all API levels. public void contentEquals_sparseArraysWithDifferentContents_returnsFalse() { SparseArray sparseArray1 = new SparseArray<>(); sparseArray1.put(1, 2); @@ -1658,10 +1652,7 @@ public void contentEquals_sparseArraysWithDifferentContents_returnsFalse() { } @Test - @Config( - minSdk = - Config.OLDEST_SDK) // Specifies the minimum SDK to enforce the test to run with all API - // levels. + @Config(minSdk = 21) // Specifies the minimum SDK to enforce the test to run with all API levels. public void contentHashCode_sparseArraysWithEqualContent_returnsEqualContentHashCode() { SparseArray sparseArray1 = new SparseArray<>(); sparseArray1.put(1, 2); @@ -1674,10 +1665,7 @@ public void contentHashCode_sparseArraysWithEqualContent_returnsEqualContentHash } @Test - @Config( - minSdk = - Config.OLDEST_SDK) // Specifies the minimum SDK to enforce the test to run with all API - // levels. + @Config(minSdk = 21) // Specifies the minimum SDK to enforce the test to run with all API levels. public void contentHashCode_sparseArraysWithDifferentContent_returnsDifferentContentHashCode() { // In theory this is not guaranteed though, adding this test to ensure a sensible // contentHashCode implementation. diff --git a/libraries/common_ktx/src/main/java/androidx/media3/common/PlayerExtensions.kt b/libraries/common_ktx/src/main/java/androidx/media3/common/PlayerExtensions.kt index 4598800dfa..3a8e43c74d 100644 --- a/libraries/common_ktx/src/main/java/androidx/media3/common/PlayerExtensions.kt +++ b/libraries/common_ktx/src/main/java/androidx/media3/common/PlayerExtensions.kt @@ -23,8 +23,6 @@ import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.resumeWithException import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.android.asCoroutineDispatcher -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @@ -67,21 +65,22 @@ suspend fun Player.listen(onEvents: Player.(Player.Events) -> Unit): Nothing { * * marking the listener as cancelled using `AtomicBoolean` to ensure that this value will be * visible immediately on any non-calling thread due to a memory barrier. * - * A note on [callbackFlow] vs [suspendCancellableCoroutine]: + * A note on [kotlinx.coroutines.flow.callbackFlow] vs [suspendCancellableCoroutine]: * - * Despite [callbackFlow] being recommended for a multi-shot API (like [Player]'s), a - * [suspendCancellableCoroutine] is a lower-level construct that allows us to overcome the - * limitations of [Flow]'s buffered dispatch. In our case, we will not be waiting for a particular - * callback to resume the continuation (i.e. the common single-shot use of - * [suspendCancellableCoroutine]), but rather handle incoming Events indefinitely. This approach - * controls the timing of dispatching events to the caller more tightly than [Flow]s. Such timing - * guarantees are critical for responding to events with frame-perfect timing and become more - * relevant in the context of front-end UI development (e.g. using Compose). + * Despite [kotlinx.coroutines.flow.callbackFlow] being recommended for a multi-shot API (like + * [Player]'s), a [suspendCancellableCoroutine] is a lower-level construct that allows us to + * overcome the limitations of [kotlinx.coroutines.flow.Flow]'s buffered dispatch. In our case, we + * will not be waiting for a particular callback to resume the continuation (i.e. the common + * single-shot use of [suspendCancellableCoroutine]), but rather handle incoming Events + * indefinitely. This approach controls the timing of dispatching events to the caller more tightly + * than [kotlinx.coroutines.flow.Flow]s. Such timing guarantees are critical for responding to + * events with frame-perfect timing and become more relevant in the context of front-end UI + * development (e.g. using Compose). */ private suspend fun Player.listenImpl(onEvents: Player.(Player.Events) -> Unit): Nothing { lateinit var listener: PlayerListener try { - suspendCancellableCoroutine { continuation -> + suspendCancellableCoroutine { continuation -> listener = PlayerListener(onEvents, continuation) continuation.invokeOnCancellation { listener.isCancelled.set(true) } addListener(listener) diff --git a/libraries/container/src/main/java/androidx/media3/container/Mp4Box.java b/libraries/container/src/main/java/androidx/media3/container/Mp4Box.java index 93d095f7b5..73067e7876 100644 --- a/libraries/container/src/main/java/androidx/media3/container/Mp4Box.java +++ b/libraries/container/src/main/java/androidx/media3/container/Mp4Box.java @@ -335,6 +335,9 @@ public abstract class Mp4Box { @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_mp4v = 0x6d703476; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mp4s = 0x6D703473; + @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_stts = 0x73747473; diff --git a/libraries/decoder_dav1d/README.md b/libraries/decoder_dav1d/README.md deleted file mode 100644 index 5b52d8603a..0000000000 --- a/libraries/decoder_dav1d/README.md +++ /dev/null @@ -1,124 +0,0 @@ -Note: This dav1d extension module is currently in a temporary location. It is -planned to replace the existing AV1 extension (decoder-av1) in the near future. - -# dav1d decoder module - -The dav1d module provides `Libdav1dVideoRenderer`, which uses dav1d native -library to decode AV1 videos. - -## License note - -Please note that whilst the code in this repository is licensed under -[Apache 2.0][], using this module also requires building and including one or -more external libraries as described below. These are licensed separately. - -[Apache 2.0]: ../../LICENSE - -## Build instructions (Linux, macOS) - -To use the module you need to clone this GitHub project and depend on its -modules locally. Instructions for doing this can be found in the -[top level README][]. - -In addition, it's necessary to fetch `cpu_features` library and manually build -the `dav1d` library, so that gradle can bundle the `dav1d` binaries in the APK: - -* Set the following environment variables: - -``` -cd "" -DAV1D_MODULE_PATH="$(pwd)/libraries/decoder_dav1d/src/main" -``` - -* Fetch `cpu_features` library: - -``` -cd "${DAV1D_MODULE_PATH}/jni" && \ -git clone https://github.com/google/cpu_features -``` - -* Install [Meson][] (0.49 or higher), [Ninja][], and, for x86* targets, - [nasm][] (2.14 or higher) - -* Fetch `dav1d` library and enter it: - -``` -cd "${DAV1D_MODULE_PATH}/jni" && \ -git clone https://code.videolan.org/videolan/dav1d.git && \ -cd dav1d -``` - -* Set path to your cross-compilation architecture file from the - `package/crossfiles` directory: - -``` -CROSS_FILE_PATH="$(pwd)/package/crossfiles/ -``` - -Note: Binary name formats may differ between distributions. Verify the names, -and use alias if certain binaries cannot be found. - -* Create and enter the build directory: - -``` -mkdir build && \ -cd build -``` - -* Compile the library: - -``` -meson setup .. --cross-file="${CROSS_FILE_PATH}" --default-library=static && \ -ninja -``` - -* The resulting `.a` static library will be located inside `build/src` - -[top level README]: ../../README.md -[Meson]: https://mesonbuild.com/ -[Ninja]: https://ninja-build.org/ -[nasm]: https://nasm.us/ - -## Build instructions (Windows) - -We do not provide support for building this module on Windows, however it should -be possible to follow the Linux instructions in [Windows PowerShell][]. - -[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell - -## Using the module with ExoPlayer - -Once you've followed the instructions above to check out, build and depend on -the module, the next step is to tell ExoPlayer to use `Libdav1dVideoRenderer`. -How you do this depends on which player API you're using: - -* If you're passing a `DefaultRenderersFactory` to `ExoPlayer.Builder`, you - can enable using the module by setting the `extensionRendererMode` parameter - of the `DefaultRenderersFactory` constructor to - `EXTENSION_RENDERER_MODE_ON`. This will use `Libdav1dVideoRenderer` for - playback if `MediaCodecVideoRenderer` doesn't support decoding the input AV1 - stream. Pass `EXTENSION_RENDERER_MODE_PREFER` to give - `Libdav1dVideoRenderer` priority over `MediaCodecVideoRenderer`. -* If you've subclassed `DefaultRenderersFactory`, add a - `Libdav1dVideoRenderer` to the output list in `buildVideoRenderers`. - ExoPlayer will use the first `Renderer` in the list that supports the input - media format. -* If you've implemented your own `RenderersFactory`, return a - `Libdav1dVideoRenderer` instance from `createRenderers`. ExoPlayer will use - the first `Renderer` in the returned array that supports the input media - format. -* If you're using `ExoPlayer.Builder`, pass a `Libdav1dVideoRenderer` in the - array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list - that supports the input media format. - -Note: These instructions assume you're using `DefaultTrackSelector`. If you have -a custom track selector the choice of `Renderer` is up to your implementation. -You need to make sure you are passing a `Libdav1dVideoRenderer` to the player -and then you need to implement your own logic to use the renderer for a given -track. - -## Links - -* [Troubleshooting using decoding extensions][] - -[Troubleshooting using decoding extensions]: https://developer.android.com/media/media3/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playback diff --git a/libraries/decoder_dav1d/build.gradle b/libraries/decoder_dav1d/build.gradle deleted file mode 100644 index 84c838aadf..0000000000 --- a/libraries/decoder_dav1d/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2024 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. -apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" - -android { - namespace 'androidx.media3.decoder.dav1d' - - defaultConfig { - externalNativeBuild { - cmake { - targets "dav1dJNI" - } - } - } -} - -// Configure the native build only if dav1d is present to avoid gradle sync -// failures if dav1d hasn't been built according to the README instructions. -if (project.file('src/main/jni/dav1d').exists()) { - android.externalNativeBuild.cmake { - path = 'src/main/jni/CMakeLists.txt' - version = '3.21.0+' - if (project.hasProperty('externalNativeBuildDir')) { - if (!new File(externalNativeBuildDir).isAbsolute()) { - ext.externalNativeBuildDir = - new File(rootDir, it.externalNativeBuildDir) - } - buildStagingDirectory = "${externalNativeBuildDir}/${project.name}" - } - } -} - -dependencies { - api project(modulePrefix + 'lib-decoder') - // TODO(b/203752526): Remove this dependency. - implementation project(modulePrefix + 'lib-exoplayer') - implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion - compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion - compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion - testImplementation project(modulePrefix + 'test-utils') - testImplementation 'org.robolectric:robolectric:' + robolectricVersion - androidTestImplementation project(modulePrefix + 'test-utils') - androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion - androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion -} diff --git a/libraries/decoder_dav1d/proguard-rules.txt b/libraries/decoder_dav1d/proguard-rules.txt deleted file mode 100644 index 2311c0b267..0000000000 --- a/libraries/decoder_dav1d/proguard-rules.txt +++ /dev/null @@ -1,20 +0,0 @@ -# Proguard rules specific to the dav1d extension. - -# This prevents the names of native methods from being obfuscated. --keepclasseswithmembernames class * { - native ; -} - -# Some members of this class are being accessed from native methods. Keep them unobfuscated. --keep class androidx.media3.decoder.VideoDecoderOutputBuffer { - *; -} --keep class androidx.media3.decoder.DecoderInputBuffer { - *; -} --keep class androidx.media3.decoder.dav1d.Dav1dDecoder { - *; -} --keep class androidx.media3.common.Format { - *; -} diff --git a/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Dav1dDecoder.java b/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Dav1dDecoder.java deleted file mode 100644 index 70e6c013c4..0000000000 --- a/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Dav1dDecoder.java +++ /dev/null @@ -1,583 +0,0 @@ -/* - * Copyright 2024 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.decoder.dav1d; - -import android.os.Build.VERSION_CODES; -import android.view.Surface; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.media3.common.C; -import androidx.media3.common.Format; -import androidx.media3.common.util.Assertions; -import androidx.media3.common.util.UnstableApi; -import androidx.media3.common.util.Util; -import androidx.media3.decoder.Decoder; -import androidx.media3.decoder.DecoderInputBuffer; -import androidx.media3.decoder.VideoDecoderOutputBuffer; -import com.google.errorprone.annotations.concurrent.GuardedBy; -import java.nio.ByteBuffer; -import java.util.ArrayDeque; - -/** dAV1d decoder. */ -@UnstableApi -public final class Dav1dDecoder - implements Decoder { - - // LINT.IfChange - private static final int DAV1D_ERROR = 0; - private static final int DAV1D_OK = 1; - private static final int DAV1D_DECODE_ONLY = 2; - private static final int DAV1D_EAGAIN = 3; - // LINT.ThenChange(../../../../../jni/dav1d_jni.cc) - - private final Thread decodeThread; - - private final Object lock; - - @GuardedBy("lock") - private final ArrayDeque queuedInputBuffers; - - @GuardedBy("lock") - private final ArrayDeque queuedOutputBuffers; - - @GuardedBy("lock") - private final DecoderInputBuffer[] availableInputBuffers; - - @GuardedBy("lock") - private final VideoDecoderOutputBuffer[] availableOutputBuffers; - - private long dav1dDecoderContext; - - private volatile @C.VideoOutputMode int outputMode; - - @GuardedBy("lock") - private int availableInputBufferCount; - - @GuardedBy("lock") - private int availableOutputBufferCount; - - @GuardedBy("lock") - @Nullable - private DecoderInputBuffer dequeuedInputBuffer; - - @GuardedBy("lock") - @Nullable - private Dav1dDecoderException exception; - - @GuardedBy("lock") - private boolean flushed; - - @GuardedBy("lock") - private boolean released; - - @GuardedBy("lock") - private int skippedOutputBufferCount; - - @GuardedBy("lock") - private long outputStartTimeUs; - - @Nullable private Surface surface; - - /** - * Creates a Dav1dDecoder. - * - * @param numInputBuffers Number of input buffers. - * @param numOutputBuffers Number of output buffers. - * @param initialInputBufferSize The initial size of each input buffer, in bytes. - * @param threads Number of threads libdav1d will use to decode. If {@link - * Libdav1dVideoRenderer#THREAD_COUNT_DECODER_DEFAULT} is passed, then this class use the - * default decoder behavior for setting the threads. - * @param maxFrameDelay Maximum amount of frames libdav1d can be behind on. - * @param useCustomAllocator Whether to use a custom allocator for the decoder. - * @throws Dav1dDecoderException Thrown if an exception occurs when initializing the decoder. - */ - // Suppressing nulless for UnderInitialization and method.invocation. - @SuppressWarnings("nullness") - public Dav1dDecoder( - int numInputBuffers, - int numOutputBuffers, - int initialInputBufferSize, - int threads, - int maxFrameDelay, - boolean useCustomAllocator) - throws Dav1dDecoderException { - if (!Dav1dLibrary.isAvailable()) { - throw new Dav1dDecoderException("Failed to load decoder native library."); - } - lock = new Object(); - outputStartTimeUs = C.TIME_UNSET; - queuedInputBuffers = new ArrayDeque<>(); - queuedOutputBuffers = new ArrayDeque<>(); - availableInputBuffers = new DecoderInputBuffer[numInputBuffers]; - availableInputBufferCount = numInputBuffers; - for (int i = 0; i < availableInputBufferCount; i++) { - availableInputBuffers[i] = - new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); - availableInputBuffers[i].ensureSpaceForWrite(initialInputBufferSize); - } - availableOutputBuffers = new VideoDecoderOutputBuffer[numOutputBuffers]; - availableOutputBufferCount = numOutputBuffers; - for (int i = 0; i < availableOutputBufferCount; i++) { - availableOutputBuffers[i] = new VideoDecoderOutputBuffer(this::releaseOutputBuffer); - } - decodeThread = - new Thread("ExoPlayer:Dav1dDecoder") { - @Override - public void run() { - Dav1dDecoder.this.dav1dDecoderContext = - dav1dInit(threads, maxFrameDelay, useCustomAllocator); - if (dav1dCheckError(Dav1dDecoder.this.dav1dDecoderContext) == DAV1D_ERROR) { - synchronized (lock) { - Dav1dDecoder.this.exception = - new Dav1dDecoderException( - "Failed to initialize decoder. Error: " - + dav1dGetErrorMessage(Dav1dDecoder.this.dav1dDecoderContext)); - } - dav1dClose(Dav1dDecoder.this.dav1dDecoderContext); - return; - } - Dav1dDecoder.this.run(); - releaseUnusedInputBuffers(Dav1dDecoder.this.dav1dDecoderContext, Dav1dDecoder.this); - dav1dClose(Dav1dDecoder.this.dav1dDecoderContext); - } - }; - decodeThread.start(); - maybeThrowException(); - } - - @Override - public String getName() { - return "libdav1d"; - } - - @Override - @Nullable - public final DecoderInputBuffer dequeueInputBuffer() throws Dav1dDecoderException { - synchronized (lock) { - maybeThrowException(); - Assertions.checkState(dequeuedInputBuffer == null || flushed); - dequeuedInputBuffer = - availableInputBufferCount == 0 || flushed - ? null - : availableInputBuffers[--availableInputBufferCount]; - return dequeuedInputBuffer; - } - } - - @Override - public final void queueInputBuffer(DecoderInputBuffer inputBuffer) throws Dav1dDecoderException { - synchronized (lock) { - maybeThrowException(); - Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); - queuedInputBuffers.addLast(inputBuffer); - maybeNotifyDecodeLoop(); - dequeuedInputBuffer = null; - } - } - - @Override - @Nullable - public final VideoDecoderOutputBuffer dequeueOutputBuffer() throws Dav1dDecoderException { - synchronized (lock) { - maybeThrowException(); - if (queuedOutputBuffers.isEmpty() || flushed) { - return null; - } - return queuedOutputBuffers.removeFirst(); - } - } - - @Override - public final void flush() { - synchronized (lock) { - flushed = true; - lock.notify(); - } - } - - @Override - public final void setOutputStartTimeUs(long outputStartTimeUs) { - synchronized (lock) { - this.outputStartTimeUs = outputStartTimeUs; - } - } - - @Override - @RequiresApi(VERSION_CODES.M) - public void release() { - synchronized (lock) { - released = true; - lock.notify(); - } - try { - decodeThread.join(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - /** - * Sets the output mode for frames rendered by the decoder. - * - * @param outputMode The output mode. - */ - public void setOutputMode(@C.VideoOutputMode int outputMode) { - this.outputMode = outputMode; - } - - /** - * Renders output buffer to the given surface. Must only be called when in {@link - * C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode. - * - * @param outputBuffer Output buffer. - * @param surface Output surface. - * @throws Dav1dDecoderException Thrown if called with invalid output mode or frame rendering - * fails. - */ - public void renderToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface) - throws Dav1dDecoderException { - if (outputMode != C.VIDEO_OUTPUT_MODE_SURFACE_YUV) { - throw new Dav1dDecoderException("Unsupported Output Mode."); - } - int error = dav1dRenderFrame(dav1dDecoderContext, surface, outputBuffer); - if (error != DAV1D_OK) { - throw new Dav1dDecoderException("Failed to render output buffer to surface."); - } - } - - /* package */ - void releaseOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { - synchronized (lock) { - dav1dReleaseFrame(dav1dDecoderContext, outputBuffer); - releaseOutputBufferInternal(outputBuffer); - maybeNotifyDecodeLoop(); - } - } - - /* package */ - final boolean isAtLeastOutputStartTimeUs(long timeUs) { - synchronized (lock) { - return outputStartTimeUs == C.TIME_UNSET || timeUs >= outputStartTimeUs; - } - } - - /* package */ - Dav1dDecoderException createUnexpectedDecodeException(Throwable error) { - return new Dav1dDecoderException("Unexpected decode error", error); - } - - // Setting and checking deprecated decode-only flag for compatibility with custom decoders that - // are still using it. - private boolean decode() throws InterruptedException { - DecoderInputBuffer inputBuffer; - VideoDecoderOutputBuffer outputBuffer; - // Wait until we have an input and output buffer to decode. - synchronized (lock) { - if (flushed) { - flushInternal(); - } - while (!released && !(canDecodeInputBuffer() && canDecodeOutputBuffer()) && !flushed) { - lock.wait(); - } - if (released) { - return false; - } - if (flushed) { - // Flushed may have changed after lock.wait() is finished. - flushInternal(); - // Queued Input Buffers have been cleared, there is no data to decode. - return true; - } - inputBuffer = queuedInputBuffers.removeFirst(); - outputBuffer = availableOutputBuffers[--availableOutputBufferCount]; - } - - if (inputBuffer.isEndOfStream()) { - outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); - releaseInputBuffer(inputBuffer); - synchronized (lock) { - if (flushed) { - outputBuffer.release(); - flushInternal(); - } else { - outputBuffer.skippedOutputBufferCount = skippedOutputBufferCount; - skippedOutputBufferCount = 0; - queuedOutputBuffers.addLast(outputBuffer); - } - } - } else { - @C.BufferFlags int flags = 0; - boolean decodeOnly = false; - if (!isAtLeastOutputStartTimeUs(inputBuffer.timeUs)) { - decodeOnly = true; - } - if (inputBuffer.isFirstSample()) { - flags |= C.BUFFER_FLAG_FIRST_SAMPLE; - } - @Nullable Dav1dDecoderException exception = null; - try { - ByteBuffer inputData = Util.castNonNull(inputBuffer.data); - int inputOffset = inputData.position(); - int inputSize = inputData.remaining(); - int status = - dav1dDecode( - dav1dDecoderContext, - inputBuffer, - inputOffset, - inputSize, - decodeOnly, - flags, - inputBuffer.timeUs, - outputMode, - inputBuffer.format); - if (status == DAV1D_ERROR) { - throw new Dav1dDecoderException( - "dav1dDecode error: " + dav1dGetErrorMessage(dav1dDecoderContext)); - } - while ((status = dav1dGetFrame(dav1dDecoderContext, outputBuffer)) == DAV1D_OK - || status == DAV1D_DECODE_ONLY) { - if (status == DAV1D_DECODE_ONLY) { - outputBuffer.shouldBeSkipped = true; - } - synchronized (lock) { - if (flushed) { - outputBuffer.release(); - flushInternal(); - break; - } else if (!isAtLeastOutputStartTimeUs(outputBuffer.timeUs) - || outputBuffer.shouldBeSkipped) { - skippedOutputBufferCount++; - outputBuffer.release(); - } else { - outputBuffer.skippedOutputBufferCount = skippedOutputBufferCount; - skippedOutputBufferCount = 0; - queuedOutputBuffers.addLast(outputBuffer); - } - while (!released && !canDecodeOutputBuffer() && !flushed) { - lock.wait(); - } - if (released) { - return false; - } - if (flushed) { - flushInternal(); - return true; - } - outputBuffer = availableOutputBuffers[--availableOutputBufferCount]; - } - } - if (status == DAV1D_ERROR) { - throw new Dav1dDecoderException( - "dav1dGetFrame error: " + dav1dGetErrorMessage(dav1dDecoderContext)); - } else if (status == DAV1D_EAGAIN) { - outputBuffer.release(); - } - } catch (RuntimeException e) { - // This can occur if a sample is malformed in a way that the decoder is not robust against. - // We don't want the process to die in this case, but we do want to propagate the error. - exception = createUnexpectedDecodeException(e); - } catch (OutOfMemoryError e) { - // This can occur if a sample is malformed in a way that causes the decoder to think it - // needs to allocate a large amount of memory. We don't want the process to die in this - // case, but we do want to propagate the error. - exception = createUnexpectedDecodeException(e); - } catch (Dav1dDecoderException e) { - exception = e; - } - if (exception != null) { - synchronized (lock) { - this.exception = exception; - } - return false; - } - } - releaseUnusedInputBuffers(dav1dDecoderContext, this); - return true; - } - - @GuardedBy("lock") - private void maybeThrowException() throws Dav1dDecoderException { - if (this.exception != null) { - throw this.exception; - } - } - - private void releaseInputBuffer(DecoderInputBuffer inputBuffer) { - synchronized (lock) { - releaseInputBufferInternal(inputBuffer); - } - } - - @GuardedBy("lock") - private void releaseInputBufferInternal(DecoderInputBuffer inputBuffer) { - inputBuffer.clear(); - availableInputBuffers[availableInputBufferCount++] = inputBuffer; - } - - @GuardedBy("lock") - private void releaseOutputBufferInternal(VideoDecoderOutputBuffer outputBuffer) { - outputBuffer.clear(); - availableOutputBuffers[availableOutputBufferCount++] = outputBuffer; - } - - @GuardedBy("lock") - private boolean canDecodeInputBuffer() { - return !queuedInputBuffers.isEmpty(); - } - - @GuardedBy("lock") - private boolean canDecodeOutputBuffer() { - return availableOutputBufferCount > 0; - } - - @GuardedBy("lock") - private void maybeNotifyDecodeLoop() { - if (canDecodeInputBuffer() || canDecodeOutputBuffer()) { - lock.notify(); - } - } - - @GuardedBy("lock") - private void flushInternal() { - skippedOutputBufferCount = 0; - if (dequeuedInputBuffer != null) { - releaseInputBufferInternal(dequeuedInputBuffer); - dequeuedInputBuffer = null; - } - while (!queuedInputBuffers.isEmpty()) { - releaseInputBufferInternal(queuedInputBuffers.removeFirst()); - } - while (!queuedOutputBuffers.isEmpty()) { - queuedOutputBuffers.removeFirst().release(); - } - dav1dFlush(dav1dDecoderContext); - flushed = false; - } - - private void run() { - try { - while (decode()) { - // Do nothing. - } - } catch (InterruptedException e) { - // Not expected. - throw new IllegalStateException(e); - } - } - - /** - * Initializes a libdav1d decoder. - * - * @param threads Number of threads to be used by a libdav1d decoder. - * @param maxFrameDelay Max frame delay permitted for libdav1d decoder. - * @param useCustomAllocator Whether to use a custom picture allocator. - * @return The address of the decoder context or {@link #DAV1D_ERROR} if there was an error. - */ - private native long dav1dInit(int threads, int maxFrameDelay, boolean useCustomAllocator); - - /** - * Deallocates the decoder context. - * - * @param context Decoder context. - */ - private native void dav1dClose(long context); - - /** - * Decodes the encoded data passed and gets the resulting frame. - * - * @param context Decoder context. - * @param inputBuffer Encoded input buffer. - * @param inputOffset Offset of the data buffer. - * @param inputSize Length of the data buffer - * @param decodeOnly Whether the input data is decode only. - * @param flags {@link androidx.media3.common.C#BufferFlags} Information about output buffer. - * @param timeUs Time of input data. - * @param outputMode Output mode for output buffer. - * @param format Format for output buffer. - * @return {@link #DAV1D_OK} if successful, {@link #DAV1D_ERROR} if an error occurred, {@link - * #DAV1D_EAGAIN} - */ - private native int dav1dDecode( - long context, - DecoderInputBuffer inputBuffer, - int inputOffset, - int inputSize, - boolean decodeOnly, - @C.BufferFlags int flags, - long timeUs, - @C.VideoOutputMode int outputMode, - @Nullable Format format); - - /** - * Gets the decoded frame. - * - * @param context Decoder context. - * @param outputBuffer Output buffer with the decoded frame. - * @return {@link #DAV1D_OK} if successful, {@link #DAV1D_ERROR} if an error occurred, {@link - * #DAV1D_EAGAIN} if more input data is needed. - */ - private native int dav1dGetFrame(long context, VideoDecoderOutputBuffer outputBuffer); - - /** - * Renders the frame to the surface. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only. - * - * @param context Decoder context. - * @param surface Output surface. - * @param outputBuffer Output buffer with the decoded frame. - * @return {@link #DAV1D_OK} if successful, {@link #DAV1D_ERROR} if an error occurred. - */ - private native int dav1dRenderFrame( - long context, Surface surface, VideoDecoderOutputBuffer outputBuffer); - - /** - * Releases the frame. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only. - * - * @param context Decoder context. - * @param outputBuffer Output buffer. - */ - private native void dav1dReleaseFrame(long context, VideoDecoderOutputBuffer outputBuffer); - - /** - * Returns a human-readable string describing the last error encountered in the given context. - * - * @param context Decoder context. - * @return A string describing the last encountered error. - */ - private native String dav1dGetErrorMessage(long context); - - /** - * Returns whether an error occurred. - * - * @param context Decoder context. - * @return {@link #DAV1D_OK} if there was no error, {@link #DAV1D_ERROR} if an error occurred. - */ - private native int dav1dCheckError(long context); - - /** - * Flushes the decoder. - * - * @param context Decoder context. - */ - private native void dav1dFlush(long context); - - /** - * Release unused input buffers. - * - * @param context Decoder context. - * @param decoder Dav1dDecoder instance. - */ - private native void releaseUnusedInputBuffers(long context, Dav1dDecoder decoder); -} diff --git a/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Dav1dDecoderException.java b/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Dav1dDecoderException.java deleted file mode 100644 index f4585a294d..0000000000 --- a/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Dav1dDecoderException.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2024 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.decoder.dav1d; - -import androidx.media3.common.util.UnstableApi; -import androidx.media3.decoder.DecoderException; - -/** Thrown when a libdav1d decoder error occurs. */ -@UnstableApi -public final class Dav1dDecoderException extends DecoderException { - - /** - * Constructs a {@code Dav1dDecoderException} with the specified message. - * - * @param message The error message. - */ - public Dav1dDecoderException(String message) { - super(message); - } - - /** - * Constructs a {@code Dav1dDecoderException} with the specified message and cause. - * - * @param message The error message. - * @param cause The cause of the exception. - */ - public Dav1dDecoderException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Dav1dLibrary.java b/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Dav1dLibrary.java deleted file mode 100644 index 7d32b52db4..0000000000 --- a/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Dav1dLibrary.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2024 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.decoder.dav1d; - -import androidx.media3.common.MediaLibraryInfo; -import androidx.media3.common.util.LibraryLoader; -import androidx.media3.common.util.UnstableApi; - -/** Configures and queries the underlying native library. */ -@UnstableApi -public final class Dav1dLibrary { - - static { - MediaLibraryInfo.registerModule("media3.decoder.dav1d"); - } - - private static final LibraryLoader LOADER = - new LibraryLoader("dav1dJNI") { - @Override - protected void loadLibrary(String name) { - System.loadLibrary(name); - } - }; - - private Dav1dLibrary() {} - - /** Returns whether the underlying library is available, loading it if necessary. */ - public static boolean isAvailable() { - try { - return LOADER.isAvailable(); - } catch (Exception e) { - return false; - } - } -} diff --git a/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Libdav1dVideoRenderer.java b/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Libdav1dVideoRenderer.java deleted file mode 100644 index abedc54819..0000000000 --- a/libraries/decoder_dav1d/src/main/java/androidx/media3/decoder/dav1d/Libdav1dVideoRenderer.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright 2024 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.decoder.dav1d; - -import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION; - -import android.os.Handler; -import android.view.Surface; -import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.Format; -import androidx.media3.common.MimeTypes; -import androidx.media3.common.util.TraceUtil; -import androidx.media3.common.util.UnstableApi; -import androidx.media3.common.util.Util; -import androidx.media3.decoder.CryptoConfig; -import androidx.media3.decoder.VideoDecoderOutputBuffer; -import androidx.media3.exoplayer.DecoderReuseEvaluation; -import androidx.media3.exoplayer.RendererCapabilities; -import androidx.media3.exoplayer.video.DecoderVideoRenderer; -import androidx.media3.exoplayer.video.VideoRendererEventListener; - -/** Decodes and renders video using libdav1d decoder. */ -@UnstableApi -public class Libdav1dVideoRenderer extends DecoderVideoRenderer { - - // Attempts to use as many threads as is the default for the dav1d library. - public static final int THREAD_COUNT_DECODER_DEFAULT = 0; - // Current default: # of performance cores. - public static final int THREAD_COUNT_PERFORMACE_CORES = -1; - // Current experimental: # of cores / 2. - public static final int THREAD_COUNT_EXPERIMENTAL = -2; - - private static final String TAG = "Libdav1dVideoRenderer"; - private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4; - private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4; - private static final int DEFAULT_MAX_FRAME_DELAY = 2; - - /** - * Default input buffer size in bytes, based on 720p resolution video compressed by a factor of - * two. - */ - private static final int DEFAULT_INPUT_BUFFER_SIZE = - Util.ceilDivide(1280, 64) * Util.ceilDivide(720, 64) * (64 * 64 * 3 / 2) / 2; - - /** The number of input buffers. */ - private final int numInputBuffers; - - /** - * The number of output buffers. The renderer may limit the minimum possible value due to - * requiring multiple output buffers to be dequeued at a time for it to make progress. - */ - private final int numOutputBuffers; - - private final int threads; - - private final int maxFrameDelay; - - private final boolean useCustomAllocator; - - @Nullable private Dav1dDecoder decoder; - - /** - * Creates a new instance. - * - * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer - * can attempt to seamlessly join an ongoing playback. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between - * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - */ - public Libdav1dVideoRenderer( - long allowedJoiningTimeMs, - @Nullable Handler eventHandler, - @Nullable VideoRendererEventListener eventListener, - int maxDroppedFramesToNotify) { - this( - allowedJoiningTimeMs, - eventHandler, - eventListener, - maxDroppedFramesToNotify, - THREAD_COUNT_DECODER_DEFAULT, - DEFAULT_MAX_FRAME_DELAY, - DEFAULT_NUM_OF_INPUT_BUFFERS, - DEFAULT_NUM_OF_OUTPUT_BUFFERS, - /* useCustomAllocator= */ false); - } - - /** - * Creates a new instance. - * - * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer - * can attempt to seamlessly join an ongoing playback. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between - * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - * @param threads Number of threads libdav1d will use to decode. If {@link - * #THREAD_COUNT_DECODER_DEFAULT} is passed, then the number of threads to use is determined - * by the dav1d library. If {@link #THREAD_COUNT_PERFORMACE_CORES} is passed, then the number - * of threads to use is the number of performance cores on the device. If {@link - * #THREAD_COUNT_EXPERIMENTAL} is passed, then the number of threads to use is the number of - * cores on the device divided by 2. - * @param maxFrameDelay Maximum frame delay for libdav1d. - * @param numInputBuffers Number of input buffers. - * @param numOutputBuffers Number of output buffers. - * @param useCustomAllocator Whether to use a custom allocator for libdav1d. - */ - public Libdav1dVideoRenderer( - long allowedJoiningTimeMs, - @Nullable Handler eventHandler, - @Nullable VideoRendererEventListener eventListener, - int maxDroppedFramesToNotify, - int threads, - int maxFrameDelay, - int numInputBuffers, - int numOutputBuffers, - boolean useCustomAllocator) { - super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify); - this.threads = threads; - this.numInputBuffers = numInputBuffers; - this.numOutputBuffers = numOutputBuffers; - this.maxFrameDelay = maxFrameDelay; - this.useCustomAllocator = useCustomAllocator; - } - - @Override - public String getName() { - return TAG; - } - - @Override - public final @Capabilities int supportsFormat(Format format) { - if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType) - || !Dav1dLibrary.isAvailable()) { - return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); - } - if (format.cryptoType != C.CRYPTO_TYPE_NONE) { - return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_DRM); - } - return RendererCapabilities.create( - C.FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED); - } - - /** - * {@inheritDoc} - * - * @hide - */ - @Override - protected final Dav1dDecoder createDecoder(Format format, @Nullable CryptoConfig cryptoConfig) - throws Dav1dDecoderException { - TraceUtil.beginSection("createDav1dDecoder"); - int initialInputBufferSize = - format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; - Dav1dDecoder decoder = - new Dav1dDecoder( - numInputBuffers, - numOutputBuffers, - initialInputBufferSize, - threads, - maxFrameDelay, - useCustomAllocator); - this.decoder = decoder; - TraceUtil.endSection(); - return decoder; - } - - @Override - protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface) - throws Dav1dDecoderException { - if (decoder == null) { - throw new Dav1dDecoderException( - "Failed to render output buffer to surface: decoder is not initialized."); - } - try { - decoder.renderToSurface(outputBuffer, surface); - } finally { - outputBuffer.release(); - } - } - - @Override - protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) { - if (decoder != null) { - decoder.setOutputMode(outputMode); - } - } - - @Override - protected DecoderReuseEvaluation canReuseDecoder( - String decoderName, Format oldFormat, Format newFormat) { - return new DecoderReuseEvaluation( - decoderName, - oldFormat, - newFormat, - REUSE_RESULT_YES_WITHOUT_RECONFIGURATION, - /* discardReasons= */ 0); - } -} diff --git a/libraries/decoder_dav1d/src/main/jni/CMakeLists.txt b/libraries/decoder_dav1d/src/main/jni/CMakeLists.txt deleted file mode 100644 index d15c745452..0000000000 --- a/libraries/decoder_dav1d/src/main/jni/CMakeLists.txt +++ /dev/null @@ -1,70 +0,0 @@ -# -# Copyright 2024 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. -# - -# LINT.IfChange -cmake_minimum_required(VERSION 3.21.0 FATAL_ERROR) -# LINT.ThenChange(../../../build.gradle) - -set(CMAKE_CXX_STANDARD 11) - -project(libdav1dJNI C CXX) - -# Devices using armeabi-v7a are not required to support -# Neon which is why Neon is disabled by default for -# armeabi-v7a build. This flag enables it. -if(${ANDROID_ABI} MATCHES "armeabi-v7a") - add_compile_options("-mfpu=neon") - add_compile_options("-marm") - add_compile_options("-fPIC") -endif() - -string(TOLOWER "${CMAKE_BUILD_TYPE}" build_type) -if(build_type MATCHES "^rel") - add_compile_options("-O2") -endif() - -set(libdav1d_jni_root "${CMAKE_CURRENT_SOURCE_DIR}") - -# Build cpu_features library. -add_subdirectory("${libdav1d_jni_root}/cpu_features" - EXCLUDE_FROM_ALL) - -# Path to compiled static libdav1d. -set(DAV1D_LIB_PATH "${libdav1d_jni_root}/dav1d/build/src/libdav1d.a") - -# Add the include directory from dav1d. -include_directories ("${libdav1d_jni_root}/dav1d/include") - -# Build libdav1dJNI. -add_library(dav1dJNI - SHARED - dav1d_jni.cc - cpu_info.cc - cpu_info.h) - -# Locate NDK log library. -find_library(android_log_lib log) - -# Link libdav1dJNI against used libraries. -target_link_libraries(dav1dJNI - PRIVATE android - PRIVATE cpu_features - PRIVATE ${DAV1D_LIB_PATH} - PRIVATE ${android_log_lib}) - -# Enable 16 KB ELF alignment. -target_link_options(dav1dJNI - PRIVATE "-Wl,-z,max-page-size=16384") diff --git a/libraries/decoder_dav1d/src/main/jni/cpu_info.cc b/libraries/decoder_dav1d/src/main/jni/cpu_info.cc deleted file mode 100644 index b8a4440175..0000000000 --- a/libraries/decoder_dav1d/src/main/jni/cpu_info.cc +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2024 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. - */ -#include "cpu_info.h" // NOLINT - -#include - -#include -#include -#include -#include -#include - -namespace dav1d_jni { - -// Note: The code in this file needs to use the 'long' type because it is the -// return type of the Standard C Library function strtol(). The linter warnings -// are suppressed with NOLINT comments since they are integers at runtime. - -// Returns the number of online processor cores. -int GetNumberOfProcessorsOnline() { - // See https://developer.android.com/ndk/guides/cpu-features. - long num_cpus = sysconf(_SC_NPROCESSORS_ONLN); // NOLINT - if (num_cpus < 0) { - return 0; - } - // It is safe to cast num_cpus to int. sysconf(_SC_NPROCESSORS_ONLN) returns - // the return value of get_nprocs(), which is an int. - return static_cast(num_cpus); -} -// These CPUs support heterogeneous multiprocessing. -#if defined(__arm__) || defined(__aarch64__) - -// A helper function used by GetNumberOfPerformanceCoresOnline(). -// -// Returns the cpuinfo_max_freq value (in kHz) of the given CPU. Returns 0 on -// failure. -long GetCpuinfoMaxFreq(int cpu_index) { // NOLINT - char buffer[128]; - const int rv = snprintf( - buffer, sizeof(buffer), - "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", cpu_index); - if (rv < 0 || rv >= sizeof(buffer)) { - return 0; - } - FILE* file = fopen(buffer, "r"); - if (file == nullptr) { - return 0; - } - char* const str = fgets(buffer, sizeof(buffer), file); - fclose(file); - if (str == nullptr) { - return 0; - } - const long freq = strtol(str, nullptr, 10); // NOLINT - if (freq <= 0 || freq == LONG_MAX) { - return 0; - } - return freq; -} - -// Returns the number of performance CPU cores that are online. The number of -// efficiency CPU cores is subtracted from the total number of CPU cores. Uses -// cpuinfo_max_freq to determine whether a CPU is a performance core or an -// efficiency core. -// -// This function is not perfect. For example, the Snapdragon 632 SoC used in -// Motorola Moto G7 has performance and efficiency cores with the same -// cpuinfo_max_freq but different cpuinfo_min_freq. This function fails to -// differentiate the two kinds of cores and reports all the cores as -// performance cores. -int GetNumberOfPerformanceCoresOnline() { - // Get the online CPU list. Some examples of the online CPU list are: - // "0-7" - // "0" - // "0-1,2,3,4-7" - FILE* file = fopen("/sys/devices/system/cpu/online", "r"); - if (file == nullptr) { - return 0; - } - char online[512]; - char* const str = fgets(online, sizeof(online), file); - fclose(file); - file = nullptr; - if (str == nullptr) { - return 0; - } - - // Count the number of the slowest CPUs. Some SoCs such as Snapdragon 855 - // have performance cores with different max frequencies, so only the slowest - // CPUs are efficiency cores. If we count the number of the fastest CPUs, we - // will fail to count the second fastest performance cores. - long slowest_cpu_freq = LONG_MAX; // NOLINT - int num_slowest_cpus = 0; - int num_cpus = 0; - const char* cp = online; - int range_begin = -1; - while (true) { - char* str_end; - const int cpu = static_cast(strtol(cp, &str_end, 10)); // NOLINT - if (str_end == cp) { - break; - } - cp = str_end; - if (*cp == '-') { - range_begin = cpu; - } else { - if (range_begin == -1) { - range_begin = cpu; - } - - num_cpus += cpu - range_begin + 1; - for (int i = range_begin; i <= cpu; ++i) { - const long freq = GetCpuinfoMaxFreq(i); // NOLINT - if (freq <= 0) { - return 0; - } - if (freq < slowest_cpu_freq) { - slowest_cpu_freq = freq; - num_slowest_cpus = 0; - } - if (freq == slowest_cpu_freq) { - ++num_slowest_cpus; - } - } - - range_begin = -1; - } - if (*cp == '\0') { - break; - } - ++cp; - } - - // If there are faster CPU cores than the slowest CPU cores, exclude the - // slowest CPU cores. - if (num_slowest_cpus < num_cpus) { - num_cpus -= num_slowest_cpus; - } - return num_cpus; -} - -#else - -// Assume symmetric multiprocessing. -int GetNumberOfPerformanceCoresOnline() { - return GetNumberOfProcessorsOnline(); -} - -#endif - -} // namespace dav1d_jni diff --git a/libraries/decoder_dav1d/src/main/jni/cpu_info.h b/libraries/decoder_dav1d/src/main/jni/cpu_info.h deleted file mode 100644 index 7ae4163325..0000000000 --- a/libraries/decoder_dav1d/src/main/jni/cpu_info.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2024 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. - */ -#ifndef EXOPLAYER_V2_EXTENSIONS_DAV1D_SRC_MAIN_JNI_CPU_INFO_H_ -#define EXOPLAYER_V2_EXTENSIONS_DAV1D_SRC_MAIN_JNI_CPU_INFO_H_ - -namespace dav1d_jni { - -// Returns the number of performance cores that are available for AV1 decoding. -// This is a heuristic that works on most common android devices. Returns 0 on -// error or if the number of performance cores cannot be determined. -int GetNumberOfPerformanceCoresOnline(); - -// Returns the number of cores online. Returns 0 on error or if the number of -// cores cannot be determined. -int GetNumberOfProcessorsOnline(); - -} // namespace dav1d_jni - -#endif // EXOPLAYER_V2_EXTENSIONS_DAV1D_SRC_MAIN_JNI_CPU_INFO_H_ diff --git a/libraries/decoder_dav1d/src/main/jni/dav1d_jni.cc b/libraries/decoder_dav1d/src/main/jni/dav1d_jni.cc deleted file mode 100644 index 33781033de..0000000000 --- a/libraries/decoder_dav1d/src/main/jni/dav1d_jni.cc +++ /dev/null @@ -1,925 +0,0 @@ -/* - * Copyright 2024 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. - */ - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "cpu_features_macros.h" // NOLINT -#include "dav1d/common.h" -#include "dav1d/data.h" -#include "dav1d/dav1d.h" -#include "dav1d/picture.h" - -// For ARMv7, we use `cpu_feature` to detect availability of NEON at runtime. -#ifdef CPU_FEATURES_ARCH_ARM -#include "cpuinfo_arm.h" // NOLINT -#endif // CPU_FEATURES_ARCH_ARM - -// For ARM in general (v7/v8) we detect compile time availability of NEON. -#ifdef CPU_FEATURES_ARCH_ANY_ARM -#if CPU_FEATURES_COMPILED_ANY_ARM_NEON // always defined to 0 or 1. -#define HAS_COMPILE_TIME_NEON_SUPPORT -#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON -#endif // CPU_FEATURES_ARCH_ANY_ARM - -#include - -#include -#include -#include // NOLINT -#include -#include - -#include "cpu_info.h" // NOLINT - -#define LOG_TAG "dav1d_jni" -#define LOGE(...) \ - ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)) - -#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \ - extern "C" { \ - JNIEXPORT RETURN_TYPE \ - Java_androidx_media3_decoder_dav1d_Dav1dDecoder_##NAME(JNIEnv* env, \ - jobject thiz, \ - ##__VA_ARGS__); \ - } \ - JNIEXPORT RETURN_TYPE \ - Java_androidx_media3_decoder_dav1d_Dav1dDecoder_##NAME( \ - JNIEnv* env, jobject thiz, ##__VA_ARGS__) - -// If ANDROID_NATIVE_LIB_MERGING is set, rename JNI_OnLoad to -// JNI_OnLoad_libdav1dJNI. The suffix has to be the same as "lib" -// where is the name of the android_jni_library target. This lets -// apps merge native libraries and reduce the binary size. See go/android_onelib -// for details. -#ifdef ANDROID_NATIVE_LIB_MERGING -jint JNI_OnLoad_libdav1dJNI -#else -jint JNI_OnLoad -#endif - (JavaVM* vm, void* reserved) { - JNIEnv* env; - if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { - return -1; - } - return JNI_VERSION_1_6; -} - -namespace { - -// YUV plane indices. -const int kPlaneY = 0; -const int kPlaneU = 1; -const int kPlaneV = 2; -const int kMaxPlanes = 3; - -// Android YUV format. See: -// https://developer.android.com/reference/android/graphics/ImageFormat.html#YV12. -const int kImageFormatYV12 = 0x32315659; - -// LINT.IfChange -// Output modes. -const int kOutputModeYuv = 0; -const int kOutputModeSurfaceYuv = 1; -// LINT.ThenChange(../../../../common/src/main/java/androidx/media3/common/C.java) - -// LINT.IfChange -const int kColorSpaceUnknown = 0; -const int kColorSpaceBT601 = 1; -const int kColorSpaceBT709 = 2; -const int kColorSpaceBT2020 = 3; -// LINT.ThenChange(../../../../decoder/src/main/java/androidx/media3/decoder/VideoDecoderOutputBuffer.java) - -// LINT.IfChange -// Return codes for jni methods. -const int kStatusError = 0; -const int kStatusOk = 1; -const int kStatusDecodeOnly = 2; -const int kStatusEagain = 3; -// LINT.ThenChange(../java/androidx/media3/decoder/dav1d/Dav1dDecoder.java) - -// LINT.IfChange -// Dav1d thread count settings -const int kDav1dThreadCountDefault = 0; -const int kDav1dThreadCountPerformanceCores = -1; -const int kDav1dThreadCountExperimental = -2; -// LINT.ThenChange(../java/androidx/media3/decoder/dav1d/Libdav1dVideoRenderer.java) - -// Status codes specific to the JNI wrapper code. -enum JniStatusCode { - kJniStatusOk = 0, - kJniStatusOutOfMemory = -1, - kJniStatusBufferAlreadyReleased = -2, - kJniStatusInvalidNumOfPlanes = -3, - kJniStatusHighBitDepthNotSupportedWithYuv = -4, - kJniStatusBufferResizeError = -5, - kJniStatusNeonNotSupported = -6, - kJniStatusSurfaceYuvNotSupported = -7, - kJniStatusDecoderInitFailed = -8, - kJniStatusBufferInitError = -9, - kJniStatusANativeWindowError = -10, -}; - -const int kLibdav1dDecoderStatusOk = 0; - -const char* GetJniErrorMessage(JniStatusCode error_code) { - switch (error_code) { - case kJniStatusOutOfMemory: - return "Out of memory."; - case kJniStatusBufferAlreadyReleased: - return "JNI buffer already released."; - case kJniStatusHighBitDepthNotSupportedWithYuv: - return "High bit depth (10 or 12 bits per pixel) output format is not " - "supported with YUV."; - case kJniStatusInvalidNumOfPlanes: - return "Libdav1d decoded buffer has invalid number of planes."; - case kJniStatusBufferResizeError: - return "Buffer resize failed."; - case kJniStatusNeonNotSupported: - return "Neon is not supported."; - case kJniStatusSurfaceYuvNotSupported: - return "Surface YUV is not supported."; - case kJniStatusDecoderInitFailed: - return "Decoder initialization failed."; - case kJniStatusBufferInitError: - return "Output buffer initialization failed."; - default: - return "Unrecognized error code."; - } -} - -const int GetThreadCount(jint threads) { - switch (threads) { - case kDav1dThreadCountDefault: - return 0; - case kDav1dThreadCountPerformanceCores: - return dav1d_jni::GetNumberOfPerformanceCoresOnline(); - case kDav1dThreadCountExperimental: - return dav1d_jni::GetNumberOfProcessorsOnline() / 2; - default: - return threads; - } -} - -struct Cookie { - jobject global_ref_input_buffer; - jobject global_ref_dav1d_data; - jlong jni_context; -}; - -struct UserDataCookie { - jboolean decode_only; - jint flags; - jint output_mode; - jlong time_us; - jobject format; - jlong jni_context; -}; - -struct PictureAllocatorCookie { - JavaVM* jvm; - jlong jni_context; -}; - -struct PictureAllocatorData { - int aligned_width; - int aligned_height; - int offset; - uint8_t* aligned_buffer_ptr; - jobject direct_byte_buffer; -}; - -struct JniContext { - ~JniContext() { - if (native_window) { - ANativeWindow_release(native_window); - } - } - - bool MaybeAcquireNativeWindow(JNIEnv* env, jobject new_surface) { - if (surface == new_surface) { - return true; - } - if (native_window) { - ANativeWindow_release(native_window); - } - native_window_width = 0; - native_window_height = 0; - native_window = ANativeWindow_fromSurface(env, new_surface); - if (native_window == nullptr) { - jni_status_code = kJniStatusANativeWindowError; - surface = nullptr; - return false; - } - surface = new_surface; - return true; - } - - jobject input_buffer_class; - jobject output_buffer_class; - jobject byte_buffer_class; - jobject decoder_class; - jfieldID output_format_field; - jfieldID display_width_field; - jfieldID display_height_field; - jfieldID data_field; - jfieldID input_data_field; - jfieldID output_buffer_stride_array_field; - jfieldID ystride_field; - jfieldID uvstride_field; - jfieldID decoder_private_field; - jmethodID init_for_yuv_frame_method; - jmethodID release_input_buffer_method; - jmethodID init_output_buffer_method; - jmethodID set_flags_method; - jmethodID set_format_method; - jmethodID init_for_offset_frames_method; - jmethodID init_for_private_frame_method; - jmethodID create_direct_byte_buffer_method; - - Dav1dContext* decoder; - - int libdav1d_status_code = kLibdav1dDecoderStatusOk; - JniStatusCode jni_status_code = kJniStatusOk; - std::vector> unused_cookies; - std::mutex unused_cookies_mutex; - std::vector> unused_user_data_cookies; - std::mutex unused_user_data_cookies_mutex; - std::vector> - unused_picture_allocator_data; - std::mutex unused_picture_allocator_data_mutex; - bool use_custom_allocator; - std::unique_ptr picture_allocator_cookie; - ANativeWindow* native_window = nullptr; - jobject surface = nullptr; - int native_window_width = 0; - int native_window_height = 0; -}; - -void CopyFrameToDataBuffer(const Dav1dPicture* dav1d_picture, jbyte* data) { - for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) { - int stride_index = plane_index; - if (plane_index == kPlaneV) { - stride_index = kPlaneU; - } - uint64_t planeHeight = (plane_index == kPlaneY) - ? dav1d_picture->p.h - : ((dav1d_picture->p.h + 1) / 2); - uint64_t length = dav1d_picture->stride[stride_index] * planeHeight; - memcpy(data, dav1d_picture->data[plane_index], length); - data += length; - } -} - -constexpr int AlignTo16(int value) { return (value + 15) & (~15); } - -void CopyPlane(const uint8_t* source, int source_stride, uint8_t* destination, - int destination_stride, int width, int height) { - while (height--) { - std::memcpy(destination, source, width); - source += source_stride; - destination += destination_stride; - } -} - -constexpr int GetColorSpace(Dav1dColorPrimaries primary) { - switch (primary) { - case DAV1D_COLOR_PRI_BT601: - return kColorSpaceBT601; - case DAV1D_COLOR_PRI_BT709: - return kColorSpaceBT709; - case DAV1D_COLOR_PRI_BT2020: - return kColorSpaceBT2020; - default: - return kColorSpaceUnknown; - } -} -void Dav1dDataFreeCallback(const uint8_t* data, void* cookie) { - Cookie* cookie_ptr = reinterpret_cast(cookie); - JniContext* const context = - reinterpret_cast(cookie_ptr->jni_context); - std::lock_guard unused_cookies_lock( - context->unused_cookies_mutex); - context->unused_cookies.emplace_back(cookie_ptr); -} - -void Dav1dUserDataFreeCallback(const uint8_t* data, void* cookie) { - const UserDataCookie* cookie_ptr = - reinterpret_cast(data); - JniContext* const context = - reinterpret_cast(cookie_ptr->jni_context); - std::lock_guard unused_user_data_cookies_lock( - context->unused_user_data_cookies_mutex); - context->unused_user_data_cookies.emplace_back(cookie_ptr); -} - -static int dav1d_picture_allocator(Dav1dPicture* p, void* cookie) { - // Do all of the sizing math here. - const int hbd = p->p.bpc > 8; - const int aligned_w = (p->p.w + 127) & ~127; - const int aligned_h = (p->p.h + 127) & ~127; - const int has_chroma = p->p.layout != DAV1D_PIXEL_LAYOUT_I400; - const int ss_ver = p->p.layout == DAV1D_PIXEL_LAYOUT_I420; - const int ss_hor = p->p.layout != DAV1D_PIXEL_LAYOUT_I444; - ptrdiff_t y_stride = aligned_w << hbd; - ptrdiff_t uv_stride = has_chroma ? y_stride >> ss_hor : 0; - if (!(y_stride & 1023)) y_stride += DAV1D_PICTURE_ALIGNMENT; - if (!(uv_stride & 1023) && has_chroma) uv_stride += DAV1D_PICTURE_ALIGNMENT; - p->stride[0] = y_stride; - p->stride[1] = uv_stride; - const size_t y_sz = y_stride * aligned_h; - const size_t uv_sz = uv_stride * (aligned_h >> ss_ver); - const size_t pic_size = y_sz + 2 * uv_sz; - const size_t total_size = pic_size + 2 * DAV1D_PICTURE_ALIGNMENT; - - // Get the byte buffer for storing data. - JNIEnv* env; - PictureAllocatorCookie* allocator_cookie = - reinterpret_cast(cookie); - allocator_cookie->jvm->GetEnv(reinterpret_cast(&env), - JNI_VERSION_1_6); - - JniContext* const context = - reinterpret_cast(allocator_cookie->jni_context); - jobject direct_byte_buffer = env->CallStaticObjectMethod( - reinterpret_cast(context->byte_buffer_class), - context->create_direct_byte_buffer_method, total_size); - - if (direct_byte_buffer == nullptr) { - LOGE("Failed to create direct byte buffer."); - return DAV1D_ERR(ENOMEM); - } - - void* buffer_ptr = env->GetDirectBufferAddress(direct_byte_buffer); - if (buffer_ptr == nullptr) { - LOGE("Failed to get direct buffer address."); - return DAV1D_ERR(ENOMEM); - } - - size_t space = total_size; - uint8_t* aligned_buf_address = reinterpret_cast( - std::align(DAV1D_PICTURE_ALIGNMENT, pic_size + DAV1D_PICTURE_ALIGNMENT, - buffer_ptr, space)); - if (aligned_buf_address == nullptr) { - LOGE("Failed to align buffer."); - return DAV1D_ERR(ENOMEM); - } - - PictureAllocatorData* allocator_data = new PictureAllocatorData(); - allocator_data->aligned_width = aligned_w; - allocator_data->aligned_height = aligned_h; - allocator_data->offset = total_size - space; - allocator_data->aligned_buffer_ptr = aligned_buf_address; - allocator_data->direct_byte_buffer = env->NewGlobalRef(direct_byte_buffer); - - p->allocator_data = reinterpret_cast(allocator_data); - - p->data[0] = aligned_buf_address; - p->data[1] = has_chroma ? aligned_buf_address + y_sz : NULL; - p->data[2] = has_chroma ? aligned_buf_address + y_sz + uv_sz : NULL; - - return 0; -} - -static void release_picture_allocator(Dav1dPicture* p, void* cookie) { - PictureAllocatorCookie* allocator_cookie = - reinterpret_cast(cookie); - - JniContext* const context = - reinterpret_cast(allocator_cookie->jni_context); - - std::lock_guard unused_picture_allocator_data_lock( - context->unused_picture_allocator_data_mutex); - - context->unused_picture_allocator_data.emplace_back( - reinterpret_cast(p->allocator_data)); -} - -void CleanUpAllocatorData(jlong jContext, JNIEnv* env) { - if (jContext == kStatusError) { - return; - } - JniContext* const context = reinterpret_cast(jContext); - std::lock_guard unused_picture_allocator_data_lock( - context->unused_picture_allocator_data_mutex); - while (!context->unused_picture_allocator_data.empty()) { - PictureAllocatorData* allocator_data = - context->unused_picture_allocator_data.back().get(); - env->DeleteGlobalRef(allocator_data->direct_byte_buffer); - context->unused_picture_allocator_data.pop_back(); - } -} -} // namespace - -DECODER_FUNC(jlong, dav1dInit, jint threads, jint max_frame_delay, - jboolean use_custom_allocator) { - JniContext* context = new (std::nothrow) JniContext(); - if (context == nullptr) { - return kStatusError; - } - -#ifdef CPU_FEATURES_ARCH_ANY_ARM // Arm v7/v8 -#ifndef HAS_COMPILE_TIME_NEON_SUPPORT // no compile time NEON support -#ifdef CPU_FEATURES_ARCH_ARM // check runtime support for ARMv7 - if (cpu_features::GetArmInfo().features.neon == false) { - context->jni_status_code = kJniStatusNeonNotSupported; - return reinterpret_cast(context); - } -#else // Unexpected case of an ARMv8 with no NEON support. - context->jni_status_code = kJniStatusNeonNotSupported; - return reinterpret_cast(context); -#endif // CPU_FEATURES_ARCH_ARM -#endif // HAS_COMPILE_TIME_NEON_SUPPORT -#endif // CPU_FEATURES_ARCH_ANY_ARM - - Dav1dSettings settings; - dav1d_default_settings(&settings); - settings.n_threads = GetThreadCount(threads); - settings.max_frame_delay = max_frame_delay; - context->use_custom_allocator = use_custom_allocator; - if (use_custom_allocator) { - PictureAllocatorCookie* cookie = new PictureAllocatorCookie(); - JavaVM* jvm; - env->GetJavaVM(&jvm); - cookie->jvm = jvm; - cookie->jni_context = reinterpret_cast(context); - context->picture_allocator_cookie = - std::unique_ptr(cookie); - - Dav1dPicAllocator allocator = { - .cookie = reinterpret_cast(cookie), - .alloc_picture_callback = dav1d_picture_allocator, - .release_picture_callback = release_picture_allocator, - }; - settings.allocator = allocator; - } - - context->libdav1d_status_code = dav1d_open(&context->decoder, &settings); - if (context->libdav1d_status_code != 0) { - context->jni_status_code = kJniStatusDecoderInitFailed; - } - - // Populate JNI References. - const jclass inputBufferClass = - env->FindClass("androidx/media3/decoder/DecoderInputBuffer"); - const jclass outputBufferClass = - env->FindClass("androidx/media3/decoder/VideoDecoderOutputBuffer"); - const jclass decoderClass = - env->FindClass("androidx/media3/decoder/dav1d/Dav1dDecoder"); - const jclass byteBufferClass = env->FindClass("java/nio/ByteBuffer"); - - context->input_buffer_class = env->NewGlobalRef(inputBufferClass); - context->output_buffer_class = env->NewGlobalRef(outputBufferClass); - context->decoder_class = env->NewGlobalRef(decoderClass); - context->byte_buffer_class = env->NewGlobalRef(byteBufferClass); - - context->input_data_field = - env->GetFieldID(inputBufferClass, "data", "Ljava/nio/ByteBuffer;"); - context->output_format_field = env->GetFieldID( - outputBufferClass, "format", "Landroidx/media3/common/Format;"); - context->display_width_field = - env->GetFieldID(outputBufferClass, "width", "I"); - context->display_height_field = - env->GetFieldID(outputBufferClass, "height", "I"); - context->output_buffer_stride_array_field = - env->GetFieldID(outputBufferClass, "yuvStrides", "[I"); - context->ystride_field = env->GetFieldID(outputBufferClass, "yStride", "I"); - context->uvstride_field = env->GetFieldID(outputBufferClass, "uvStride", "I"); - context->init_output_buffer_method = - env->GetMethodID(outputBufferClass, "init", "(JILjava/nio/ByteBuffer;)V"); - context->data_field = - env->GetFieldID(outputBufferClass, "data", "Ljava/nio/ByteBuffer;"); - context->decoder_private_field = - env->GetFieldID(outputBufferClass, "decoderPrivate", "J"); - context->init_for_yuv_frame_method = - env->GetMethodID(outputBufferClass, "initForYuvFrame", "(IIIII)Z"); - context->init_for_private_frame_method = - env->GetMethodID(outputBufferClass, "initForPrivateFrame", "(II)V"); - context->set_flags_method = - env->GetMethodID(outputBufferClass, "setFlags", "(I)V"); - context->release_input_buffer_method = - env->GetMethodID(decoderClass, "releaseInputBuffer", - "(Landroidx/media3/decoder/DecoderInputBuffer;)V"); - context->init_for_offset_frames_method = - env->GetMethodID(outputBufferClass, "initForOffsetFrames", "(IIIIIII)Z"); - context->create_direct_byte_buffer_method = env->GetStaticMethodID( - byteBufferClass, "allocateDirect", "(I)Ljava/nio/ByteBuffer;"); - - // Assert JNI References are valid. - assert(inputBufferClass); - assert(outputBufferClass); - assert(decoderClass); - assert(byteBufferClass); - assert(context->input_buffer_class); - assert(context->output_buffer_class); - assert(context->input_data_field); - assert(context->output_format_field); - assert(context->data_field); - assert(context->display_width_field); - assert(context->display_height_field); - assert(context->ystride_field); - assert(context->uvstride_field); - assert(context->output_buffer_stride_array_field); - assert(context->decoder_private_field); - assert(context->init_output_buffer_method); - assert(context->set_flags_method); - assert(context->init_for_yuv_frame_method); - assert(context->init_for_private_frame_method); - assert(context->release_input_buffer_method); - assert(context->init_for_offset_frames_method); - assert(context->create_direct_byte_buffer_method); - - return reinterpret_cast(context); -} - -DECODER_FUNC(void, dav1dClose, jlong jContext) { - if (jContext == kStatusError) { - return; - } - JniContext* const context = reinterpret_cast(jContext); - if (context->decoder) { - dav1d_close(&context->decoder); - } - env->DeleteGlobalRef(context->input_buffer_class); - env->DeleteGlobalRef(context->output_buffer_class); - env->DeleteGlobalRef(context->decoder_class); - { - std::lock_guard unused_cookies_lock( - context->unused_cookies_mutex); - while (!context->unused_cookies.empty()) { - Cookie* cookie = context->unused_cookies.back().get(); - env->DeleteGlobalRef(cookie->global_ref_dav1d_data); - env->DeleteGlobalRef(cookie->global_ref_input_buffer); - context->unused_cookies.pop_back(); - } - } - { - std::lock_guard unused_user_data_cookies_lock( - context->unused_user_data_cookies_mutex); - while (!context->unused_user_data_cookies.empty()) { - const UserDataCookie* cookie = - context->unused_user_data_cookies.back().get(); - env->DeleteGlobalRef(cookie->format); - context->unused_user_data_cookies.pop_back(); - } - } - { - if (context->use_custom_allocator) { - CleanUpAllocatorData(jContext, env); - } - } - delete context; -} - -DECODER_FUNC(jint, dav1dDecode, jlong jContext, jobject jInputBuffer, - jint offset, jint length, jboolean decodeOnly, jint flags, - jlong timeUs, jint outputMode, jobject format) { - if (jContext == kStatusError) { - return kStatusError; - } - JniContext* const context = reinterpret_cast(jContext); - const jobject encoded_data = - env->GetObjectField(jInputBuffer, context->input_data_field); - void* const buffer_ptr = env->GetDirectBufferAddress(encoded_data); - if (buffer_ptr == nullptr) { - LOGE("Failed to get direct buffer address."); - return kStatusError; - } - const uint8_t* const buf = - reinterpret_cast(buffer_ptr) + offset; - - Dav1dData data = {}; - - Cookie* cookie = new Cookie(); - cookie->global_ref_input_buffer = env->NewGlobalRef(jInputBuffer); - cookie->global_ref_dav1d_data = env->NewGlobalRef(encoded_data); - cookie->jni_context = jContext; - - context->libdav1d_status_code = dav1d_data_wrap( - &data, buf, length, Dav1dDataFreeCallback, static_cast(cookie)); - - if (context->libdav1d_status_code != 0) { - env->DeleteGlobalRef(cookie->global_ref_input_buffer); - env->DeleteGlobalRef(cookie->global_ref_dav1d_data); - delete cookie; - return kStatusError; - } - - UserDataCookie* user_data = new UserDataCookie(); - user_data->decode_only = decodeOnly; - user_data->flags = flags; - user_data->output_mode = outputMode; - user_data->time_us = timeUs; - user_data->format = env->NewGlobalRef(format); - user_data->jni_context = jContext; - - context->libdav1d_status_code = dav1d_data_wrap_user_data( - &data, reinterpret_cast(user_data), - Dav1dUserDataFreeCallback, nullptr); - if (context->libdav1d_status_code != 0) { - LOGE("Failed to wrap user data."); - env->DeleteGlobalRef(user_data->format); - delete user_data; - dav1d_data_unref(&data); - return kStatusError; - } - context->libdav1d_status_code = dav1d_send_data(context->decoder, &data); - if (context->libdav1d_status_code != 0 && - context->libdav1d_status_code != DAV1D_ERR(EAGAIN)) { - LOGE("Failed to send data."); - dav1d_data_unref(&data); - return kStatusError; - } - return kStatusOk; -} - -DECODER_FUNC(jint, dav1dGetFrame, jlong jContext, jobject jOutputBuffer) { - if (jContext == kStatusError) { - return kStatusError; - } - JniContext* const context = reinterpret_cast(jContext); - - auto cleanup_dav1d_picture = [](Dav1dPicture* picture) { - dav1d_picture_unref(picture); - delete picture; - }; - std::unique_ptr dav1d_picture( - new Dav1dPicture(), cleanup_dav1d_picture); - context->libdav1d_status_code = - dav1d_get_picture(context->decoder, dav1d_picture.get()); - if (context->libdav1d_status_code != 0 && - context->libdav1d_status_code != DAV1D_ERR(EAGAIN)) { - LOGE("Failed to get picture. %d", context->libdav1d_status_code); - return kStatusError; - } - if (context->libdav1d_status_code == DAV1D_ERR(EAGAIN)) { - return kStatusEagain; - } - const UserDataCookie* returned_user_data = - reinterpret_cast(dav1d_picture->m.user_data.data); - env->CallVoidMethod(jOutputBuffer, context->set_flags_method, - returned_user_data->flags); - if (env->ExceptionCheck()) { - context->jni_status_code = kJniStatusBufferInitError; - return kStatusError; - } - - if (returned_user_data->decode_only) { - return kStatusDecodeOnly; - } - - // Set up remaining output buffer information - env->CallVoidMethod(jOutputBuffer, context->init_output_buffer_method, - returned_user_data->time_us, - returned_user_data->output_mode, nullptr); - if (env->ExceptionCheck()) { - context->jni_status_code = kJniStatusBufferInitError; - return kStatusError; - } - if (dav1d_picture->p.bpc != 8) { - context->jni_status_code = kJniStatusHighBitDepthNotSupportedWithYuv; - return kStatusError; - } - if (returned_user_data->output_mode == kOutputModeYuv) { - jboolean init_result; - if (context->use_custom_allocator) { - PictureAllocatorData* allocator_data = - reinterpret_cast( - dav1d_picture->allocator_data); - env->SetObjectField(jOutputBuffer, context->data_field, - allocator_data->direct_byte_buffer); - init_result = env->CallBooleanMethod( - jOutputBuffer, context->init_for_offset_frames_method, - allocator_data->offset, dav1d_picture->p.w, dav1d_picture->p.h, - dav1d_picture->stride[kPlaneY], dav1d_picture->stride[kPlaneU], - GetColorSpace(dav1d_picture->seq_hdr->pri), - allocator_data->aligned_height); - CleanUpAllocatorData(jContext, env); - } else { - env->SetObjectField(jOutputBuffer, context->output_format_field, - returned_user_data->format); - init_result = env->CallBooleanMethod( - jOutputBuffer, context->init_for_yuv_frame_method, dav1d_picture->p.w, - dav1d_picture->p.h, dav1d_picture->stride[kPlaneY], - dav1d_picture->stride[kPlaneU], - GetColorSpace(dav1d_picture->seq_hdr->pri)); - } - if (!init_result) { - context->jni_status_code = kJniStatusBufferResizeError; - return kStatusError; - } - if (env->ExceptionCheck()) { - // Exception is thrown in Java when returning from the native call. - context->jni_status_code = kJniStatusBufferResizeError; - return kStatusError; - } - if (!context->use_custom_allocator) { - const jobject data_object = - env->GetObjectField(jOutputBuffer, context->data_field); - jbyte* const data = - reinterpret_cast(env->GetDirectBufferAddress(data_object)); - CopyFrameToDataBuffer(dav1d_picture.get(), data); - } - } else if (returned_user_data->output_mode == kOutputModeSurfaceYuv) { - // Dav1dPicture is cleaned up as part of dav1dReleaseFrame. - Dav1dPicture* dav1d_picture_raw_ptr = dav1d_picture.release(); - env->SetLongField(jOutputBuffer, context->decoder_private_field, - (uint64_t)dav1d_picture_raw_ptr); - env->SetObjectField(jOutputBuffer, context->output_format_field, - returned_user_data->format); - env->CallVoidMethod(jOutputBuffer, context->init_for_private_frame_method, - dav1d_picture_raw_ptr->p.w, dav1d_picture_raw_ptr->p.h); - } - return kStatusOk; -} - -DECODER_FUNC(jint, dav1dRenderFrame, jlong jContext, jobject jSurface, - jobject jOutputBuffer) { - if (jContext == kStatusError) { - LOGE("Failed to render frame. jContext is error."); - return kStatusError; - } - JniContext* const context = reinterpret_cast(jContext); - if (!context->MaybeAcquireNativeWindow(env, jSurface)) { - LOGE("Failed to acquire native window."); - return kStatusError; - } - - int64_t width = env->GetIntField(jOutputBuffer, context->display_width_field); - int64_t height = - env->GetIntField(jOutputBuffer, context->display_height_field); - if (context->native_window_width != width || - context->native_window_height != height) { - if (ANativeWindow_setBuffersGeometry(context->native_window, width, height, - kImageFormatYV12)) { - context->jni_status_code = kJniStatusBufferResizeError; - LOGE("Failed to set buffers geometry."); - return kStatusError; - } - context->native_window_width = width; - context->native_window_height = height; - } - - ANativeWindow_Buffer native_window_buffer; - if (ANativeWindow_lock(context->native_window, &native_window_buffer, - /*inOutDirtyBounds=*/nullptr) || - native_window_buffer.bits == nullptr) { - context->jni_status_code = kJniStatusANativeWindowError; - LOGE("Failed to lock native window."); - return kStatusError; - } - - Dav1dPicture* dav1d_picture = reinterpret_cast( - env->GetLongField(jOutputBuffer, context->decoder_private_field)); - if (dav1d_picture == nullptr) { - LOGE("Failed to get dav1d picture."); - return kStatusError; - } - // Y plane - CopyPlane(reinterpret_cast(dav1d_picture->data[kPlaneY]), - dav1d_picture->stride[kPlaneY], - reinterpret_cast(native_window_buffer.bits), - native_window_buffer.stride, width, height); - - const int y_plane_size = - native_window_buffer.stride * native_window_buffer.height; - const int32_t native_window_buffer_uv_height = - (native_window_buffer.height + 1) / 2; - const int native_window_buffer_uv_stride = - AlignTo16(native_window_buffer.stride / 2); - - const int uv_plane_height = native_window_buffer_uv_height; - - // TODO(b/140606738): Handle monochrome videos. - // V plane - // Since the format for ANativeWindow is YV12, V plane is being processed - // before U plane. - CopyPlane( - reinterpret_cast(dav1d_picture->data[kPlaneV]), - dav1d_picture->stride[kPlaneU], - reinterpret_cast(native_window_buffer.bits) + y_plane_size, - native_window_buffer_uv_stride, width / 2, uv_plane_height); - - const int v_plane_size = uv_plane_height * native_window_buffer_uv_stride; - - // U plane - CopyPlane(reinterpret_cast(dav1d_picture->data[kPlaneU]), - dav1d_picture->stride[kPlaneU], - reinterpret_cast(native_window_buffer.bits) + - y_plane_size + v_plane_size, - native_window_buffer_uv_stride, width / 2, uv_plane_height); - - if (ANativeWindow_unlockAndPost(context->native_window)) { - context->jni_status_code = kJniStatusANativeWindowError; - LOGE("Failed to unlock and post native window."); - return kStatusError; - } - return kStatusOk; -} - -DECODER_FUNC(void, dav1dReleaseFrame, jlong jContext, jobject jOutputBuffer) { - if (jContext == kStatusError) { - return; - } - JniContext* const context = reinterpret_cast(jContext); - Dav1dPicture* dav1d_picture = (Dav1dPicture*)env->GetLongField( - jOutputBuffer, context->decoder_private_field); - env->SetLongField(jOutputBuffer, context->decoder_private_field, 0); - if (dav1d_picture != nullptr) { - dav1d_picture_unref(dav1d_picture); - delete dav1d_picture; - } -} - -DECODER_FUNC(jstring, dav1dGetErrorMessage, jlong jContext) { - if (jContext == kStatusError) { - return env->NewStringUTF("Failed to initialize JNI context."); - } - - JniContext* const context = reinterpret_cast(jContext); - if (context->libdav1d_status_code != kLibdav1dDecoderStatusOk) { - char error_message[100]; - snprintf(error_message, sizeof(error_message), - "There is a decoder error. %d", context->libdav1d_status_code); - return env->NewStringUTF(error_message); - } - if (context->jni_status_code != kJniStatusOk) { - return env->NewStringUTF(GetJniErrorMessage(context->jni_status_code)); - } - return env->NewStringUTF("None."); -} - -DECODER_FUNC(jint, dav1dCheckError, jlong jContext) { - if (jContext == kStatusError) { - return kStatusError; - } - JniContext* const context = reinterpret_cast(jContext); - return (context->libdav1d_status_code != kLibdav1dDecoderStatusOk || - context->jni_status_code != kJniStatusOk) - ? kStatusError - : kStatusOk; -} - -DECODER_FUNC(void, dav1dFlush, jlong jContext) { - if (jContext == kStatusError) { - return; - } - JniContext* const context = reinterpret_cast(jContext); - dav1d_flush(context->decoder); -} - -DECODER_FUNC(void, releaseUnusedInputBuffers, jlong jContext, jobject decoder) { - if (jContext == kStatusError) { - return; - } - JniContext* const context = reinterpret_cast(jContext); - { - std::lock_guard unused_cookies_lock( - context->unused_cookies_mutex); - while (!context->unused_cookies.empty()) { - Cookie* cookie = context->unused_cookies.back().get(); - env->CallVoidMethod(decoder, context->release_input_buffer_method, - cookie->global_ref_input_buffer); - if (env->ExceptionCheck()) { - LOGE("Failed to release input buffer."); - env->ExceptionClear(); - break; - } - env->DeleteGlobalRef(cookie->global_ref_dav1d_data); - env->DeleteGlobalRef(cookie->global_ref_input_buffer); - context->unused_cookies.pop_back(); - } - } - { - std::lock_guard unused_user_data_cookies_lock( - context->unused_user_data_cookies_mutex); - while (!context->unused_user_data_cookies.empty()) { - const UserDataCookie* cookie = - context->unused_user_data_cookies.back().get(); - env->DeleteGlobalRef(cookie->format); - context->unused_user_data_cookies.pop_back(); - } - } -} diff --git a/libraries/decoder_dav1d/src/test/AndroidManifest.xml b/libraries/decoder_dav1d/src/test/AndroidManifest.xml deleted file mode 100644 index 91d138f353..0000000000 --- a/libraries/decoder_dav1d/src/test/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - diff --git a/libraries/decoder_dav1d/src/test/java/androidx/media3/decoder/dav1d/DefaultRenderersFactoryTest.java b/libraries/decoder_dav1d/src/test/java/androidx/media3/decoder/dav1d/DefaultRenderersFactoryTest.java deleted file mode 100644 index bbe2edccfc..0000000000 --- a/libraries/decoder_dav1d/src/test/java/androidx/media3/decoder/dav1d/DefaultRenderersFactoryTest.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2024 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.decoder.dav1d; - -import androidx.media3.common.C; -import androidx.media3.test.utils.DefaultRenderersFactoryAsserts; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link DefaultRenderersFactoryTest} with {@link Libdav1dVideoRenderer}. */ -@RunWith(AndroidJUnit4.class) -public final class DefaultRenderersFactoryTest { - - @Test - public void createRenderers_instantiatesDav1dRenderer() { - DefaultRenderersFactoryAsserts.assertExtensionRendererCreated( - Libdav1dVideoRenderer.class, C.TRACK_TYPE_VIDEO); - } -} diff --git a/libraries/decoder_midi/build.gradle b/libraries/decoder_midi/build.gradle index 36bfa91661..394087aa01 100644 --- a/libraries/decoder_midi/build.gradle +++ b/libraries/decoder_midi/build.gradle @@ -31,8 +31,8 @@ dependencies { api project(modulePrefix + 'lib-extractor') api project(modulePrefix + 'lib-common') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion - // Jsyn v17.1.0 - implementation 'com.github.philburk:jsyn:40a41092cbab558d7d410ec43d93bb1e4121e86a' + // Jsyn v17.2.0 + implementation 'com.github.philburk:jsyn:3f6b44b853bccc0d2e3027104d575fcc5ccb6d4e' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation 'androidx.test:core:' + androidxTestCoreVersion testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion diff --git a/libraries/exoplayer/proguard-rules.txt b/libraries/exoplayer/proguard-rules.txt index e355bff4bb..048d9b7545 100644 --- a/libraries/exoplayer/proguard-rules.txt +++ b/libraries/exoplayer/proguard-rules.txt @@ -9,10 +9,6 @@ -keepclassmembers class androidx.media3.decoder.av1.Libgav1VideoRenderer { (long, android.os.Handler, androidx.media3.exoplayer.video.VideoRendererEventListener, int); } --dontnote androidx.media3.decoder.dav1d.Libdav1dVideoRenderer --keepclassmembers class androidx.media3.decoder.dav1d.Libdav1dVideoRenderer { - (long, android.os.Handler, androidx.media3.exoplayer.video.VideoRendererEventListener, int); -} -dontnote androidx.media3.decoder.ffmpeg.ExperimentalFfmpegVideoRenderer -keepclassmembers class androidx.media3.decoder.ffmpeg.ExperimentalFfmpegVideoRenderer { (long, android.os.Handler, androidx.media3.exoplayer.video.VideoRendererEventListener, int); diff --git a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/StreamVolumeManagerTest.java b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/StreamVolumeManagerTest.java index 9d60593b7d..40d1c7f510 100644 --- a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/StreamVolumeManagerTest.java +++ b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/StreamVolumeManagerTest.java @@ -318,7 +318,6 @@ public void setStreamType_toNonDefaultType_notifiesStreamTypeAndVolume() throws assertThat(streamVolumeManager.getVolume()).isEqualTo(testStreamVolume); }); } - ; @Test public void onStreamVolumeChanged_isCalled_whenAudioManagerChangesIt() throws Exception { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java index c73b36020a..39da12544a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java @@ -486,35 +486,6 @@ protected void buildVideoRenderers( throw new IllegalStateException("Error instantiating VP9 extension", e); } - try { - // LINT.IfChange - Class clazz = Class.forName("androidx.media3.decoder.dav1d.Libdav1dVideoRenderer"); - // Full class names used for media3 constructor args so the LINT rule triggers if any of them - // move. - @SuppressWarnings("UnnecessarilyFullyQualified") - Constructor constructor = - clazz.getConstructor( - long.class, - Handler.class, - androidx.media3.exoplayer.video.VideoRendererEventListener.class, - int.class); - // LINT.ThenChange(../../../../../../proguard-rules.txt) - Renderer renderer = - (Renderer) - constructor.newInstance( - allowedVideoJoiningTimeMs, - eventHandler, - eventListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); - out.add(extensionRendererIndex++, renderer); - Log.i(TAG, "Loaded Libdav1dVideoRenderer."); - } catch (ClassNotFoundException e) { - // Expected if the app was built without the extension. - } catch (Exception e) { - // The extension is present, but instantiation failed. - throw new IllegalStateException("Error instantiating dav1d AV1 extension", e); - } - try { // LINT.IfChange Class clazz = Class.forName("androidx.media3.decoder.av1.Libgav1VideoRenderer"); 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 37a7509590..9cb13f2077 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -1381,6 +1381,15 @@ void setMediaSources( @UnstableApi void setShuffleOrder(ShuffleOrder shuffleOrder); + /** + * Returns the shuffle order. + * + *

The {@link ShuffleOrder} returned will have the same length as the current playlist ({@link + * Player#getMediaItemCount()}). + */ + @UnstableApi + ShuffleOrder getShuffleOrder(); + /** * Sets the {@linkplain PreloadConfiguration preload configuration} to configure playlist * preloading. 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 11e9a0c588..d825df1032 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -786,6 +786,12 @@ public void setShuffleOrder(ShuffleOrder shuffleOrder) { /* repeatCurrentMediaItem= */ false); } + @Override + public ShuffleOrder getShuffleOrder() { + verifyApplicationThread(); + return shuffleOrder; + } + @Override public void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) { verifyApplicationThread(); 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 b9eb4b6f3d..baefc5c3be 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -18,6 +18,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.castNonNull; +import static androidx.media3.common.util.Util.constrainValue; import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.exoplayer.MediaPeriodQueue.UPDATE_PERIOD_QUEUE_ALTERED_PREWARMING_PERIOD; import static androidx.media3.exoplayer.MediaPeriodQueue.UPDATE_PERIOD_QUEUE_ALTERED_READING_PERIOD; @@ -3520,6 +3521,13 @@ private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange( timeline.getPeriodPositionUs(window, period, windowIndex, windowPositionUs); newPeriodUid = periodPositionUs.first; newContentPositionUs = periodPositionUs.second; + } else { + // For all other periods, we may need to clip the duration again. + long newPeriodDurationUs = timeline.getPeriodByUid(newPeriodUid, period).durationUs; + if (newPeriodDurationUs != C.TIME_UNSET) { + newContentPositionUs = + constrainValue(newContentPositionUs, /* min= */ 0, /* max= */ period.durationUs - 1); + } } // Use an explicitly requested content position as new target live offset. setTargetLiveOffset = true; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java index 7a3738ff6a..238da62d79 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java @@ -423,6 +423,7 @@ private final class MediaSourceHandlerCallback implements Handler.Callback { private @MonotonicNonNull MediaSource mediaSource; private @MonotonicNonNull MediaPeriod mediaPeriod; private @MonotonicNonNull Timeline timeline; + private boolean released; public MediaSourceHandlerCallback() { mediaSourceCaller = new MediaSourceCaller(); @@ -430,6 +431,9 @@ public MediaSourceHandlerCallback() { @Override public boolean handleMessage(Message msg) { + if (released) { + return true; + } switch (msg.what) { case MESSAGE_PREPARE_SOURCE: MediaItem mediaItem = (MediaItem) msg.obj; @@ -465,6 +469,7 @@ public boolean handleMessage(Message msg) { } mediaSourceHandler.removeCallbacksAndMessages(/* token= */ null); SHARED_WORKER_THREAD.removeWorker(); + released = true; return true; default: return false; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ScrubbingModeParameters.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ScrubbingModeParameters.java index 397f5b1efd..a5ed9ca182 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ScrubbingModeParameters.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ScrubbingModeParameters.java @@ -52,7 +52,7 @@ public static final class Builder { @Nullable private Double fractionalSeekToleranceBefore; @Nullable private Double fractionalSeekToleranceAfter; private boolean shouldIncreaseCodecOperatingRate; - private boolean isMediaCodecFlushEnabled; + private boolean allowSkippingMediaCodecFlush; private boolean shouldEnableDynamicScheduling; private boolean useDecodeOnlyFlag; @@ -60,6 +60,7 @@ public static final class Builder { public Builder() { this.disabledTrackTypes = ImmutableSet.of(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_METADATA); shouldIncreaseCodecOperatingRate = true; + allowSkippingMediaCodecFlush = true; shouldEnableDynamicScheduling = true; useDecodeOnlyFlag = true; } @@ -70,7 +71,7 @@ private Builder(ScrubbingModeParameters scrubbingModeParameters) { this.fractionalSeekToleranceAfter = scrubbingModeParameters.fractionalSeekToleranceAfter; this.shouldIncreaseCodecOperatingRate = scrubbingModeParameters.shouldIncreaseCodecOperatingRate; - this.isMediaCodecFlushEnabled = scrubbingModeParameters.isMediaCodecFlushEnabled; + this.allowSkippingMediaCodecFlush = scrubbingModeParameters.allowSkippingMediaCodecFlush; this.shouldEnableDynamicScheduling = scrubbingModeParameters.shouldEnableDynamicScheduling; this.useDecodeOnlyFlag = scrubbingModeParameters.useDecodeOnlyFlag; } @@ -165,19 +166,31 @@ public Builder setShouldEnableDynamicScheduling(boolean shouldEnableDynamicSched } /** - * Sets whether the decoder is flushed in scrubbing mode. + * @deprecated Use {@link #setAllowSkippingMediaCodecFlush} instead (but note that the value it + * takes is inverted). + */ + @Deprecated + @CanIgnoreReturnValue + public Builder setIsMediaCodecFlushEnabled(boolean isMediaCodecFlushEnabled) { + this.allowSkippingMediaCodecFlush = !isMediaCodecFlushEnabled; + return this; + } + + /** + * Sets whether to avoid flushing the decoder (where possible) in scrubbing mode. * - *

Setting this to {@code false} will disable flushing the decoder when a new seek starts - * decoding from a key-frame. + *

Setting this to {@code true} will avoid flushing the decoder when a new seek starts + * decoding from a key-frame in compatible content. * - *

Defaults to {@code false} (this may change in a future release). + *

Defaults to {@code true} (this may change in a future release). * - * @param isMediaCodecFlushEnabled Whether to enable flushing of decoder in scrubbing mode. + * @param allowSkippingMediaCodecFlush Whether skip flushing the decoder (where possible) in + * scrubbing mode. * @return This builder for convenience. */ @CanIgnoreReturnValue - public Builder setIsMediaCodecFlushEnabled(boolean isMediaCodecFlushEnabled) { - this.isMediaCodecFlushEnabled = isMediaCodecFlushEnabled; + public Builder setAllowSkippingMediaCodecFlush(boolean allowSkippingMediaCodecFlush) { + this.allowSkippingMediaCodecFlush = allowSkippingMediaCodecFlush; return this; } @@ -241,11 +254,17 @@ public ScrubbingModeParameters build() { public final boolean shouldIncreaseCodecOperatingRate; /** - * Whether the decoder is flushed in scrubbing mode. + * @deprecated Use {@link #allowSkippingMediaCodecFlush} instead (but note that it's value is + * inverted). + */ + @Deprecated public final boolean isMediaCodecFlushEnabled; + + /** + * Whether flushing the decoder is avoided where possible in scrubbing mode. * - *

Defaults to {@code false}. + *

Defaults to {@code true}. */ - public final boolean isMediaCodecFlushEnabled; + public final boolean allowSkippingMediaCodecFlush; /** * Whether to enable ExoPlayer's {@linkplain @@ -266,7 +285,8 @@ private ScrubbingModeParameters(Builder builder) { this.fractionalSeekToleranceBefore = builder.fractionalSeekToleranceBefore; this.fractionalSeekToleranceAfter = builder.fractionalSeekToleranceAfter; this.shouldIncreaseCodecOperatingRate = builder.shouldIncreaseCodecOperatingRate; - this.isMediaCodecFlushEnabled = builder.isMediaCodecFlushEnabled; + this.isMediaCodecFlushEnabled = !builder.allowSkippingMediaCodecFlush; + this.allowSkippingMediaCodecFlush = builder.allowSkippingMediaCodecFlush; this.shouldEnableDynamicScheduling = builder.shouldEnableDynamicScheduling; this.useDecodeOnlyFlag = builder.useDecodeOnlyFlag; } @@ -283,7 +303,7 @@ public boolean equals(@Nullable Object o) { } ScrubbingModeParameters that = (ScrubbingModeParameters) o; return disabledTrackTypes.equals(that.disabledTrackTypes) - && isMediaCodecFlushEnabled == that.isMediaCodecFlushEnabled + && allowSkippingMediaCodecFlush == that.allowSkippingMediaCodecFlush && Objects.equals(fractionalSeekToleranceBefore, that.fractionalSeekToleranceBefore) && Objects.equals(fractionalSeekToleranceAfter, that.fractionalSeekToleranceAfter) && shouldIncreaseCodecOperatingRate == that.shouldIncreaseCodecOperatingRate @@ -298,7 +318,7 @@ public int hashCode() { fractionalSeekToleranceBefore, fractionalSeekToleranceAfter, shouldIncreaseCodecOperatingRate, - isMediaCodecFlushEnabled, + allowSkippingMediaCodecFlush, shouldEnableDynamicScheduling, useDecodeOnlyFlag); } 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 a778b94fad..85e464c47d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java @@ -929,6 +929,12 @@ public void setShuffleOrder(ShuffleOrder shuffleOrder) { player.setShuffleOrder(shuffleOrder); } + @Override + public ShuffleOrder getShuffleOrder() { + blockUntilConstructorFinished(); + return player.getShuffleOrder(); + } + @Override public void setPlayWhenReady(boolean playWhenReady) { blockUntilConstructorFinished(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java index ed2906425d..27e0ef5acf 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java @@ -303,21 +303,12 @@ public long getCurrentPositionUs() { ? audioTimestampPoller.getTimestampPositionUs(systemTimeUs, audioTrackPlaybackSpeed) : getPlaybackHeadPositionEstimateUs(systemTimeUs); - if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) { - if (enableOnAudioPositionAdvancingFix - && onPositionAdvancingFromPositionUs != C.TIME_UNSET - && positionUs >= onPositionAdvancingFromPositionUs - && (useGetTimestampMode || !audioTimestampPoller.isWaitingForAdvancingTimestamp())) { + int audioTrackPlayState = audioTrack.getPlayState(); + if (audioTrackPlayState == PLAYSTATE_PLAYING) { + if (useGetTimestampMode || !audioTimestampPoller.isWaitingForAdvancingTimestamp()) { // Assume the new position is reliable to estimate the playout start time once we have an // advancing timestamp from the AudioTimestampPoller, or we stopped waiting for it. - long mediaDurationSinceResumeUs = positionUs - onPositionAdvancingFromPositionUs; - long playoutDurationSinceLastPositionUs = - Util.getPlayoutDurationForMediaDuration( - mediaDurationSinceResumeUs, audioTrackPlaybackSpeed); - long playoutStartSystemTimeMs = - clock.currentTimeMillis() - Util.usToMs(playoutDurationSinceLastPositionUs); - onPositionAdvancingFromPositionUs = C.TIME_UNSET; - listener.onPositionAdvancing(playoutStartSystemTimeMs); + maybeTriggerOnPositionAdvancingCallback(positionUs); } if (lastSystemTimeUs != C.TIME_UNSET) { @@ -357,6 +348,10 @@ public long getCurrentPositionUs() { lastSystemTimeUs = systemTimeUs; lastPositionUs = positionUs; + } else if (audioTrackPlayState == PLAYSTATE_STOPPED) { + // Once stopped, the position is simulated anyway and we don't need to wait for the timestamp + // poller to produce reliable data. + maybeTriggerOnPositionAdvancingCallback(positionUs); } return positionUs; @@ -450,22 +445,15 @@ public boolean hasPendingData(long writtenFrames) { || forceHasPendingData(); } - /** - * Pauses the audio track position tracker, returning whether the audio track needs to be paused - * to cause playback to pause. If {@code false} is returned the audio track will pause without - * further interaction, as the end of stream has been handled. - */ - public boolean pause() { + /** Pauses the audio track position tracker. */ + public void pause() { resetSyncParams(); if (stopTimestampUs == C.TIME_UNSET) { // The audio track is going to be paused, so reset the timestamp poller to ensure it doesn't // supply an advancing position. checkNotNull(audioTimestampPoller).reset(); - return true; } stopPlaybackHeadPosition = getPlaybackHeadPosition(); - // We've handled the end of the stream already, so there's no need to pause the track. - return false; } /** @@ -513,6 +501,22 @@ private boolean hasPendingAudioTrackUnderruns() { return result; } + private void maybeTriggerOnPositionAdvancingCallback(long positionUs) { + if (!enableOnAudioPositionAdvancingFix + || onPositionAdvancingFromPositionUs == C.TIME_UNSET + || positionUs < onPositionAdvancingFromPositionUs) { + return; + } + long mediaDurationSinceResumeUs = positionUs - onPositionAdvancingFromPositionUs; + long playoutDurationSinceLastPositionUs = + Util.getPlayoutDurationForMediaDuration( + mediaDurationSinceResumeUs, audioTrackPlaybackSpeed); + long playoutStartSystemTimeMs = + clock.currentTimeMillis() - Util.usToMs(playoutDurationSinceLastPositionUs); + onPositionAdvancingFromPositionUs = C.TIME_UNSET; + listener.onPositionAdvancing(playoutStartSystemTimeMs); + } + private void maybeSampleSyncParams() { long systemTimeUs = clock.nanoTime() / 1000; if (systemTimeUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) { 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 7b0c57d8f8..0a0f8b5e54 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 @@ -935,7 +935,9 @@ public void play() { playing = true; if (isAudioTrackInitialized()) { audioTrackPositionTracker.start(); - audioTrack.play(); + if (!stoppedAudioTrack || isOffloadedPlayback(audioTrack)) { + audioTrack.play(); + } } } @@ -1599,9 +1601,11 @@ private void setVolumeInternal() { @Override public void pause() { playing = false; - if (isAudioTrackInitialized() - && (audioTrackPositionTracker.pause() || isOffloadedPlayback(audioTrack))) { - audioTrack.pause(); + if (isAudioTrackInitialized()) { + audioTrackPositionTracker.pause(); + if (!stoppedAudioTrack || isOffloadedPlayback(audioTrack)) { + audioTrack.pause(); + } } } @@ -1668,17 +1672,13 @@ public void release() { public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { Looper myLooper = Looper.myLooper(); - if (playbackLooper != myLooper) { - String playbackLooperName = - playbackLooper == null ? "null" : playbackLooper.getThread().getName(); - String myLooperName = myLooper == null ? "null" : myLooper.getThread().getName(); - throw new IllegalStateException( - "Current looper (" - + myLooperName - + ") is not the playback looper (" - + playbackLooperName - + ")"); - } + checkState( + playbackLooper == myLooper, + "Current looper (" + + getLooperThreadName(myLooper) + + ") is not the playback looper (" + + getLooperThreadName(playbackLooper) + + ")"); if (this.audioCapabilities != null && !audioCapabilities.equals(this.audioCapabilities)) { this.audioCapabilities = audioCapabilities; if (listener != null) { @@ -1888,10 +1888,17 @@ private long getWrittenFrames() { @EnsuresNonNull("audioCapabilities") private void maybeStartAudioCapabilitiesReceiver() { + @Nullable Looper myLooper = Looper.myLooper(); + checkState( + audioCapabilitiesReceiver == null || playbackLooper == myLooper, + "DefaultAudioSink accessed on multiple threads: " + + getLooperThreadName(playbackLooper) + + " and " + + getLooperThreadName(myLooper)); if (audioCapabilitiesReceiver == null && context != null) { // Must be lazily initialized to receive audio capabilities receiver listener event on the // current (playback) thread as the constructor is not called in the playback thread. - playbackLooper = Looper.myLooper(); + playbackLooper = myLooper; audioCapabilitiesReceiver = new AudioCapabilitiesReceiver( context, this::onAudioCapabilitiesChanged, audioAttributes, preferredDevice); @@ -2461,6 +2468,10 @@ private static int getNonPcmMaximumEncodedRateBytesPerSecond(@C.Encoding int enc return rate; } + private static String getLooperThreadName(@Nullable Looper looper) { + return looper == null ? "null" : looper.getThread().getName(); + } + @RequiresApi(23) private static final class Api23 { private Api23() {} diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java index 1e9c0cdd5e..8c3bbe2c3e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java @@ -15,6 +15,9 @@ */ package androidx.media3.exoplayer.drm; +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; + import android.media.MediaDrm; import android.os.ConditionVariable; import android.os.Handler; @@ -22,8 +25,10 @@ import android.os.Looper; import android.util.Pair; import androidx.annotation.Nullable; +import androidx.media3.common.C; import androidx.media3.common.DrmInitData; import androidx.media3.common.Format; +import androidx.media3.common.MediaItem; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.NullableType; import androidx.media3.common.util.UnstableApi; @@ -49,6 +54,32 @@ public final class OfflineLicenseHelper { private final Handler handler; private final DrmSessionEventListener.EventDispatcher eventDispatcher; + /** + * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance + * is no longer required. + * + * @param drmConfiguration The {@link MediaItem.DrmConfiguration} specifying the request details. + * Must use {@link C#WIDEVINE_UUID} as {@link MediaItem.DrmConfiguration#scheme} and contain a + * non-null {@link MediaItem.DrmConfiguration#licenseUri}. + * @param dataSourceFactory A factory from which to obtain {@link DataSource} instances. + * @param eventDispatcher A {@link DrmSessionEventListener.EventDispatcher} used to distribute + * DRM-related events. + * @return A new instance which uses Widevine CDM. + */ + public static OfflineLicenseHelper newWidevineInstance( + MediaItem.DrmConfiguration drmConfiguration, + DataSource.Factory dataSourceFactory, + DrmSessionEventListener.EventDispatcher eventDispatcher) { + checkArgument(drmConfiguration.scheme.equals(C.WIDEVINE_UUID)); + return newWidevineInstance( + checkNotNull(drmConfiguration.licenseUri).toString(), + drmConfiguration.forceDefaultLicenseUri, + drmConfiguration.licenseRequestHeaders, + dataSourceFactory, + /* optionalKeyRequestParameters= */ null, + eventDispatcher); + } + /** * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance * is no longer required. @@ -116,12 +147,33 @@ public static OfflineLicenseHelper newWidevineInstance( DataSource.Factory dataSourceFactory, @Nullable Map optionalKeyRequestParameters, DrmSessionEventListener.EventDispatcher eventDispatcher) { + return newWidevineInstance( + defaultLicenseUrl, + forceDefaultLicenseUrl, + /* optionalKeyRequestHeaders= */ null, + dataSourceFactory, + optionalKeyRequestParameters, + eventDispatcher); + } + + private static OfflineLicenseHelper newWidevineInstance( + String defaultLicenseUrl, + boolean forceDefaultLicenseUrl, + @Nullable Map optionalKeyRequestHeaders, + DataSource.Factory dataSourceFactory, + @Nullable Map optionalKeyRequestParameters, + DrmSessionEventListener.EventDispatcher eventDispatcher) { + HttpMediaDrmCallback httpDrmCallback = + new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, dataSourceFactory); + if (optionalKeyRequestHeaders != null) { + for (Map.Entry entry : optionalKeyRequestHeaders.entrySet()) { + httpDrmCallback.setKeyRequestProperty(entry.getKey(), entry.getValue()); + } + } return new OfflineLicenseHelper( new DefaultDrmSessionManager.Builder() .setKeyRequestParameters(optionalKeyRequestParameters) - .build( - new HttpMediaDrmCallback( - defaultLicenseUrl, forceDefaultLicenseUrl, dataSourceFactory)), + .build(httpDrmCallback), eventDispatcher); } @@ -176,7 +228,7 @@ public void onDrmKeysRemoved(int windowIndex, @Nullable MediaPeriodId mediaPerio * @throws DrmSessionException Thrown when a DRM session error occurs. */ public synchronized byte[] downloadLicense(Format format) throws DrmSessionException { - Assertions.checkArgument(format.drmInitData != null); + checkArgument(format.drmInitData != null); return acquireSessionAndGetOfflineLicenseKeySetIdOnHandlerThread( DefaultDrmSessionManager.MODE_DOWNLOAD, /* offlineLicenseKeySetId= */ null, format); } 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 c23d005f20..97cd968143 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 @@ -941,6 +941,8 @@ private static boolean needsProfileExcludedWorkaround(String mimeType, int profi private static boolean needsDetachedSurfaceUnsupportedWorkaround() { return Build.MANUFACTURER.equals("Xiaomi") || Build.MANUFACTURER.equals("OPPO") - || Build.MANUFACTURER.equals("realme"); + || Build.MANUFACTURER.equals("realme") + || Build.MANUFACTURER.equals("motorola") + || Build.MANUFACTURER.equals("LENOVO"); } } 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 ffd0158e1d..43ff0a80ba 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 @@ -705,6 +705,11 @@ protected final MediaCodecAdapter getCodec() { return codec; } + @Nullable + protected final Format getCodecInputFormat() { + return codecInputFormat; + } + @Nullable protected final MediaFormat getCodecOutputMediaFormat() { return codecOutputMediaFormat; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DefaultDownloadIndex.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DefaultDownloadIndex.java index ef611c9f3e..fa4cede263 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DefaultDownloadIndex.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DefaultDownloadIndex.java @@ -368,7 +368,7 @@ private List loadDownloadsFromVersion2(SQLiteDatabase database) { /* selectionArgs= */ null, /* groupBy= */ null, /* having= */ null, - /* orderBy= */ null); ) { + /* orderBy= */ null)) { while (cursor.moveToNext()) { downloads.add(getDownloadForCurrentRowV2(cursor)); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java index ce8c0c74a4..89cb4ec1fe 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java @@ -75,8 +75,10 @@ import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.BandwidthMeter; import androidx.media3.exoplayer.upstream.DefaultAllocator; +import androidx.media3.exoplayer.util.ReleasableExecutor; import androidx.media3.extractor.ExtractorsFactory; import androidx.media3.extractor.SeekMap; +import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; @@ -118,11 +120,13 @@ public static final class Factory { @Nullable private RenderersFactory renderersFactory; private TrackSelectionParameters trackSelectionParameters; @Nullable private DrmSessionManager drmSessionManager; + @Nullable private Supplier loadExecutorSupplier; private boolean debugLoggingEnabled; /** Creates a {@link Factory}. */ public Factory() { this.trackSelectionParameters = DEFAULT_TRACK_SELECTOR_PARAMETERS; + loadExecutorSupplier = null; } /** @@ -181,6 +185,21 @@ public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManage return this; } + /** + * Sets a supplier for an {@link ReleasableExecutor} that is used for loading the media. + * + *

This is only used for progressive streams. + * + * @param loadExecutor A {@link Supplier} that provides an externally managed {@link + * ReleasableExecutor} for loading. + * @return This factory, for convenience. + */ + @CanIgnoreReturnValue + public Factory setLoadExecutor(Supplier loadExecutor) { + this.loadExecutorSupplier = loadExecutor; + return this; + } + /** * Sets whether to log debug information. The default is {@code false}. * @@ -209,7 +228,10 @@ public DownloadHelper create(MediaItem mediaItem) { isProgressive && dataSourceFactory == null ? null : createMediaSourceInternal( - mediaItem, castNonNull(dataSourceFactory), drmSessionManager), + mediaItem, + castNonNull(dataSourceFactory), + drmSessionManager, + loadExecutorSupplier), trackSelectionParameters, renderersFactory != null ? new DefaultRendererCapabilitiesList.Factory(renderersFactory) @@ -461,7 +483,10 @@ public static MediaSource createMediaSource( DataSource.Factory dataSourceFactory, @Nullable DrmSessionManager drmSessionManager) { return createMediaSourceInternal( - downloadRequest.toMediaItem(), dataSourceFactory, drmSessionManager); + downloadRequest.toMediaItem(), + dataSourceFactory, + drmSessionManager, + /* loadExecutorSupplier= */ null); } private static final String TAG = "DownloadHelper"; @@ -1185,11 +1210,19 @@ private TrackSelectorResult runTrackSelection(int periodIndex) throws ExoPlaybac private static MediaSource createMediaSourceInternal( MediaItem mediaItem, DataSource.Factory dataSourceFactory, - @Nullable DrmSessionManager drmSessionManager) { - MediaSource.Factory mediaSourceFactory = - isProgressive(checkNotNull(mediaItem.localConfiguration)) - ? new ProgressiveMediaSource.Factory(dataSourceFactory) - : new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY); + @Nullable DrmSessionManager drmSessionManager, + @Nullable Supplier loadExecutorSupplier) { + MediaSource.Factory mediaSourceFactory; + if (isProgressive(checkNotNull(mediaItem.localConfiguration))) { + mediaSourceFactory = new ProgressiveMediaSource.Factory(dataSourceFactory); + if (loadExecutorSupplier != null) { + ((ProgressiveMediaSource.Factory) mediaSourceFactory) + .setDownloadExecutor(loadExecutorSupplier); + } + } else { + mediaSourceFactory = + new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY); + } if (drmSessionManager != null) { mediaSourceFactory.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java index 11f0cced2b..1d2d131219 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java @@ -259,8 +259,21 @@ public Factory setDrmSessionManagerProvider( @CanIgnoreReturnValue public Factory setDownloadExecutor( Supplier downloadExecutor, Consumer downloadExecutorReleaser) { - this.downloadExecutorSupplier = - () -> ReleasableExecutor.from(downloadExecutor.get(), downloadExecutorReleaser); + setDownloadExecutor( + () -> ReleasableExecutor.from(downloadExecutor.get(), downloadExecutorReleaser)); + return this; + } + + /** + * Sets a supplier for an {@link ReleasableExecutor} that is used for loading the media. + * + * @param downloadExecutor A {@link Supplier} that provides an externally managed {@link + * ReleasableExecutor} for downloading and extraction. + * @return This factory, for convenience. + */ + @CanIgnoreReturnValue + public MediaSource.Factory setDownloadExecutor(Supplier downloadExecutor) { + this.downloadExecutorSupplier = downloadExecutor; return this; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtil.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtil.java index 7722d4078d..dd5a158705 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtil.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionUtil.java @@ -84,22 +84,22 @@ public static AdPlaybackState addAdGroupToAdPlaybackState( } /** - * Returns the position in the underlying server-side inserted ads stream for the current playback - * position in the {@link Player}. + * Returns the position in the underlying server-side inserted ads stream of the given ads ID for + * the current playback position in the {@link Player}. * * @param player The {@link Player}. - * @param adPlaybackState The {@link AdPlaybackState} defining the ad groups. + * @param adsId The ads ID of the period for which to get the stream position. * @return The position in the underlying server-side inserted ads stream, in microseconds, or * {@link C#TIME_UNSET} if it can't be determined. */ - public static long getStreamPositionUs(Player player, AdPlaybackState adPlaybackState) { + public static long getStreamPositionUs(Player player, Object adsId) { Timeline timeline = player.getCurrentTimeline(); if (timeline.isEmpty()) { return C.TIME_UNSET; } Timeline.Period period = timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period()); - if (!Objects.equals(period.getAdsId(), adPlaybackState.adsId)) { + if (!Objects.equals(period.getAdsId(), adsId)) { return C.TIME_UNSET; } if (player.isPlayingAd()) { @@ -107,12 +107,12 @@ public static long getStreamPositionUs(Player player, AdPlaybackState adPlayback int adIndexInAdGroup = player.getCurrentAdIndexInAdGroup(); long adPositionUs = Util.msToUs(player.getCurrentPosition()); return getStreamPositionUsForAd( - adPositionUs, adGroupIndex, adIndexInAdGroup, adPlaybackState); + adPositionUs, adGroupIndex, adIndexInAdGroup, period.adPlaybackState); } long periodPositionUs = Util.msToUs(player.getCurrentPosition()) - period.getPositionInWindowUs(); return getStreamPositionUsForContent( - periodPositionUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, adPlaybackState); + periodPositionUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, period.adPlaybackState); } /** diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreCacheHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreCacheHelper.java index efe3639f61..24d9267009 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreCacheHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreCacheHelper.java @@ -43,11 +43,14 @@ import androidx.media3.exoplayer.offline.Downloader; import androidx.media3.exoplayer.offline.DownloaderFactory; import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.exoplayer.util.ReleasableExecutor; import com.google.common.base.Supplier; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.util.concurrent.CancellationException; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** A helper for pre-caching a single media. */ @UnstableApi @@ -260,7 +263,7 @@ public PreCacheHelper create(MediaItem mediaItem) { new DefaultDownloaderFactory(cacheDataSourceFactory, downloadExecutor); return new PreCacheHelper( mediaItem, - /* mediaSourceFactory= */ null, + /* testMediaSourceFactory= */ null, downloadHelperFactory, downloaderFactory, preCacheLooper, @@ -271,7 +274,9 @@ public PreCacheHelper create(MediaItem mediaItem) { @VisibleForTesting /* package */ static final int DEFAULT_MIN_RETRY_COUNT = 5; private final MediaItem mediaItem; - private final Supplier downloadHelperSupplier; + + @Nullable private final MediaSource.Factory testMediaSourceFactory; + private final DownloadHelper.Factory downloadHelperFactory; private final DownloaderFactory downloaderFactory; @Nullable private final Listener listener; private final Handler preCacheHandler; @@ -280,17 +285,14 @@ public PreCacheHelper create(MediaItem mediaItem) { /* package */ PreCacheHelper( MediaItem mediaItem, - @Nullable MediaSource.Factory mediaSourceFactory, + @Nullable MediaSource.Factory testMediaSourceFactory, DownloadHelper.Factory downloadHelperFactory, DownloaderFactory downloaderFactory, Looper preCacheLooper, @Nullable Listener listener) { this.mediaItem = mediaItem; - this.downloadHelperSupplier = - () -> - mediaSourceFactory != null - ? downloadHelperFactory.create(mediaSourceFactory.createMediaSource(mediaItem)) - : downloadHelperFactory.create(mediaItem); + this.testMediaSourceFactory = testMediaSourceFactory; + this.downloadHelperFactory = downloadHelperFactory; this.downloaderFactory = downloaderFactory; this.listener = listener; this.preCacheHandler = Util.createHandler(preCacheLooper, /* callback= */ null); @@ -352,14 +354,84 @@ public void release(boolean removeCachedContent) { }); } + private static final class ReleasableSingleThreadExecutor implements ReleasableExecutor { + + private final ExecutorService executor; + private final Runnable releaseRunnable; + + private ReleasableSingleThreadExecutor(Runnable releaseRunnable) { + this.executor = Util.newSingleThreadExecutor("PreCacheHelper:Loader"); + this.releaseRunnable = releaseRunnable; + } + + @Override + public void release() { + execute(releaseRunnable); + executor.shutdown(); + } + + @Override + public void execute(Runnable command) { + executor.execute(command); + } + } + + private static final class ReleasableExecutorSupplier implements Supplier { + private final Handler preCacheHandler; + private @MonotonicNonNull DownloadCallback downloadCallback; + + @GuardedBy("this") + private int executorCount; + + private ReleasableExecutorSupplier(Handler preCacheHandler) { + this.preCacheHandler = preCacheHandler; + } + + public void setDownloadCallback(DownloadCallback downloadCallback) { + this.downloadCallback = downloadCallback; + } + + @Override + public ReleasableSingleThreadExecutor get() { + synchronized (ReleasableExecutorSupplier.this) { + executorCount++; + } + return new ReleasableSingleThreadExecutor(this::onExecutorReleased); + } + + private void onExecutorReleased() { + synchronized (ReleasableExecutorSupplier.this) { + checkState(executorCount > 0); + executorCount--; + if (wereExecutorsReleased()) { + preCacheHandler.post( + () -> { + checkState(wereExecutorsReleased()); + if (downloadCallback != null) { + downloadCallback.maybeSubmitPendingDownloadRequest(); + } + }); + } + } + } + + public boolean wereExecutorsReleased() { + synchronized (ReleasableExecutorSupplier.this) { + return executorCount == 0; + } + } + } + private final class DownloadCallback implements DownloadHelper.Callback { private final Object lock; private final long startPositionMs; private final long durationMs; + @Nullable private final ReleasableExecutorSupplier releasableExecutorSupplier; private final DownloadHelper downloadHelper; private boolean isPreparationOngoing; + @Nullable private DownloadRequest pendingDownloadRequest; @Nullable private Downloader downloader; @Nullable private Task downloaderTask; @@ -371,7 +443,16 @@ public DownloadCallback(long startPositionMs, long durationMs) { this.lock = new Object(); this.startPositionMs = startPositionMs; this.durationMs = durationMs; - this.downloadHelper = downloadHelperSupplier.get(); + if (testMediaSourceFactory != null) { + this.releasableExecutorSupplier = null; + this.downloadHelper = + downloadHelperFactory.create(testMediaSourceFactory.createMediaSource(mediaItem)); + } else { + this.releasableExecutorSupplier = new ReleasableExecutorSupplier(preCacheHandler); + downloadHelperFactory.setLoadExecutor(releasableExecutorSupplier); + this.downloadHelper = downloadHelperFactory.create(mediaItem); + this.releasableExecutorSupplier.setDownloadCallback(this); + } this.isPreparationOngoing = true; this.downloadHelper.prepare(this); } @@ -386,14 +467,11 @@ public void onPrepared(DownloadHelper helper, boolean tracksInfoAvailable) { downloadHelper.release(); MediaItem updatedMediaItem = downloadRequest.toMediaItem(mediaItem.buildUpon()); notifyListeners(listener -> listener.onPrepared(mediaItem, updatedMediaItem)); - downloader = downloaderFactory.createDownloader(downloadRequest); - downloaderTask = - new Task( - downloader, - /* isRemove= */ false, - DEFAULT_MIN_RETRY_COUNT, - /* downloadCallback= */ this); - downloaderTask.start(); + pendingDownloadRequest = downloadRequest; + if (releasableExecutorSupplier == null + || releasableExecutorSupplier.wereExecutorsReleased()) { + maybeSubmitPendingDownloadRequest(); + } } @Override @@ -405,6 +483,21 @@ public void onPrepareError(DownloadHelper helper, IOException e) { notifyListeners(listener -> listener.onPrepareError(mediaItem, e)); } + public void maybeSubmitPendingDownloadRequest() { + checkState(Looper.myLooper() == preCacheHandler.getLooper()); + if (pendingDownloadRequest != null) { + downloader = downloaderFactory.createDownloader(pendingDownloadRequest); + downloaderTask = + new Task( + downloader, + /* isRemove= */ false, + DEFAULT_MIN_RETRY_COUNT, + /* downloadCallback= */ this); + downloaderTask.start(); + pendingDownloadRequest = null; + } + } + public void onDownloadStopped(Task task) { preCacheHandler.post( () -> { @@ -440,6 +533,7 @@ public void cancel(boolean removeCachedContent) { synchronized (lock) { isCanceled = true; } + pendingDownloadRequest = null; downloadHelper.release(); if (downloaderTask != null && downloaderTask.isRemove) { return; 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 c68f9ab81d..a72c3baf04 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 @@ -92,6 +92,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A default {@link TrackSelector} suitable for most use cases. @@ -2410,6 +2411,7 @@ public static SelectionOverride fromBundle(Bundle bundle) { @Nullable private SpatializerWrapperV32 spatializer; private AudioAttributes audioAttributes; + private @MonotonicNonNull Boolean deviceIsTV; /** * @param context Any {@link Context}. @@ -2606,10 +2608,14 @@ public void onRendererCapabilitiesChanged(Renderer renderer) { playbackThread = Thread.currentThread(); parameters = this.parameters; } + if (deviceIsTV == null && context != null) { + deviceIsTV = Util.isTv(context); + } if (parameters.constrainAudioChannelCountToDeviceCapabilities && SDK_INT >= 32 && spatializer == null) { - spatializer = new SpatializerWrapperV32(context, /* defaultTrackSelector= */ this); + spatializer = + new SpatializerWrapperV32(context, /* defaultTrackSelector= */ this, deviceIsTV); } int rendererCount = mappedTrackInfo.getRendererCount(); ExoTrackSelection.@NullableType Definition[] definitions = @@ -2881,6 +2887,7 @@ protected Pair selectAudioTrack( private boolean isAudioFormatWithinAudioChannelCountConstraints( Format format, Parameters parameters) { return !parameters.constrainAudioChannelCountToDeviceCapabilities + || (deviceIsTV != null && deviceIsTV) || (format.channelCount == Format.NO_VALUE || format.channelCount <= 2) || (isDolbyAudio(format) && (SDK_INT < 32 || spatializer == null || !spatializer.isSpatializationSupported())) @@ -4282,12 +4289,13 @@ private static class SpatializerWrapperV32 { @Nullable private final Spatializer.OnSpatializerStateChangedListener listener; public SpatializerWrapperV32( - @Nullable Context context, DefaultTrackSelector defaultTrackSelector) { + @Nullable Context context, + DefaultTrackSelector defaultTrackSelector, + @Nullable Boolean deviceIsTv) { @Nullable AudioManager audioManager = context == null ? null : AudioManagerCompat.getAudioManager(context); - ; - if (audioManager == null || Util.isTv(checkNotNull(context))) { + if (audioManager == null || (deviceIsTv != null && deviceIsTv)) { spatializer = null; spatializationSupported = false; handler = null; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java index 26095bff5c..4fb4dcb2b3 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java @@ -456,7 +456,7 @@ public void onVideoSizeChanged(EventTime eventTime, VideoSize videoSize) { StringBuilder description = new StringBuilder("w=" + videoSize.width + ", h=" + videoSize.height); if (videoSize.pixelWidthHeightRatio != 1.0f) { - description.append(", par=" + videoSize.pixelWidthHeightRatio); + description.append(", par=").append(videoSize.pixelWidthHeightRatio); } logd(eventTime, "videoSize", description.toString()); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/SntpClient.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/SntpClient.java index db84e8494d..524a2e46c0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/SntpClient.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/SntpClient.java @@ -269,7 +269,7 @@ private static long loadNtpTimeOffsetMs() throws IOException { // Extract the results. final byte leap = (byte) ((buffer[0] >> 6) & 0x3); final byte mode = (byte) (buffer[0] & 0x7); - final int stratum = (int) (buffer[1] & 0xff); + final int stratum = buffer[1] & 0xff; final long originateTime = readTimestamp(buffer, ORIGINATE_TIME_OFFSET); final long receiveTime = readTimestamp(buffer, RECEIVE_TIME_OFFSET); final long transmitTime = readTimestamp(buffer, TRANSMIT_TIME_OFFSET); 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 112cf959be..8714b0492c 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 @@ -1512,10 +1512,13 @@ protected final boolean shouldReleaseCodecInsteadOfFlushing() { @Override protected final boolean shouldFlushCodec() { + Format inputFormat = getCodecInputFormat(); return scrubbingModeParameters == null ? super.shouldFlushCodec() - : scrubbingModeParameters.isMediaCodecFlushEnabled + : !scrubbingModeParameters.allowSkippingMediaCodecFlush || isFlushRequired + || tunneling + || (inputFormat != null && inputFormat.maxNumReorderSamples > 0) || hasSkippedFlushAndWaitingForEarlierFrame() || getLastBufferInStreamPresentationTimeUs() != C.TIME_UNSET; } @@ -1571,7 +1574,8 @@ protected int getCodecBufferFlags(DecoderInputBuffer buffer) { && (enableMediaCodecBufferDecodeOnlyFlag || (scrubbingModeParameters != null && scrubbingModeParameters.useDecodeOnlyFlag) || tunneling) - && isBufferBeforeStartTime(buffer)) { + && isBufferBeforeStartTime(buffer) + && !isBufferProbablyLastSample(buffer)) { // The buffer likely needs to be dropped because its timestamp is less than the start time. // If tunneling, we can't decide to do this after decoding because we won't get the buffer // back from the codec in tunneling mode. This may not work perfectly, e.g. when the codec is diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaceholderSurface.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaceholderSurface.java index ba5eb3bd6c..03783917a6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaceholderSurface.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaceholderSurface.java @@ -30,6 +30,7 @@ import androidx.media3.common.util.EGLSurfaceTexture; import androidx.media3.common.util.EGLSurfaceTexture.SecureMode; import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.GlUtil.GlException; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import com.google.errorprone.annotations.InlineMe; @@ -114,17 +115,22 @@ public void release() { } private static @SecureMode int getSecureMode(Context context) { - if (GlUtil.isProtectedContentExtensionSupported(context)) { - if (GlUtil.isSurfacelessContextExtensionSupported()) { - return SECURE_MODE_SURFACELESS_CONTEXT; + try { + if (GlUtil.isProtectedContentExtensionSupported(context)) { + if (GlUtil.isSurfacelessContextExtensionSupported()) { + return SECURE_MODE_SURFACELESS_CONTEXT; + } else { + // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. + // This may require support for EXT_protected_surface, but in practice it works on some + // devices that don't have that extension. See also + // https://github.com/google/ExoPlayer/issues/3558. + return SECURE_MODE_PROTECTED_PBUFFER; + } } else { - // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. - // This may require support for EXT_protected_surface, but in practice it works on some - // devices that don't have that extension. See also - // https://github.com/google/ExoPlayer/issues/3558. - return SECURE_MODE_PROTECTED_PBUFFER; + return SECURE_MODE_NONE; } - } else { + } catch (GlException e) { + Log.e(TAG, "Failed to determine secure mode due to GL error: " + e.getMessage()); return SECURE_MODE_NONE; } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java index b895595085..567da6440d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java @@ -49,6 +49,7 @@ import androidx.media3.common.VideoSize; import androidx.media3.common.util.Clock; import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.GlUtil.GlException; import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.Log; import androidx.media3.common.util.Size; @@ -494,27 +495,32 @@ private boolean registerInput(Format sourceFormat, int inputIndex) checkState(state == STATE_CREATED); ColorInfo inputColorInfo = getAdjustedInputColorInfo(sourceFormat.colorInfo); ColorInfo outputColorInfo; - if (requestOpenGlToneMapping) { - outputColorInfo = ColorInfo.SDR_BT709_LIMITED; - } else if (inputColorInfo.colorTransfer == C.COLOR_TRANSFER_HLG - && SDK_INT < 34 - && GlUtil.isBt2020PqExtensionSupported()) { - // PQ SurfaceView output is supported from API 33, but HLG output is supported from API - // 34. Therefore, convert HLG to PQ if PQ is supported, so that HLG input can be displayed - // properly on API 33. - outputColorInfo = - inputColorInfo.buildUpon().setColorTransfer(C.COLOR_TRANSFER_ST2084).build(); - // Force OpenGL tone mapping if the GL extension required to output HDR colors is not - // available. OpenGL tone mapping is only supported on API 29+. - } else if (!GlUtil.isColorTransferSupported(inputColorInfo.colorTransfer) && SDK_INT >= 29) { - Log.w( - TAG, - Util.formatInvariant( - "Color transfer %d is not supported. Falling back to OpenGl tone mapping.", - inputColorInfo.colorTransfer)); - outputColorInfo = ColorInfo.SDR_BT709_LIMITED; - } else { - outputColorInfo = inputColorInfo; + try { + if (requestOpenGlToneMapping) { + outputColorInfo = ColorInfo.SDR_BT709_LIMITED; + } else if (inputColorInfo.colorTransfer == C.COLOR_TRANSFER_HLG + && SDK_INT < 34 + && GlUtil.isBt2020PqExtensionSupported()) { + // PQ SurfaceView output is supported from API 33, but HLG output is supported from API + // 34. Therefore, convert HLG to PQ if PQ is supported, so that HLG input can be displayed + // properly on API 33. + outputColorInfo = + inputColorInfo.buildUpon().setColorTransfer(C.COLOR_TRANSFER_ST2084).build(); + // Force OpenGL tone mapping if the GL extension required to output HDR colors is not + // available. OpenGL tone mapping is only supported on API 29+. + } else if (!GlUtil.isColorTransferSupported(inputColorInfo.colorTransfer) + && SDK_INT >= 29) { + Log.w( + TAG, + Util.formatInvariant( + "Color transfer %d is not supported. Falling back to OpenGl tone mapping.", + inputColorInfo.colorTransfer)); + outputColorInfo = ColorInfo.SDR_BT709_LIMITED; + } else { + outputColorInfo = inputColorInfo; + } + } catch (GlException e) { + throw new VideoSink.VideoSinkException(e, sourceFormat); } handler = clock.createHandler(checkStateNotNull(Looper.myLooper()), /* callback= */ null); try { 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 de990fdc50..ee1654458c 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -8089,15 +8089,18 @@ public void run(ExoPlayer player) { @Test public void setShuffleOrder_keepsCurrentPosition() throws Exception { AtomicLong positionAfterSetShuffleOrder = new AtomicLong(C.TIME_UNSET); + AtomicReference shuffleOrderRef = new AtomicReference<>(); + FakeShuffleOrder shuffleOrder = new FakeShuffleOrder(/* length= */ 1); ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) .playUntilPosition(0, 5000) - .setShuffleOrder(new FakeShuffleOrder(/* length= */ 1)) + .setShuffleOrder(shuffleOrder) .executeRunnable( new PlayerRunnable() { @Override public void run(ExoPlayer player) { positionAfterSetShuffleOrder.set(player.getCurrentPosition()); + shuffleOrderRef.set(player.getShuffleOrder()); } }) .play() @@ -8109,6 +8112,7 @@ public void run(ExoPlayer player) { .blockUntilEnded(TIMEOUT_MS); assertThat(positionAfterSetShuffleOrder.get()).isAtLeast(5000); + assertThat(shuffleOrderRef.get()).isEqualTo(shuffleOrder); } @Test diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MetadataRetrieverTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MetadataRetrieverTest.java index 5c8afd2271..4eb0060aa5 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MetadataRetrieverTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MetadataRetrieverTest.java @@ -24,9 +24,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; import android.content.Context; import android.net.Uri; +import android.os.Looper; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.Metadata; @@ -42,6 +44,7 @@ import androidx.media3.extractor.metadata.mp4.SmtaMetadataEntry; import androidx.media3.test.utils.FakeClock; import androidx.media3.test.utils.FakeMediaSource; +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; @@ -57,6 +60,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.LooperMode; +import org.robolectric.shadows.ShadowLooper; /** Tests for {@link MetadataRetriever}. */ @LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST) @@ -76,6 +80,15 @@ public void setUp() throws Exception { @After public void tearDown() throws Exception { + // Drain loopers to ensure async cleanup tasks complete before the test ends. + // This prevents state leakage between tests. + for (Looper looper : ShadowLooper.getAllLoopers()) { + try { + shadowOf(looper).idle(); + } catch (IllegalStateException e) { + // Looper was already quit, safe to ignore. + } + } MetadataRetriever.setMaximumParallelRetrievals( MetadataRetriever.DEFAULT_MAXIMUM_PARALLEL_RETRIEVALS); } @@ -612,6 +625,7 @@ public void retrieveUsingInstance_releasesMediaSource_afterRetrieval() throws Ex @Test public void retrieveUsingInstance_releasesMediaSource_afterCancellation() throws Exception { FakeMediaSource fakeMediaSource = new FakeMediaSource(); + fakeMediaSource.setAllowPreparation(false); MediaSource.Factory mediaSourceFactory = mock(MediaSource.Factory.class); when(mediaSourceFactory.createMediaSource(any(MediaItem.class))).thenReturn(fakeMediaSource); MediaItem mediaItem = @@ -651,20 +665,33 @@ public void retrieveUsingInstance_closeWhileRetrievalOngoing_doesNotInterruptRet @Test public void retrieveUsingInstance_cancelOneFuture_doesNotAffectOthers() throws Exception { + Timeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition.Builder().setPeriodCount(1).build()); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline); + fakeMediaSource.setAllowPreparation(false); + MediaSource.Factory mediaSourceFactory = mock(MediaSource.Factory.class); + when(mediaSourceFactory.createMediaSource(any(MediaItem.class))).thenReturn(fakeMediaSource); MediaItem mediaItem = MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")); try (MetadataRetriever retriever = - new MetadataRetriever.Builder(context, mediaItem).setClock(clock).build()) { + new MetadataRetriever.Builder(context, mediaItem) + .setClock(clock) + .setMediaSourceFactory(mediaSourceFactory) + .build()) { ListenableFuture trackGroupsFuture = retriever.retrieveTrackGroups(); ListenableFuture timelineFuture = retriever.retrieveTimeline(); + assertThat(trackGroupsFuture.isDone()).isFalse(); + assertThat(timelineFuture.isDone()).isFalse(); assertThat(trackGroupsFuture.cancel(true)).isTrue(); assertThrows(CancellationException.class, trackGroupsFuture::get); + fakeMediaSource.setAllowPreparation(true); // The other future should still complete successfully. - Timeline timeline = timelineFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS); - assertThat(timeline).isNotNull(); - assertThat(timeline.getWindowCount()).isEqualTo(1); + Timeline retrievedTimeline = timelineFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS); + assertThat(retrievedTimeline).isNotNull(); + assertThat(retrievedTimeline.getPeriodCount()).isEqualTo(1); } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ScrubbingModeParametersTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ScrubbingModeParametersTest.java index 7833cc5c12..6bd4fd4dba 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ScrubbingModeParametersTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ScrubbingModeParametersTest.java @@ -27,9 +27,11 @@ public final class ScrubbingModeParametersTest { @Test + @SuppressWarnings("deprecation") // Testing deprecated fields. public void defaultValues() { assertThat(ScrubbingModeParameters.DEFAULT.disabledTrackTypes) .containsExactly(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_METADATA); assertThat(ScrubbingModeParameters.DEFAULT.isMediaCodecFlushEnabled).isFalse(); + assertThat(ScrubbingModeParameters.DEFAULT.allowSkippingMediaCodecFlush).isTrue(); } } 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 0fe6383b14..ae2992c102 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 @@ -911,7 +911,7 @@ public void dynamicTimelineChange() throws Exception { period1Seq0 /* PLAYLIST_CHANGED (sources in playlist moved) */) .inOrder(); assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) - .containsExactly(window0Period1Seq0, window0Period1Seq0, period1Seq0, period1Seq0); + .containsExactly(window0Period1Seq0, window0Period1Seq0); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(window0Period1Seq0); assertThat(listener.getEvents(EVENT_LOAD_STARTED)) .containsExactly( diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/AudioTrackPositionTrackerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/AudioTrackPositionTrackerTest.java index 35dc88e87c..9f9e83c007 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/AudioTrackPositionTrackerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/AudioTrackPositionTrackerTest.java @@ -16,7 +16,11 @@ package androidx.media3.exoplayer.audio; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; 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 android.media.AudioFormat; import android.media.AudioTrack; @@ -264,6 +268,156 @@ public void getCurrentPositionUs_afterHandleEndOfStreamWithPausePlay_returnsCorr assertThat(audioTrackPositionTracker.getCurrentPositionUs()).isEqualTo(2_000_000L); } + @Test + public void onPositionAdvancing_isTriggeredWhenPlaying() { + AudioTrackPositionTracker.Listener listener = mock(AudioTrackPositionTracker.Listener.class); + AudioTrackPositionTracker audioTrackPositionTracker = new AudioTrackPositionTracker(listener); + audioTrackPositionTracker.setClock(clock); + audioTrackPositionTracker.setAudioTrack( + audioTrack, + /* isPassthrough= */ false, + C.ENCODING_PCM_16BIT, + OUTPUT_PCM_FRAME_SIZE, + MIN_BUFFER_SIZE, + /* enableOnAudioPositionAdvancingFix= */ true); + // Start the tracker to set the initial position for advancing check. + audioTrackPositionTracker.start(); + audioTrack.play(); + // Write data to advance the position. + writeBytesAndAdvanceTime(audioTrack); + + // Call getCurrentPositionUs() to request an update. + audioTrackPositionTracker.getCurrentPositionUs(); + + verify(listener).onPositionAdvancing(anyLong()); + } + + @Test + public void onPositionAdvancing_isNotTriggeredWhenPaused() { + AudioTrackPositionTracker.Listener listener = mock(AudioTrackPositionTracker.Listener.class); + AudioTrackPositionTracker audioTrackPositionTracker = new AudioTrackPositionTracker(listener); + audioTrackPositionTracker.setClock(clock); + audioTrackPositionTracker.setAudioTrack( + audioTrack, + /* isPassthrough= */ false, + C.ENCODING_PCM_16BIT, + OUTPUT_PCM_FRAME_SIZE, + MIN_BUFFER_SIZE, + /* enableOnAudioPositionAdvancingFix= */ true); + // Start the tracker to set the initial position for advancing check. + audioTrackPositionTracker.start(); + audioTrack.play(); + // Write data to advance the position. + writeBytesAndAdvanceTime(audioTrack); + // Pause the tracker and audio track. + audioTrackPositionTracker.pause(); + audioTrack.pause(); + + // Call getCurrentPositionUs() while stopped to request an update. + audioTrackPositionTracker.getCurrentPositionUs(); + + verify(listener, never()).onPositionAdvancing(anyLong()); + } + + @Test + public void onPositionAdvancing_isTriggeredAgainAfterPauseAndResume() { + AudioTrackPositionTracker.Listener listener = mock(AudioTrackPositionTracker.Listener.class); + AudioTrackPositionTracker audioTrackPositionTracker = new AudioTrackPositionTracker(listener); + audioTrackPositionTracker.setClock(clock); + audioTrackPositionTracker.setAudioTrack( + audioTrack, + /* isPassthrough= */ false, + C.ENCODING_PCM_16BIT, + OUTPUT_PCM_FRAME_SIZE, + MIN_BUFFER_SIZE, + /* enableOnAudioPositionAdvancingFix= */ true); + // Start the tracker to set the initial position for advancing check. + audioTrackPositionTracker.start(); + audioTrack.play(); + // Write data to advance the position. + writeBytesAndAdvanceTime(audioTrack); + // Call getCurrentPositionUs() to request an initial update. + audioTrackPositionTracker.getCurrentPositionUs(); + + // Pause the tracker and audio track. + audioTrackPositionTracker.pause(); + audioTrack.pause(); + // Call getCurrentPositionUs() while paused to request an update. + audioTrackPositionTracker.getCurrentPositionUs(); + // Write some more data to advance the position again. + writeBytesAndAdvanceTime(audioTrack); + // Start the tracker again. + audioTrackPositionTracker.start(); + audioTrack.play(); + // Call getCurrentPositionUs() to request another update. + audioTrackPositionTracker.getCurrentPositionUs(); + + verify(listener, times(2)).onPositionAdvancing(anyLong()); + } + + @Test + public void onPositionAdvancing_isTriggeredWhenStopped() { + AudioTrackPositionTracker.Listener listener = mock(AudioTrackPositionTracker.Listener.class); + AudioTrackPositionTracker audioTrackPositionTracker = new AudioTrackPositionTracker(listener); + audioTrackPositionTracker.setClock(clock); + audioTrackPositionTracker.setAudioTrack( + audioTrack, + /* isPassthrough= */ false, + C.ENCODING_PCM_16BIT, + OUTPUT_PCM_FRAME_SIZE, + MIN_BUFFER_SIZE, + /* enableOnAudioPositionAdvancingFix= */ true); + // Start the tracker to set the initial position for advancing check. + audioTrackPositionTracker.start(); + audioTrack.play(); + // Write data to advance the position. + writeBytesAndAdvanceTime(audioTrack); + // Simulate stopping the track before the advancing callback is triggered. + audioTrackPositionTracker.handleEndOfStream(/* writtenFrames= */ SAMPLE_RATE); + audioTrack.stop(); + verify(listener, never()).onPositionAdvancing(anyLong()); + + // Call getCurrentPositionUs() while the track is stopped to request an update. + audioTrackPositionTracker.getCurrentPositionUs(); + + verify(listener).onPositionAdvancing(anyLong()); + } + + @Test + public void onPositionAdvancing_isTriggeredAgainAfterStoppedPauseAndResume() { + AudioTrackPositionTracker.Listener listener = mock(AudioTrackPositionTracker.Listener.class); + AudioTrackPositionTracker audioTrackPositionTracker = new AudioTrackPositionTracker(listener); + audioTrackPositionTracker.setClock(clock); + audioTrackPositionTracker.setAudioTrack( + audioTrack, + /* isPassthrough= */ false, + C.ENCODING_PCM_16BIT, + OUTPUT_PCM_FRAME_SIZE, + MIN_BUFFER_SIZE, + /* enableOnAudioPositionAdvancingFix= */ true); + // Start the tracker to set the initial position for advancing check. + audioTrackPositionTracker.start(); + audioTrack.play(); + // Write data to advance the position. + writeBytesAndAdvanceTime(audioTrack); + // Simulate stopping the track before the advancing callback is triggered. + audioTrackPositionTracker.handleEndOfStream(/* writtenFrames= */ SAMPLE_RATE); + audioTrack.stop(); + // Call getCurrentPositionUs() to request an initial update. + audioTrackPositionTracker.getCurrentPositionUs(); + + // Pause the tracker. + audioTrackPositionTracker.pause(); + // Call getCurrentPositionUs() while paused to request an update. + audioTrackPositionTracker.getCurrentPositionUs(); + // Start the tracker again. + audioTrackPositionTracker.start(); + // Call getCurrentPositionUs() to request another update. + audioTrackPositionTracker.getCurrentPositionUs(); + + verify(listener, times(2)).onPositionAdvancing(anyLong()); + } + private void writeBytesAndAdvanceTime(AudioTrack audioTrack) { ByteBuffer byteBuffer = createDefaultSilenceBuffer(); int bytesRemaining = byteBuffer.remaining(); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DecoderAudioRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DecoderAudioRendererTest.java index b1133468bf..8c994513c6 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DecoderAudioRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DecoderAudioRendererTest.java @@ -81,7 +81,7 @@ public void setUp() throws Exception { audioRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT); } - @Config(sdk = Config.OLDEST_SDK) + @Config(sdk = 21) @Test public void supportsFormatAtApi21() { // From API 21, tunneling is supported. diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java index 99da127afc..17c257760a 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java @@ -349,7 +349,7 @@ public void floatPcmNeedsTranscodingIfFloatOutputDisabled() { .isEqualTo(SINK_FORMAT_SUPPORTED_WITH_TRANSCODING); } - @Config(minSdk = Config.OLDEST_SDK) + @Config(minSdk = 21) @Test public void floatOutputSupportedIfFloatOutputEnabledFromApi21() { defaultAudioSink = new DefaultAudioSink.Builder().setEnableFloatOutput(true).build(); @@ -488,8 +488,7 @@ public void bluetoothDeviceAddedAndRemoved_audioCapabilitiesUpdated() { } @Test - @Config( - minSdk = Config.OLDEST_SDK) // AudioManager.ACTION_HDMI_AUDIO_PLUG is supported from API 21. + @Config(minSdk = 21) // AudioManager.ACTION_HDMI_AUDIO_PLUG is supported from API 21. public void hdmiDeviceAddedAndRemoved_audioCapabilitiesUpdated() { // Set UI mode to TV. getShadowUiModeManager().setCurrentModeType(Configuration.UI_MODE_TYPE_TELEVISION); @@ -689,8 +688,7 @@ public void afterRelease_bluetoothDeviceAdded_audioCapabilitiesShouldNotBeUpdate } @Test - @Config( - minSdk = Config.OLDEST_SDK) // AudioManager.ACTION_HDMI_AUDIO_PLUG is supported from API 21. + @Config(minSdk = 21) // AudioManager.ACTION_HDMI_AUDIO_PLUG is supported from API 21. public void afterRelease_hdmiDeviceAdded_audioCapabilitiesShouldNotBeUpdated() { // Set UI mode to TV. getShadowUiModeManager().setCurrentModeType(Configuration.UI_MODE_TYPE_TELEVISION); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java index 0dc22cf4a0..eafc4fa770 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java @@ -15,9 +15,12 @@ */ package androidx.media3.exoplayer.e2etest; +import static org.robolectric.annotation.GraphicsMode.Mode.NATIVE; + import android.content.Context; import android.graphics.SurfaceTexture; import android.view.Surface; +import androidx.annotation.Nullable; import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.exoplayer.ExoPlayer; @@ -35,44 +38,47 @@ import org.robolectric.ParameterizedRobolectricTestRunner; import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; +import org.robolectric.annotation.GraphicsMode; /** End-to-end tests using MP4 samples. */ +@GraphicsMode(NATIVE) @RunWith(ParameterizedRobolectricTestRunner.class) public class Mp4PlaybackTest { @Parameters(name = "{0}") - public static ImmutableList mediaSamples() { + public static ImmutableList mediaSamples() { return ImmutableList.of( - "midroll-5s.mp4", - "postroll-5s.mp4", - "preroll-5s.mp4", - "pixel-motion-photo-2-hevc-tracks.mp4", - "sample_ac3_fragmented.mp4", - "sample_ac3.mp4", - "sample_ac4_fragmented.mp4", - "sample_ac4.mp4", - "sample_android_slow_motion.mp4", - "sample_eac3_fragmented.mp4", - "sample_eac3.mp4", - "sample_eac3joc_fragmented.mp4", - "sample_eac3joc.mp4", - "sample_fragmented.mp4", - "sample_fragmented_seekable.mp4", - "sample_fragmented_large_bitrates.mp4", - "sample_fragmented_sei.mp4", - "sample_mdat_too_long.mp4", - "sample.mp4", - "sample_with_metadata.mp4", - "sample_with_numeric_genre.mp4", - "sample_opus_fragmented.mp4", - "sample_opus.mp4", - "sample_partially_fragmented.mp4", - "testvid_1022ms.mp4", - "sample_edit_list.mp4", - "sample_edit_list_no_sync_frame_before_edit.mp4"); + Sample.forFile("midroll-5s.mp4"), + Sample.forFile("postroll-5s.mp4"), + Sample.forFile("preroll-5s.mp4"), + Sample.forFile("pixel-motion-photo-2-hevc-tracks.mp4"), + Sample.forFile("sample_ac3_fragmented.mp4"), + Sample.forFile("sample_ac3.mp4"), + Sample.forFile("sample_ac4_fragmented.mp4"), + Sample.forFile("sample_ac4.mp4"), + Sample.forFile("sample_android_slow_motion.mp4"), + Sample.forFile("sample_eac3_fragmented.mp4"), + Sample.forFile("sample_eac3.mp4"), + Sample.forFile("sample_eac3joc_fragmented.mp4"), + Sample.forFile("sample_eac3joc.mp4"), + Sample.forFile("sample_fragmented.mp4"), + Sample.forFile("sample_fragmented_seekable.mp4"), + Sample.forFile("sample_fragmented_large_bitrates.mp4"), + Sample.forFile("sample_fragmented_sei.mp4"), + Sample.forFile("sample_mdat_too_long.mp4"), + Sample.forFile("sample.mp4"), + Sample.forFile("sample_with_metadata.mp4"), + Sample.forFile("sample_with_numeric_genre.mp4"), + Sample.forFile("sample_opus_fragmented.mp4"), + Sample.forFile("sample_opus.mp4"), + Sample.forFile("sample_partially_fragmented.mp4"), + Sample.withSubtitles("sample_with_vobsub.mp4", "eng"), + Sample.forFile("testvid_1022ms.mp4"), + Sample.forFile("sample_edit_list.mp4"), + Sample.forFile("sample_edit_list_no_sync_frame_before_edit.mp4")); } - @Parameter public String inputFile; + @Parameter public Sample sample; @Rule public ShadowMediaCodecConfig mediaCodecConfig = @@ -86,12 +92,20 @@ public void test() throws Exception { new ExoPlayer.Builder(applicationContext, renderersFactory) .setClock(new FakeClock(/* isAutoAdvancing= */ true)) .build(); + if (sample.subtitleLanguageToSelect != null) { + player.setTrackSelectionParameters( + player + .getTrackSelectionParameters() + .buildUpon() + .setPreferredTextLanguage(sample.subtitleLanguageToSelect) + .build()); + } Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); player.setVideoSurface(surface); PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory); - player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/" + inputFile)); + player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/" + sample.filename)); player.prepare(); player.play(); TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); @@ -99,6 +113,29 @@ public void test() throws Exception { surface.release(); DumpFileAsserts.assertOutput( - applicationContext, playbackOutput, "playbackdumps/mp4/" + inputFile + ".dump"); + applicationContext, playbackOutput, "playbackdumps/mp4/" + sample.filename + ".dump"); + } + + private static final class Sample { + public final String filename; + @Nullable public final String subtitleLanguageToSelect; + + private Sample(String filename, @Nullable String subtitleLanguageToSelect) { + this.filename = filename; + this.subtitleLanguageToSelect = subtitleLanguageToSelect; + } + + public static Sample forFile(String filename) { + return new Sample(filename, /* subtitleLanguageToSelect= */ null); + } + + public static Sample withSubtitles(String filename, String subtitleLanguageToSelect) { + return new Sample(filename, /* enableSubtitles= */ subtitleLanguageToSelect); + } + + @Override + public String toString() { + return filename; + } } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/ScrubbingPlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/ScrubbingPlaybackTest.java index afa0fd39d7..d6b5261067 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/ScrubbingPlaybackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/ScrubbingPlaybackTest.java @@ -87,7 +87,7 @@ public void scrubbingPlayback_withFlushingDisabled_dumpsCorrectOutput() throws E player.release(); surface.release(); - assertThat(player.getScrubbingModeParameters().isMediaCodecFlushEnabled).isFalse(); + assertThat(player.getScrubbingModeParameters().allowSkippingMediaCodecFlush).isTrue(); DumpFileAsserts.assertOutput( applicationContext, playbackOutput, @@ -125,7 +125,7 @@ public void scrubbingPlayback_withSeekBackwardsAndFlushingDisabled_dumpsCorrectO player.release(); surface.release(); - assertThat(player.getScrubbingModeParameters().isMediaCodecFlushEnabled).isFalse(); + assertThat(player.getScrubbingModeParameters().allowSkippingMediaCodecFlush).isTrue(); DumpFileAsserts.assertOutput( applicationContext, playbackOutput, @@ -163,7 +163,7 @@ public void scrubbingPlayback_withSeekToBufferInCodecAndFlushingDisabled_dumpsCo player.release(); surface.release(); - assertThat(player.getScrubbingModeParameters().isMediaCodecFlushEnabled).isFalse(); + assertThat(player.getScrubbingModeParameters().allowSkippingMediaCodecFlush).isTrue(); DumpFileAsserts.assertOutput( applicationContext, playbackOutput, @@ -194,7 +194,11 @@ public void scrubbingPlayback_withSeekToBufferInCodecAndFlushingDisabled_dumpsCo play(player).untilBackgroundThreadCondition(hasReceivedOutputBufferPastBlockTime::get); player.setScrubbingModeEnabled(true); player.setScrubbingModeParameters( - player.getScrubbingModeParameters().buildUpon().setIsMediaCodecFlushEnabled(true).build()); + player + .getScrubbingModeParameters() + .buildUpon() + .setAllowSkippingMediaCodecFlush(false) + .build()); player.seekTo(500); // End blocking in renderer. @@ -205,7 +209,7 @@ public void scrubbingPlayback_withSeekToBufferInCodecAndFlushingDisabled_dumpsCo player.release(); surface.release(); - assertThat(player.getScrubbingModeParameters().isMediaCodecFlushEnabled).isTrue(); + assertThat(player.getScrubbingModeParameters().allowSkippingMediaCodecFlush).isFalse(); DumpFileAsserts.assertOutput( applicationContext, playbackOutput, 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 index 951fc3c0a9..89910af048 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2Test.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ConcatenatingMediaSource2Test.java @@ -16,7 +16,9 @@ package androidx.media3.exoplayer.source; import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil; +import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.advance; import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState; +import static com.google.common.collect.Iterables.getLast; import static com.google.common.truth.Truth.assertThat; import static java.lang.Math.max; import static org.mockito.ArgumentMatchers.any; @@ -24,10 +26,10 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; 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 android.os.ConditionVariable; import android.os.Handler; import android.os.Looper; import android.util.Pair; @@ -39,26 +41,29 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.Player; import androidx.media3.common.Timeline; +import androidx.media3.common.util.Clock; import androidx.media3.common.util.Util; import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.LoadingInfo; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.util.EventLogger; +import androidx.media3.test.utils.FakeClock; import androidx.media3.test.utils.FakeMediaSource; import androidx.media3.test.utils.FakeTimeline; import androidx.media3.test.utils.TestExoPlayerBuilder; import androidx.media3.test.utils.TestUtil; import androidx.media3.test.utils.robolectric.RobolectricUtil; 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.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.ParameterizedRobolectricTestRunner; @@ -302,6 +307,15 @@ public static ImmutableList params() { /* 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, true}, + /* windowDurationMs= */ 840, + /* manifest= */ null), new ExpectedTimelineData( /* isSeekable= */ true, /* isDynamic= */ true, @@ -316,7 +330,7 @@ public static ImmutableList params() { builder.add( new TestConfig( "duplicated_and_nested_sources", - () -> { + clock -> { MediaSource duplicateSource = buildMediaSource( buildWindow( @@ -324,8 +338,8 @@ public static ImmutableList params() { /* isSeekable= */ true, /* isDynamic= */ false, /* durationMs= */ 1000)) - .get(); - Supplier duplicateSourceSupplier = () -> duplicateSource; + .apply(clock); + Function duplicateSourceSupplier = unused -> duplicateSource; return buildConcatenatingMediaSource( duplicateSourceSupplier, buildConcatenatingMediaSource( @@ -333,7 +347,7 @@ public static ImmutableList params() { buildConcatenatingMediaSource( duplicateSourceSupplier, duplicateSourceSupplier), duplicateSourceSupplier) - .get(); + .apply(clock); }, /* expectedAdDiscontinuities= */ 0, new ExpectedTimelineData( @@ -456,6 +470,15 @@ public static ImmutableList params() { /* 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, @@ -481,9 +504,16 @@ public static ImmutableList params() { private static final String TEST_MEDIA_ITEM_ID = "test_media_item_id"; + private Clock testClock; + + @Before + public void setUp() { + testClock = new FakeClock(/* isAutoAdvancing= */ true); + } + @Test public void prepareSource_reportsExpectedTimelines() throws Exception { - MediaSource mediaSource = config.mediaSourceSupplier.get(); + MediaSource mediaSource = config.mediaSourceSupplier.apply(testClock); ArrayList timelines = new ArrayList<>(); mediaSource.prepareSource( (source, timeline) -> timelines.add(timeline), @@ -536,7 +566,7 @@ public void prepareSource_reportsExpectedTimelines() throws Exception { @Test public void prepareSource_afterRelease_reportsSameFinalTimeline() throws Exception { // Fully prepare source once. - MediaSource mediaSource = config.mediaSourceSupplier.get(); + MediaSource mediaSource = config.mediaSourceSupplier.apply(testClock); ArrayList timelines = new ArrayList<>(); MediaSource.MediaSourceCaller caller = (source, timeline) -> timelines.add(timeline); mediaSource.prepareSource(caller, /* mediaTransferListener= */ null, PlayerId.UNSET); @@ -549,13 +579,13 @@ public void prepareSource_afterRelease_reportsSameFinalTimeline() throws Excepti mediaSource.prepareSource(secondCaller, /* mediaTransferListener= */ null, PlayerId.UNSET); // Assert that we receive the same final timeline. - runMainLooperUntil(() -> Iterables.getLast(timelines).equals(secondTimeline.get())); + runMainLooperUntil(() -> getLast(timelines).equals(secondTimeline.get())); } @Test public void preparePeriod_reportsExpectedPeriodLoadEvents() throws Exception { // Prepare source and register listener. - MediaSource mediaSource = config.mediaSourceSupplier.get(); + MediaSource mediaSource = config.mediaSourceSupplier.apply(testClock); MediaSourceEventListener eventListener = mock(MediaSourceEventListener.class); mediaSource.addEventListener(new Handler(Looper.myLooper()), eventListener); ArrayList timelines = new ArrayList<>(); @@ -569,7 +599,7 @@ public void preparePeriod_reportsExpectedPeriodLoadEvents() throws Exception { // should support creating the same period more than once. ArrayList mediaPeriods = new ArrayList<>(); ArrayList mediaPeriodIds = new ArrayList<>(); - Timeline timeline = Iterables.getLast(timelines); + Timeline timeline = getLast(timelines); for (int i = 0; i < timeline.getPeriodCount(); i++) { Timeline.Period period = timeline.getPeriod(/* periodIndex= */ i, new Timeline.Period(), /* setIds= */ true); @@ -641,8 +671,10 @@ public void preparePeriod_reportsExpectedPeriodLoadEvents() throws Exception { public void playback_fromDefaultPosition_startsFromCorrectPositionAndPlaysToEnd() throws Exception { ExoPlayer player = - new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); - player.setMediaSource(config.mediaSourceSupplier.get()); + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) + .setClock(testClock) + .build(); + player.setMediaSource(config.mediaSourceSupplier.apply(testClock)); Player.Listener eventListener = mock(Player.Listener.class); player.addListener(eventListener); player.addAnalyticsListener(new EventLogger()); @@ -658,7 +690,7 @@ public void playback_fromDefaultPosition_startsFromCorrectPositionAndPlaysToEnd( } player.release(); - ExpectedTimelineData expectedData = Iterables.getLast(config.expectedTimelineData); + ExpectedTimelineData expectedData = getLast(config.expectedTimelineData); assertThat(positionAfterPrepareMs).isEqualTo(expectedData.defaultPositionMs); if (!isDynamic) { verify( @@ -673,8 +705,10 @@ public void playback_fromDefaultPosition_startsFromCorrectPositionAndPlaysToEnd( playback_fromSpecificPeriodPositionInFirstPeriod_startsFromCorrectPositionAndPlaysToEnd() throws Exception { ExoPlayer player = - new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); - MediaSource mediaSource = config.mediaSourceSupplier.get(); + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) + .setClock(testClock) + .build(); + MediaSource mediaSource = config.mediaSourceSupplier.apply(testClock); player.setMediaSource(mediaSource); Player.Listener eventListener = mock(Player.Listener.class); player.addListener(eventListener); @@ -693,7 +727,7 @@ public void playback_fromDefaultPosition_startsFromCorrectPositionAndPlaysToEnd( } player.release(); - ExpectedTimelineData expectedData = Iterables.getLast(config.expectedTimelineData); + ExpectedTimelineData expectedData = getLast(config.expectedTimelineData); assertThat(windowPositionAfterPrepareMs).isEqualTo(startWindowPositionMs); if (!isDynamic) { verify( @@ -710,8 +744,10 @@ public void playback_fromDefaultPosition_startsFromCorrectPositionAndPlaysToEnd( Timeline.Period period = new Timeline.Period(); Timeline.Window window = new Timeline.Window(); ExoPlayer player = - new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); - MediaSource mediaSource = config.mediaSourceSupplier.get(); + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) + .setClock(testClock) + .build(); + MediaSource mediaSource = config.mediaSourceSupplier.apply(testClock); player.setMediaSource(mediaSource); Player.Listener eventListener = mock(Player.Listener.class); player.addListener(eventListener); @@ -739,7 +775,7 @@ public void playback_fromDefaultPosition_startsFromCorrectPositionAndPlaysToEnd( } player.release(); - ExpectedTimelineData expectedData = Iterables.getLast(config.expectedTimelineData); + ExpectedTimelineData expectedData = getLast(config.expectedTimelineData); assertThat(periodPositionAfterPrepareMs).isEqualTo(startPeriodPositionMs); if (timeline.getPeriod(periodIndexAfterPrepare, period).getAdGroupCount() == 0) { assertThat(periodIndexAfterPrepare).isEqualTo(startPeriodIndex); @@ -758,11 +794,74 @@ public void playback_fromDefaultPosition_startsFromCorrectPositionAndPlaysToEnd( } } + @Test + public void playback_fromPositionBeyondDuration_startsFromEndAndPlaysToEnd() throws Exception { + Timeline.Period period = new Timeline.Period(); + Timeline.Window window = new Timeline.Window(); + ExoPlayer player = + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) + .setClock(testClock) + .build(); + MediaSource mediaSource = config.mediaSourceSupplier.apply(testClock); + player.setMediaSource(mediaSource); + Player.Listener eventListener = mock(Player.Listener.class); + player.addListener(eventListener); + player.addAnalyticsListener(new EventLogger()); + + player.seekTo(999_999_999_999L); + player.prepare(); + AtomicBoolean longWaitTimeUntilAllTimelinesUpdatesAreDone = new AtomicBoolean(); + testClock + .createHandler(Looper.getMainLooper(), /* callback= */ null) + .postDelayed(() -> longWaitTimeUntilAllTimelinesUpdatesAreDone.set(true), 5_000); + advance(player) + .untilBackgroundThreadCondition(longWaitTimeUntilAllTimelinesUpdatesAreDone::get); + Timeline timeline = player.getCurrentTimeline(); + long windowPositionAfterPrepareMs = player.getContentPosition(); + boolean isPlayingAd = player.isPlayingAd(); + 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 = getLast(config.expectedTimelineData); + int lastPeriodIndex = expectedData.periodDurationsMs.length - 1; + long lastPeriodDurationMs = + expectedData.periodDurationsMs[expectedData.periodDurationsMs.length - 1]; + long lastPeriodOffsetInWindowMs = + expectedData.periodOffsetsInWindowMs[expectedData.periodOffsetsInWindowMs.length - 1]; + if (!isPlayingAd) { + assertThat(periodIndexAfterPrepare).isEqualTo(lastPeriodIndex); + if (lastPeriodDurationMs == C.TIME_UNSET) { + assertThat(windowPositionAfterPrepareMs).isEqualTo(999_999_999_999L); + assertThat(periodPositionAfterPrepareMs) + .isEqualTo(999_999_999_999L - lastPeriodOffsetInWindowMs); + } else { + assertThat(windowPositionAfterPrepareMs).isEqualTo(expectedData.windowDurationMs - 1); + assertThat(periodPositionAfterPrepareMs).isEqualTo(lastPeriodDurationMs - 1); + } + verify(eventListener, never()) + .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(lastPeriodIndex); + verify(eventListener, atLeast(1)) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION)); + } + } + @Test public void canUpdateMediaItem_withFieldsChanged_returnsTrue() { MediaItem updatedMediaItem = TestUtil.buildFullyCustomizedMediaItem().buildUpon().setUri("http://test.test").build(); - MediaSource mediaSource = config.mediaSourceSupplier.get(); + MediaSource mediaSource = config.mediaSourceSupplier.apply(testClock); boolean canUpdateMediaItem = mediaSource.canUpdateMediaItem(updatedMediaItem); @@ -772,7 +871,7 @@ public void canUpdateMediaItem_withFieldsChanged_returnsTrue() { @Test public void updateMediaItem_createsTimelineWithUpdatedItem() throws Exception { MediaItem updatedMediaItem = new MediaItem.Builder().setUri("http://test.test").build(); - MediaSource mediaSource = config.mediaSourceSupplier.get(); + MediaSource mediaSource = config.mediaSourceSupplier.apply(testClock); AtomicReference timelineReference = new AtomicReference<>(); mediaSource.updateMediaItem(updatedMediaItem); @@ -790,13 +889,13 @@ public void updateMediaItem_createsTimelineWithUpdatedItem() throws Exception { .isEqualTo(updatedMediaItem); } - private static void blockingPrepareMediaPeriod(MediaPeriod mediaPeriod) { - ConditionVariable mediaPeriodPrepared = new ConditionVariable(); + private static void blockingPrepareMediaPeriod(MediaPeriod mediaPeriod) throws TimeoutException { + AtomicBoolean mediaPeriodPrepared = new AtomicBoolean(); mediaPeriod.prepare( new MediaPeriod.Callback() { @Override public void onPrepared(MediaPeriod mediaPeriod) { - mediaPeriodPrepared.open(); + mediaPeriodPrepared.set(true); } @Override @@ -805,50 +904,50 @@ public void onContinueLoadingRequested(MediaPeriod source) { } }, /* positionUs= */ 0); - mediaPeriodPrepared.block(); + runMainLooperUntil(mediaPeriodPrepared::get); } - private static Supplier buildConcatenatingMediaSource( - Supplier... sources) { + private static Function buildConcatenatingMediaSource( + Function... sources) { return buildConcatenatingMediaSource(/* placeholderDurationMs= */ C.TIME_UNSET, sources); } - private static Supplier buildConcatenatingMediaSource( - long placeholderDurationMs, Supplier... sources) { - return () -> { + private static Function buildConcatenatingMediaSource( + long placeholderDurationMs, Function... sources) { + return clock -> { 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); + for (Function source : sources) { + builder.add(source.apply(clock), placeholderDurationMs); } return builder.build(); }; } - private static Supplier buildMediaSource( + private static Function buildMediaSource( FakeTimeline.TimelineWindowDefinition... windows) { return buildMediaSource(/* preparationDelayCount= */ 0, windows); } - private static Supplier buildMediaSource( + private static Function buildMediaSource( int preparationDelayCount, FakeTimeline.TimelineWindowDefinition... windows) { return buildMediaSource(preparationDelayCount, /* manifests= */ null, windows); } - private static Supplier buildMediaSource( + private static Function buildMediaSource( Object[] manifests, FakeTimeline.TimelineWindowDefinition... windows) { return buildMediaSource(/* preparationDelayCount= */ 0, manifests, windows); } - private static Supplier buildMediaSource( + private static Function 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 clock -> { + // Add some delay according to the preparationDelayCount value. This ensures that all other + // message handling triggered by source preparation finishes before the new timeline update + // arrives. return new FakeMediaSource( /* timeline= */ null, new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()) { @@ -856,22 +955,15 @@ private static Supplier buildMediaSource( 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); + clock + .createHandler(Looper.myLooper(), /* callback= */ null) + .postDelayed( + () -> + setNewSourceInfo( + manifests != null + ? new FakeTimeline(manifests, windows) + : new FakeTimeline(windows)), + 10 * preparationDelayCount); } }; }; @@ -945,7 +1037,7 @@ private static FakeTimeline.TimelineWindowDefinition buildWindow( private static final class TestConfig { - public final Supplier mediaSourceSupplier; + public final Function mediaSourceSupplier; public final ImmutableList expectedTimelineData; private final int expectedAdDiscontinuities; @@ -953,7 +1045,7 @@ private static final class TestConfig { public TestConfig( String tag, - Supplier mediaSourceSupplier, + Function mediaSourceSupplier, int expectedAdDiscontinuities, ExpectedTimelineData... expectedTimelineData) { this.tag = tag; diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreCacheHelperTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreCacheHelperTest.java index 3f81303d0d..60ba69c476 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreCacheHelperTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreCacheHelperTest.java @@ -74,7 +74,6 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -113,7 +112,6 @@ public void tearDown() { } @Test - @Ignore("TODO: Fix the flakiness of this test and re-enable it") public void preCache_succeeds() throws Exception { PreCacheHelper preCacheHelper = new PreCacheHelper.Factory( @@ -931,7 +929,7 @@ public void releaseWithRemovingCachedContent_whileDownloadOngoing_downloaderCanc PreCacheHelper preCacheHelper = new PreCacheHelper( mediaItem, - /* mediaSourceFactory= */ null, + /* testMediaSourceFactory= */ null, downloadHelperFactory, fakeDownloaderFactory, preCacheLooper, @@ -965,7 +963,7 @@ public void releaseWithRemovingCachedContent_whileRemoveOngoing_doNotCancelDownl PreCacheHelper preCacheHelper = new PreCacheHelper( mediaItem, - /* mediaSourceFactory= */ null, + /* testMediaSourceFactory= */ null, downloadHelperFactory, fakeDownloaderFactory, preCacheLooper, diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/FixedFrameRateEstimatorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/FixedFrameRateEstimatorTest.java index b988925b8b..61662cefee 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/FixedFrameRateEstimatorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/FixedFrameRateEstimatorTest.java @@ -202,7 +202,7 @@ public void variableFrameRate_doesNotSync() { } } - private static final long getNsWithMsPrecision(long presentationTimeNs) { + private static long getNsWithMsPrecision(long presentationTimeNs) { return (presentationTimeNs / 1000000) * 1000000; } } 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 a1cf15f76a..676285e0be 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 @@ -2023,7 +2023,67 @@ public void render_afterSeekWithFlushingEnabled_rendersFramesAsExpected() throws // Set scrubbing mode but with flushing enabled. mediaCodecVideoRenderer.handleMessage( Renderer.MSG_SET_SCRUBBING_MODE, - new ScrubbingModeParameters.Builder().setIsMediaCodecFlushEnabled(true).build()); + new ScrubbingModeParameters.Builder().setAllowSkippingMediaCodecFlush(false).build()); + seekToUs(mediaCodecVideoRenderer, fakeSampleStream, /* positionUs= */ 200_000); + + for (int i = 0; i < 5; i++) { + mediaCodecVideoRenderer.render(200_000, SystemClock.elapsedRealtime() * 1000); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + } + shadowOf(testMainLooper).idle(); + + verify(eventListener).onVideoEnabled(argumentDecoderCounters.capture()); + + assertThat(argumentDecoderCounters.getValue().skippedOutputBufferCount).isEqualTo(1); + assertThat(argumentDecoderCounters.getValue().renderedOutputBufferCount).isEqualTo(2); + } + + @Test + public void + render_afterSeekWithFlushingDisabledAndNonZeroMaxNumReorderSamples_rendersFramesAsExpected() + throws Exception { + Format h264FormatWithNonZeroMaxNumReorderSamples = + VIDEO_H264.buildUpon().setMaxNumReorderSamples(2).build(); + ArgumentCaptor argumentDecoderCounters = + ArgumentCaptor.forClass(DecoderCounters.class); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ h264FormatWithNonZeroMaxNumReorderSamples, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 50_000), + oneByteSample(/* timeUs= */ 100_000), + oneByteSample(/* timeUs= */ 150_000, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 200_000), + oneByteSample(/* timeUs= */ 250_000), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {h264FormatWithNonZeroMaxNumReorderSamples}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0, + /* mediaPeriodId= */ new MediaSource.MediaPeriodId(new Object())); + // Render first sample and decode the second. + mediaCodecVideoRenderer.start(); + mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); + for (int i = 0; i < 5; i++) { + mediaCodecVideoRenderer.render(40_000, SystemClock.elapsedRealtime() * 1000); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + } + + // Set scrubbing mode with flushing disabled. + mediaCodecVideoRenderer.handleMessage( + Renderer.MSG_SET_SCRUBBING_MODE, + new ScrubbingModeParameters.Builder().setAllowSkippingMediaCodecFlush(true).build()); seekToUs(mediaCodecVideoRenderer, fakeSampleStream, /* positionUs= */ 200_000); for (int i = 0; i < 5; i++) { @@ -2767,7 +2827,7 @@ public void render_afterSeeksWithFlushingEnabledThenDisabled_rendersFramesAsExpe // Enable flushing so that new seek will flush the 50_000 us sample. mediaCodecVideoRenderer.handleMessage( Renderer.MSG_SET_SCRUBBING_MODE, - new ScrubbingModeParameters.Builder().setIsMediaCodecFlushEnabled(true).build()); + new ScrubbingModeParameters.Builder().setAllowSkippingMediaCodecFlush(false).build()); seekToUs(mediaCodecVideoRenderer, fakeSampleStream, /* positionUs= */ 200_000); // Disable flushing but there should be zero samples to drop with the seek. diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java index 3d07a2efd0..84a22fd8ef 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java @@ -608,7 +608,8 @@ private static int[][] getGroupedAdaptationSetIndices(List adapta if (trickPlayProperty != null) { long mainAdaptationSetId = Long.parseLong(trickPlayProperty.value); @Nullable Integer mainAdaptationSetIndex = adaptationSetIdToIndex.get(mainAdaptationSetId); - if (mainAdaptationSetIndex != null) { + if (mainAdaptationSetIndex != null + && canMergeAdaptationSets(adaptationSet, adaptationSets.get(mainAdaptationSetIndex))) { mergedGroupIndex = mainAdaptationSetIndex; } } @@ -663,8 +664,10 @@ private static boolean canMergeAdaptationSets( } Format format1 = adaptationSet1.representations.get(0).format; Format format2 = adaptationSet2.representations.get(0).format; + int format1RoleFlagsExcludingTrickPlay = format1.roleFlags & ~C.ROLE_FLAG_TRICK_PLAY; + int format2RoleFlagsExcludingTrickPlay = format2.roleFlags & ~C.ROLE_FLAG_TRICK_PLAY; return Objects.equals(format1.language, format2.language) - && format1.roleFlags == format2.roleFlags; + && format1RoleFlagsExcludingTrickPlay == format2RoleFlagsExcludingTrickPlay; } /** diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java index c2a7c4fac0..6d9f62c09b 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java @@ -143,6 +143,32 @@ public void trickPlayProperty_mergesTrackGroups() throws IOException { MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); } + @Test + public void trickPlayProperty_withIncompatibleFormats_mergesOnlyCompatibleTrackGroups() + throws IOException { + DashManifest manifest = parseManifest("media/mpd/sample_mpd_trick_play_property_incompatible"); + DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0); + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + // We expect the trick play adaptation sets to be merged with the ones to which they refer, + // retaining representations in their original order, except when their properties differ. + TrackGroupArray expectedTrackGroups = + new TrackGroupArray( + new TrackGroup( + /* id= */ "3000000000", + adaptationSets.get(0).representations.get(0).format, + adaptationSets.get(0).representations.get(1).format, + adaptationSets.get(1).representations.get(0).format), + new TrackGroup( + /* id= */ "3000000002", + adaptationSets.get(2).representations.get(0).format, + adaptationSets.get(2).representations.get(1).format), + new TrackGroup( + /* id= */ "3000000003", adaptationSets.get(3).representations.get(0).format)); + + MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); + } + @Test public void adaptationSetSwitchingProperty_andTrickPlayProperty_mergesTrackGroups() throws IOException { diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashPreCacheHelperTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashPreCacheHelperTest.java new file mode 100644 index 0000000000..26b473bc1b --- /dev/null +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashPreCacheHelperTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2025 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.exoplayer.dash; + +import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.robolectric.Shadows.shadowOf; + +import android.os.HandlerThread; +import android.os.Looper; +import androidx.media3.common.MediaItem; +import androidx.media3.datasource.cache.Cache; +import androidx.media3.datasource.cache.NoOpCacheEvictor; +import androidx.media3.datasource.cache.SimpleCache; +import androidx.media3.exoplayer.source.preload.PreCacheHelper; +import androidx.media3.test.utils.TestUtil; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.File; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class DashPreCacheHelperTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private Cache downloadCache; + private HandlerThread preCacheThread; + private Looper preCacheLooper; + + @Before + public void setUp() throws Exception { + File testDir = temporaryFolder.newFile("PreCacheHelperTest"); + assertThat(testDir.delete()).isTrue(); + assertThat(testDir.mkdirs()).isTrue(); + downloadCache = + new SimpleCache(testDir, new NoOpCacheEvictor(), TestUtil.getInMemoryDatabaseProvider()); + preCacheThread = new HandlerThread("preCache"); + preCacheThread.start(); + preCacheLooper = preCacheThread.getLooper(); + } + + @After + public void tearDown() { + downloadCache.release(); + preCacheThread.quit(); + } + + @Test + public void preCache_succeeds() throws Exception { + PreCacheHelper.Listener preCacheHelperListener = mock(PreCacheHelper.Listener.class); + AtomicBoolean preCacheCompleted = new AtomicBoolean(); + doAnswer( + invocation -> { + preCacheCompleted.set(true); + return null; + }) + .when(preCacheHelperListener) + .onPreCacheProgress(any(), anyLong(), anyLong(), eq(100f)); + PreCacheHelper preCacheHelper = + new PreCacheHelper.Factory( + ApplicationProvider.getApplicationContext(), downloadCache, preCacheLooper) + .setListener(preCacheHelperListener) + .create(MediaItem.fromUri("asset:///media/dash/multi-track/sample.mpd")); + + preCacheHelper.preCache(/* startPositionMs= */ 0, /* durationMs= */ 2000L); + shadowOf(preCacheLooper).idle(); + runMainLooperUntil(preCacheCompleted::get); + shadowOf(Looper.getMainLooper()).idle(); + + verify(preCacheHelperListener).onPrepared(any(), any()); + verify(preCacheHelperListener, never()).onPrepareError(any(), any()); + verify(preCacheHelperListener, never()).onDownloadError(any(), any()); + + preCacheHelper.release(/* removeCachedContent= */ true); + shadowOf(preCacheLooper).idle(); + } +} diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java index 00d39b179a..906215975a 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java @@ -597,12 +597,13 @@ public void getNextChunk( return; } + boolean isIndependent = isIndependent(segmentBaseHolder, playlist); boolean shouldSpliceIn = HlsMediaChunk.shouldSpliceIn( previous, loadPositionUs, selectedPlaylistUrl, - playlist, + isIndependent, segmentBaseHolder, startOfPlaylistInPeriodUs); if (shouldSpliceIn && segmentBaseHolder.isPreload) { @@ -632,10 +633,20 @@ public void getNextChunk( /* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri), /* initSegmentKey= */ keyCache.get(initSegmentKeyUri), shouldSpliceIn, + isIndependent, playerId, cmcdDataFactory); } + private static boolean isIndependent( + HlsChunkSource.SegmentBaseHolder segmentBaseHolder, HlsMediaPlaylist mediaPlaylist) { + if (segmentBaseHolder.segmentBase instanceof HlsMediaPlaylist.Part) { + return ((HlsMediaPlaylist.Part) segmentBaseHolder.segmentBase).isIndependent + || (segmentBaseHolder.partIndex == 0 && mediaPlaylist.hasIndependentSegments); + } + return mediaPlaylist.hasIndependentSegments; + } + @Nullable private static SegmentBaseHolder getNextSegmentHolder( HlsMediaPlaylist mediaPlaylist, long nextMediaSequence, int nextPartIndex) { diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java index cdac82fa23..352eee47b8 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java @@ -821,8 +821,15 @@ public boolean handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timel } else { contentPositionUs = msToUs(player.getContentPosition()); } + int adGroupIndex = + adPlaybackState.getAdGroupIndexForPositionUs(contentPositionUs, window.durationUs); maybeExecuteOrSetNextAssetListResolutionMessage( - adsId, timeline, /* windowIndex= */ 0, contentPositionUs); + adsId, + timeline, + /* windowIndex= */ 0, + adGroupIndex != C.INDEX_UNSET + ? adPlaybackState.getAdGroup(adGroupIndex).timeUs + : contentPositionUs); } } boolean adPlaybackStateUpdated = putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState); diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java index 841d95fb63..15b045d735 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaChunk.java @@ -80,6 +80,7 @@ * @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null * otherwise. * @param shouldSpliceIn Whether samples for this chunk should be spliced into existing samples. + * @param isIndependent Whether the chunk starts with a keyframe. * @param cmcdDataFactory The {@link CmcdData.Factory} for generating {@link CmcdData}. */ public static HlsMediaChunk createInstance( @@ -100,6 +101,7 @@ public static HlsMediaChunk createInstance( @Nullable byte[] mediaSegmentKey, @Nullable byte[] initSegmentKey, boolean shouldSpliceIn, + boolean isIndependent, PlayerId playerId, @Nullable CmcdData.Factory cmcdDataFactory) { // Media segment. @@ -211,6 +213,7 @@ public static HlsMediaChunk createInstance( id3Decoder, scratchId3Data, shouldSpliceIn, + isIndependent, playerId); } @@ -222,7 +225,7 @@ public static HlsMediaChunk createInstance( * @param bufferedPositionUs The position in the sample stream in microseconds since the start of * the period up to which data is already buffered. * @param playlistUrl The URL of the playlist from which the new chunk will be obtained. - * @param mediaPlaylist The {@link HlsMediaPlaylist} containing the new chunk. + * @param isIndependent Whether the new chunk is independent (i.e, starts with a keyframe). * @param segmentBaseHolder The {@link HlsChunkSource.SegmentBaseHolder} with information about * the new chunk. * @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in microseconds. @@ -232,7 +235,7 @@ public static boolean shouldSpliceIn( @Nullable HlsMediaChunk previousChunk, long bufferedPositionUs, Uri playlistUrl, - HlsMediaPlaylist mediaPlaylist, + boolean isIndependent, HlsChunkSource.SegmentBaseHolder segmentBaseHolder, long startOfPlaylistInPeriodUs) { if (previousChunk == null) { @@ -248,8 +251,7 @@ public static boolean shouldSpliceIn( // non-overlapping segments to avoid the splice. long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segmentBaseHolder.segmentBase.relativeStartTimeUs; - return !isIndependent(segmentBaseHolder, mediaPlaylist) - || segmentStartTimeInPeriodUs < bufferedPositionUs; + return !isIndependent || segmentStartTimeInPeriodUs < bufferedPositionUs; } public static final String PRIV_TIMESTAMP_FRAME_OWNER = @@ -266,8 +268,8 @@ public static boolean shouldSpliceIn( /** The url of the playlist from which this chunk was obtained. */ public final Uri playlistUrl; - /** Whether samples for this chunk should be spliced into existing samples. */ - public final boolean shouldSpliceIn; + /** Whether the chunk is independent, meaning it starts with a keyframe. */ + public final boolean isIndependent; /** The part index or {@link C#INDEX_UNSET} if the chunk is a full segment */ public final int partIndex; @@ -300,6 +302,7 @@ public static boolean shouldSpliceIn( private ImmutableList sampleQueueFirstSampleIndices; private boolean extractorInvalidated; private long publishedDurationUs; + private boolean shouldSpliceIn; private HlsMediaChunk( HlsExtractorFactory extractorFactory, @@ -329,6 +332,7 @@ private HlsMediaChunk( Id3Decoder id3Decoder, ParsableByteArray scratchId3Data, boolean shouldSpliceIn, + boolean isIndependent, PlayerId playerId) { super( mediaDataSource, @@ -359,6 +363,7 @@ private HlsMediaChunk( this.id3Decoder = id3Decoder; this.scratchId3Data = scratchId3Data; this.shouldSpliceIn = shouldSpliceIn; + this.isIndependent = isIndependent; this.playerId = playerId; sampleQueueFirstSampleIndices = ImmutableList.of(); uid = uidSource.getAndIncrement(); @@ -398,6 +403,20 @@ public void invalidateExtractor() { extractorInvalidated = true; } + /** Returns whether samples for this chunk should be spliced into existing samples. */ + @SuppressWarnings("UngroupedOverloads") // Ungrouped static method with same name + public boolean shouldSpliceIn() { + return shouldSpliceIn; + } + + /** + * Clears the {@linkplain #shouldSpliceIn() flag} that indicates if this chunk should be spliced + * into existing samples. + */ + public void clearShouldSpliceIn() { + shouldSpliceIn = false; + } + @Override public boolean isLoadCompleted() { return loadCompleted; @@ -673,13 +692,4 @@ private static DataSource buildDataSource( } return dataSource; } - - private static boolean isIndependent( - HlsChunkSource.SegmentBaseHolder segmentBaseHolder, HlsMediaPlaylist mediaPlaylist) { - if (segmentBaseHolder.segmentBase instanceof HlsMediaPlaylist.Part) { - return ((HlsMediaPlaylist.Part) segmentBaseHolder.segmentBase).isIndependent - || (segmentBaseHolder.partIndex == 0 && mediaPlaylist.hasIndependentSegments); - } - return mediaPlaylist.hasIndependentSegments; - } } diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java index 9465b6e45c..59262e2a83 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java @@ -516,7 +516,10 @@ public boolean seekToUs(long positionUs, boolean forceReset) { } // If we're not forced to reset, try and seek within the buffer. - if (sampleQueuesBuilt && !forceReset && seekInsideBufferUs(positionUs, seekToMediaChunk)) { + if (sampleQueuesBuilt + && !forceReset + && !mediaChunks.isEmpty() + && seekInsideBufferUs(positionUs, seekToMediaChunk)) { return false; } @@ -821,11 +824,7 @@ public boolean continueLoading(LoadingInfo loadingInfo) { } if (isMediaChunk(loadable)) { - if (!chunkQueue.isEmpty() && !getLast(chunkQueue).isPublished()) { - // Switching chunks to a new playlist and the last chunk of the previous playlist has an - // unknown publication status, so we have to discard it. - discardUpstream(/* preferredQueueSize= */ chunkQueue.size() - 1); - } + maybeDiscardUpstreamForNewMediaChunk((HlsMediaChunk) loadable); initMediaChunkLoad((HlsMediaChunk) loadable); } loadingChunk = loadable; @@ -834,6 +833,34 @@ public boolean continueLoading(LoadingInfo loadingInfo) { return true; } + private void maybeDiscardUpstreamForNewMediaChunk(HlsMediaChunk newChunk) { + if (mediaChunks.isEmpty()) { + return; + } + if (!getLastMediaChunk().isPublished()) { + // Switching chunks to a new playlist and the last chunk of the previous playlist has an + // unknown publication status, so we have to discard it. + discardUpstream(/* preferredQueueSize= */ mediaChunks.size() - 1); + } + if (newChunk.isIndependent && newChunk.shouldSpliceIn()) { + // Attempting to splice in an independent chunk. See if that can be avoided by cleanly + // discarding existing chunks starting from the same position. + for (int i = mediaChunks.size() - 1; i >= 0; i--) { + long existingChunkStartTimeUs = mediaChunks.get(i).startTimeUs; + if (existingChunkStartTimeUs < newChunk.startTimeUs) { + // Before new start time, this is the existing chunk we need to splice into. + break; + } else if (existingChunkStartTimeUs == newChunk.startTimeUs + && canDiscardUpstreamMediaChunksFromIndex(i)) { + // Exact match, assume we can just replace the chunk entirely. + discardUpstream(/* preferredQueueSize= */ i); + newChunk.clearShouldSpliceIn(); + break; + } + } + } + } + @Override public boolean isLoading() { return loader.isLoading(); @@ -1078,7 +1105,7 @@ private void initMediaChunkLoad(HlsMediaChunk chunk) { chunk.init(/* output= */ this, sampleQueueWriteIndicesBuilder.build()); for (HlsSampleQueue sampleQueue : sampleQueues) { sampleQueue.setSourceChunk(chunk); - if (chunk.shouldSpliceIn) { + if (chunk.shouldSpliceIn()) { sampleQueue.splice(); } } @@ -1307,7 +1334,7 @@ private boolean finishedReadingChunk(HlsMediaChunk chunk) { private boolean canDiscardUpstreamMediaChunksFromIndex(int mediaChunkIndex) { for (int i = mediaChunkIndex; i < mediaChunks.size(); i++) { - if (mediaChunks.get(i).shouldSpliceIn) { + if (mediaChunks.get(i).shouldSpliceIn()) { // Discarding not possible because a spliced-in chunk potentially removed sample metadata // from the previous chunks. // TODO: Keep sample metadata to allow restoring these chunks [internal b/159904763]. diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java index 9f8cce4373..8710aba296 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java @@ -853,6 +853,14 @@ private void processLoadedPlaylist( playlistSnapshot != oldPlaylist ? playlistSnapshot.targetDurationUs : (playlistSnapshot.targetDurationUs / 2); + } else if (playlistSnapshot == oldPlaylist) { + // To prevent infinite requests when the server responds with CAN-BLOCK-RELOAD=YES but does + // not actually block until the playlist updates, wait for half the target duration before + // retrying. + durationUntilNextLoadUs = + playlistSnapshot.partTargetDurationUs != C.TIME_UNSET + ? playlistSnapshot.partTargetDurationUs / 2 + : playlistSnapshot.targetDurationUs / 2; } earliestNextLoadTimeMs = currentTimeMs + Util.usToMs(durationUntilNextLoadUs) - loadEventInfo.loadDurationMs; diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java index 099a00bc03..b8e61560d1 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java @@ -600,7 +600,7 @@ public void getNextChunk_reloadingCurrentPreloadPartAfterPublication_returnsShou /* allowEndOfStream= */ true, output); - assertThat(((HlsMediaChunk) output.chunk).shouldSpliceIn).isTrue(); + assertThat(((HlsMediaChunk) output.chunk).shouldSpliceIn()).isTrue(); } @Test @@ -645,10 +645,10 @@ public void getNextChunk_reloadingCurrentPreloadPartAfterPublication_returnsShou HlsMediaChunk secondChunk = (HlsMediaChunk) output.chunk; assertThat(firstChunk.playlistUrl).isEqualTo(PLAYLIST_URI); - assertThat(firstChunk.shouldSpliceIn).isFalse(); + assertThat(firstChunk.shouldSpliceIn()).isFalse(); assertThat(firstChunk.startTimeUs).isEqualTo(16_000_000); assertThat(secondChunk.playlistUrl).isEqualTo(PLAYLIST_URI_2); - assertThat(secondChunk.shouldSpliceIn).isFalse(); + assertThat(secondChunk.shouldSpliceIn()).isFalse(); assertThat(secondChunk.startTimeUs).isEqualTo(20_000_000); } @@ -693,10 +693,10 @@ public void getNextChunk_changedTrackSelectionWithOverlappingSegments_returnsSho HlsMediaChunk secondChunk = (HlsMediaChunk) output.chunk; assertThat(firstChunk.playlistUrl).isEqualTo(PLAYLIST_URI); - assertThat(firstChunk.shouldSpliceIn).isFalse(); + assertThat(firstChunk.shouldSpliceIn()).isFalse(); assertThat(firstChunk.startTimeUs).isEqualTo(16_000_000); assertThat(secondChunk.playlistUrl).isEqualTo(PLAYLIST_URI_2); - assertThat(secondChunk.shouldSpliceIn).isTrue(); + assertThat(secondChunk.shouldSpliceIn()).isTrue(); assertThat(secondChunk.startTimeUs).isEqualTo(16_000_000); } @@ -742,10 +742,10 @@ public void getNextChunk_changedTrackSelectionWithOverlappingSegments_returnsSho HlsMediaChunk secondChunk = (HlsMediaChunk) output.chunk; assertThat(firstChunk.playlistUrl).isEqualTo(PLAYLIST_URI); - assertThat(firstChunk.shouldSpliceIn).isFalse(); + assertThat(firstChunk.shouldSpliceIn()).isFalse(); assertThat(firstChunk.startTimeUs).isEqualTo(16_000_000); assertThat(secondChunk.playlistUrl).isEqualTo(PLAYLIST_URI); - assertThat(secondChunk.shouldSpliceIn).isFalse(); + assertThat(secondChunk.shouldSpliceIn()).isFalse(); assertThat(secondChunk.startTimeUs).isEqualTo(20_000_000); } diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java index f5119359c5..7644dad3d9 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java @@ -2471,6 +2471,53 @@ public void handleContentTimelineChanged_preRollWithAssetList_resolveAssetListIm inOrder.verifyNoMoreInteractions(); } + @Test + public void + handleContentTimelineChanged_startPositionAfterMidRollTimeUs_resolvesAndSchedulesMidRoll() + throws IOException, TimeoutException { + String playlistString = + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:30\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n" + + "#EXTINF:30,\n" + + "main0.ts\n" + + "#EXTINF:30,\n" + + "main1.ts\n" + + "#EXTINF:30,\n" + + "main2.ts\n" + + "#EXTINF:30,\n" + + "main3.ts\n" + + "#EXT-X-ENDLIST" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:00:00.000Z\"," + + "CUE=\"PRE\"," + + "X-ASSET-LIST=\"http://example.com/assetlist-0-0.json\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad1-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:00:15.000Z\"," + + "X-ASSET-LIST=\"http://example.com/assetlist-1-0.json\"" + + "\n"; + when(mockPlayer.getContentPosition()).thenReturn(52_000L); + + callHandleContentTimelineChangedAndCaptureAdPlaybackState( + playlistString, + adsLoader, + /* windowIndex= */ 0, + /* windowPositionInPeriodUs= */ 0L, + /* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE); + + runMainLooperUntil(assetListLoadingListener::completed, TIMEOUT_MS, Clock.DEFAULT); + verify(mockAdsLoaderListener) + .onAssetListLoadStarted(eq(contentMediaItem), eq("adsId"), eq(1), eq(0)); + verify(mockAdsLoaderListener) + .onAssetListLoadCompleted(eq(contentMediaItem), eq("adsId"), eq(1), eq(0), any()); + } + @Test public void handleContentTimelineChanged_assetListWithMultipleAssets_resolvesAndExpandsAdGroup() throws IOException, TimeoutException { @@ -2714,7 +2761,7 @@ public void handleContentTimelineChanged_emptyAssetList_marksAdAsFailedAndSchedu + "ID=\"ad1-0\"," + "CLASS=\"com.apple.hls.interstitial\"," + "DURATION=3.246," - + "START-DATE=\"2020-01-02T21:00:30.000Z\"," + + "START-DATE=\"2020-01-02T21:00:15.000Z\"," + "X-ASSET-LIST=\"http://example.com/assetlist-1-0.json\"" + "\n" + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n" @@ -2729,7 +2776,7 @@ public void handleContentTimelineChanged_emptyAssetList_marksAdAsFailedAndSchedu .setPlaceholder(true) .setDynamic(true) .setLive(true) - .setDefaultPositionUs(29_999_999L) + .setDefaultPositionUs(15_000_000L) .build(); callHandleContentTimelineChangedAndCaptureAdPlaybackState( diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsPreCacheHelperTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsPreCacheHelperTest.java new file mode 100644 index 0000000000..592c7e5465 --- /dev/null +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsPreCacheHelperTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2025 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.exoplayer.hls; + +import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.robolectric.Shadows.shadowOf; + +import android.os.HandlerThread; +import android.os.Looper; +import androidx.media3.common.MediaItem; +import androidx.media3.datasource.cache.Cache; +import androidx.media3.datasource.cache.NoOpCacheEvictor; +import androidx.media3.datasource.cache.SimpleCache; +import androidx.media3.exoplayer.source.preload.PreCacheHelper; +import androidx.media3.test.utils.TestUtil; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.File; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class HlsPreCacheHelperTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private Cache downloadCache; + private HandlerThread preCacheThread; + private Looper preCacheLooper; + + @Before + public void setUp() throws Exception { + File testDir = temporaryFolder.newFile("PreCacheHelperTest"); + assertThat(testDir.delete()).isTrue(); + assertThat(testDir.mkdirs()).isTrue(); + downloadCache = + new SimpleCache(testDir, new NoOpCacheEvictor(), TestUtil.getInMemoryDatabaseProvider()); + preCacheThread = new HandlerThread("preCache"); + preCacheThread.start(); + preCacheLooper = preCacheThread.getLooper(); + } + + @After + public void tearDown() { + downloadCache.release(); + preCacheThread.quit(); + } + + @Test + public void preCache_succeeds() throws Exception { + PreCacheHelper.Listener preCacheHelperListener = mock(PreCacheHelper.Listener.class); + AtomicBoolean preCacheCompleted = new AtomicBoolean(); + doAnswer( + invocation -> { + preCacheCompleted.set(true); + return null; + }) + .when(preCacheHelperListener) + .onPreCacheProgress(any(), anyLong(), anyLong(), eq(100f)); + PreCacheHelper preCacheHelper = + new PreCacheHelper.Factory( + ApplicationProvider.getApplicationContext(), downloadCache, preCacheLooper) + .setListener(preCacheHelperListener) + .create(MediaItem.fromUri("asset:///media/hls/multi-segment/playlist.m3u8")); + + preCacheHelper.preCache(/* startPositionMs= */ 0, /* durationMs= */ 2000L); + shadowOf(preCacheLooper).idle(); + runMainLooperUntil(preCacheCompleted::get); + shadowOf(Looper.getMainLooper()).idle(); + + verify(preCacheHelperListener).onPrepared(any(), any()); + verify(preCacheHelperListener, never()).onPrepareError(any(), any()); + verify(preCacheHelperListener, never()).onDownloadError(any(), any()); + + preCacheHelper.release(/* removeCachedContent= */ true); + shadowOf(preCacheLooper).idle(); + } +} diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java index e080a3a22b..e03b2293fc 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java @@ -104,12 +104,6 @@ */ private static final long THRESHOLD_END_OF_CONTENT_MS = 5000; - /** - * Threshold before the start of an ad at which IMA is expected to be able to preload the ad, in - * milliseconds. - */ - private static final long THRESHOLD_AD_PRELOAD_MS = 4000; - /** The threshold below which ad cue points are treated as matching, in microseconds. */ private static final long THRESHOLD_AD_MATCH_US = 1000; @@ -144,7 +138,6 @@ private final Handler handler; private final ComponentListener componentListener; private final ContentPlaybackAdapter contentPlaybackAdapter; - private final VideoAdPlayerImpl videoAdPlayerImpl; private final List eventListeners; private final List adCallbacks; private final Runnable updateAdProgressRunnable; @@ -265,7 +258,6 @@ public AdTagLoader( handler = Util.createHandler(getImaLooper(), /* callback= */ null); componentListener = new ComponentListener(); contentPlaybackAdapter = new ContentPlaybackAdapter(); - videoAdPlayerImpl = new VideoAdPlayerImpl(); eventListeners = new ArrayList<>(); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); if (configuration.applicationVideoAdPlayerCallback != null) { @@ -283,6 +275,7 @@ public AdTagLoader( timeline = Timeline.EMPTY; adPlaybackState = AdPlaybackState.NONE; adLoadTimeoutRunnable = this::handleAdLoadTimeout; + VideoAdPlayerImpl videoAdPlayerImpl = new VideoAdPlayerImpl(); if (adViewGroup != null) { adDisplayContainer = imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ videoAdPlayerImpl); @@ -1375,7 +1368,7 @@ public VideoProgressUpdate getContentProgress() { // may be stuck. Detect this case and signal an error if applicable. long stuckElapsedRealtimeMs = SystemClock.elapsedRealtime() - waitingForPreloadElapsedRealtimeMs; - if (stuckElapsedRealtimeMs >= THRESHOLD_AD_PRELOAD_MS) { + if (stuckElapsedRealtimeMs >= configuration.adPreloadTimeoutMs) { waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; handleAdGroupLoadError(new IOException("Ad preloading timed out")); maybeNotifyPendingAdLoadError(); diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaAdsLoader.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaAdsLoader.java index ab6b6a1240..c8be345bce 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaAdsLoader.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaAdsLoader.java @@ -280,6 +280,9 @@ public Builder setEnableContinuousPlayback(boolean enableContinuousPlayback) { *

The purpose of this timeout is to avoid playback getting stuck in the unexpected case that * the IMA SDK does not load an ad break based on the player's reported content position. * + *

The value will be adjusted to be greater or equal to the one in {@link + * #setVastLoadTimeoutMs(int)} if provided. + * * @param adPreloadTimeoutMs The timeout buffering duration in milliseconds, or {@link * C#TIME_UNSET} for no timeout. * @return This builder, for convenience. @@ -396,6 +399,9 @@ public Builder setDebugModeEnabled(boolean debugModeEnabled) { /** Returns a new {@link ImaAdsLoader}. */ public ImaAdsLoader build() { + if (vastLoadTimeoutMs != TIMEOUT_UNSET && adPreloadTimeoutMs < vastLoadTimeoutMs) { + adPreloadTimeoutMs = vastLoadTimeoutMs; + } return new ImaAdsLoader( context, new ImaUtil.Configuration( 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 53b2f6c083..20a7c165e3 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 @@ -443,9 +443,7 @@ private AdsLoader( this.configuration = configuration; mediaSourceResources = new HashMap<>(); adPlaybackStateMap = new HashMap<>(); - for (Map.Entry entry : state.adPlaybackStates.entrySet()) { - adPlaybackStateMap.put(entry.getKey(), entry.getValue()); - } + adPlaybackStateMap.putAll(state.adPlaybackStates); } /** @@ -1360,7 +1358,9 @@ public VideoProgressUpdate getContentProgress() { AdPlaybackState adPlaybackState = checkNotNull(adPlaybackStates.get(contentPeriod.uid)); // Calculate the stream position from the current position and the playback state. streamPositionMs = - usToMs(ServerSideAdInsertionUtil.getStreamPositionUs(player, adPlaybackState)); + usToMs( + ServerSideAdInsertionUtil.getStreamPositionUs( + player, checkNotNull(adPlaybackState.adsId))); if (window.windowStartTimeMs != C.TIME_UNSET) { // Add the time since epoch at start of the window for live streams. streamPositionMs += window.windowStartTimeMs + period.getPositionInWindowMs(); diff --git a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaAdsLoaderTest.java b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaAdsLoaderTest.java index a94c1a314b..dfd4db5956 100644 --- a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaAdsLoaderTest.java +++ b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaAdsLoaderTest.java @@ -503,7 +503,7 @@ public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() { /* periodIndex= */ 0, Util.usToMs(adGroupPositionInWindowUs)); fakePlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); // Advance past the timeout and simulate polling content progress. - ShadowSystemClock.advanceBy(Duration.ofSeconds(5)); + ShadowSystemClock.advanceBy(Duration.ofSeconds(11)); contentProgressProvider.getContentProgress(); assertThat(getAdPlaybackState(/* periodIndex= */ 0)) @@ -515,6 +515,114 @@ public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() { .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)); } + @Test + public void playback_withCustomAdPreloadTimeout_triggersErrorAfterTimeout() { + imaAdsLoader = + new ImaAdsLoader.Builder(getApplicationContext()) + .setImaFactory(mockImaFactory) + .setImaSdkSettings(mockImaSdkSettings) + .setAdPreloadTimeoutMs(3_000) + .build(); + imaAdsLoader.setPlayer(fakePlayer); + adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(CONTENT_TIMELINE), + TEST_DATA_SPEC, + TEST_ADS_ID, + new DefaultMediaSourceFactory((Context) getApplicationContext()), + imaAdsLoader, + adViewProvider, + /* useLazyContentSourcePreparation= */ true); + + // Simulate an ad at 2 seconds. + long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND; + long adGroupTimeUs = + adGroupPositionInWindowUs + + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; + ImmutableList cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); + + // Advance playback to just before the midroll and simulate buffering. + imaAdsLoader.start( + adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); + fakePlayer.setPlayingContentPosition( + /* periodIndex= */ 0, Util.usToMs(adGroupPositionInWindowUs)); + fakePlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); + + // Advance past the custom timeout. + ShadowSystemClock.advanceBy(Duration.ofSeconds(4)); + contentProgressProvider.getContentProgress(); + + // Verify that the ad group is in an error state. + assertThat(getAdPlaybackState(/* periodIndex= */ 0)) + .isEqualTo( + new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) + .withContentDurationUs(CONTENT_PERIOD_DURATION_US) + .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}}) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)); + } + + @Test + public void + playback_withAdPreloadTimeoutLessThanVastLoadTimeout_adPreloadTimeoutIncreasedToVastLoadTimeout() { + imaAdsLoader = + new ImaAdsLoader.Builder(getApplicationContext()) + .setImaFactory(mockImaFactory) + .setImaSdkSettings(mockImaSdkSettings) + .setVastLoadTimeoutMs(5_000) + .setAdPreloadTimeoutMs(3_000) + .build(); + imaAdsLoader.setPlayer(fakePlayer); + adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(CONTENT_TIMELINE), + TEST_DATA_SPEC, + TEST_ADS_ID, + new DefaultMediaSourceFactory((Context) getApplicationContext()), + imaAdsLoader, + adViewProvider, + /* useLazyContentSourcePreparation= */ true); + + // Simulate an ad at 2 seconds. + long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND; + long adGroupTimeUs = + adGroupPositionInWindowUs + + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; + ImmutableList cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND); + when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints); + + // Advance playback to just before the midroll and simulate buffering. + imaAdsLoader.start( + adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); + fakePlayer.setPlayingContentPosition( + /* periodIndex= */ 0, Util.usToMs(adGroupPositionInWindowUs)); + fakePlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); + + // Advance past the original adPreloadTimeout (3s) but not the vastLoadTimeout (5s). + ShadowSystemClock.advanceBy(Duration.ofSeconds(4)); + contentProgressProvider.getContentProgress(); + + // Verify that the ad has not errored out, as the timeout should be 5s. + assertThat(getAdPlaybackState(/* periodIndex= */ 0)) + .isEqualTo( + new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) + .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); + + // Advance past the vastLoadTimeout. + ShadowSystemClock.advanceBy(Duration.ofSeconds(2)); // Total advance is 6s. + contentProgressProvider.getContentProgress(); + + // Verify that the ad group is now in an error state. + assertThat(getAdPlaybackState(/* periodIndex= */ 0)) + .isEqualTo( + new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints)) + .withContentDurationUs(CONTENT_PERIOD_DURATION_US) + .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}}) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)); + } + @Test public void startPlaybackAfterMidroll_doesNotSkipMidroll() { // Simulate an ad at 2 seconds, and starting playback with an initial seek position at the ad. @@ -561,7 +669,7 @@ public void startPlaybackAfterMidroll_withAdNotPreloadingAfterTimeout_hasErrorAd imaAdsLoader.start( adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener); contentProgressProvider.getContentProgress(); - ShadowSystemClock.advanceBy(Duration.ofSeconds(5)); + ShadowSystemClock.advanceBy(Duration.ofSeconds(11)); contentProgressProvider.getContentProgress(); assertThat(getAdPlaybackState(/* periodIndex= */ 0)) diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/SessionDescriptionParser.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/SessionDescriptionParser.java index e37805dd2a..49258e6ad2 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/SessionDescriptionParser.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/SessionDescriptionParser.java @@ -85,7 +85,7 @@ public static SessionDescription parse(String sdpString) throws ParserException for (String line : RtspMessageUtil.splitRtspMessageBody(sdpString)) { line = line.trim(); - if ("".equals(line)) { + if (line.isEmpty()) { continue; } diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH265Reader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH265Reader.java index e59e680a2c..9f2df57f5d 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH265Reader.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH265Reader.java @@ -102,8 +102,7 @@ public void consume(ParsableByteArray data, long timestamp, int sequenceNumber, if (payloadType >= 0 && payloadType < RTP_PACKET_TYPE_AP) { processSingleNalUnitPacket(data); } else if (payloadType == RTP_PACKET_TYPE_AP) { - // TODO: Support AggregationPacket mode. - throw new UnsupportedOperationException("need to implement processAggregationPacket"); + processAggregationPacket(data); } else if (payloadType == RTP_PACKET_TYPE_FU) { processFragmentationUnitPacket(data, sequenceNumber); } else { @@ -167,6 +166,68 @@ private void processSingleNalUnitPacket(ParsableByteArray data) { bufferFlags = getBufferFlagsFromNalType(nalHeaderType); } + /** + * Processes Aggregation packet (RFC7798 Section 4.4.2). + * + *

Outputs 2 or more NAL Unit (with start code prepended) to {@link #trackOutput}. Sets {@link + * #bufferFlags} and {@link #fragmentedSampleSizeBytes} accordingly. + */ + @RequiresNonNull("trackOutput") + private void processAggregationPacket(ParsableByteArray data) throws ParserException { + // The structure of an Aggregation Packet. + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | PayloadHdr (Type=48) | | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + // | | + // | two or more aggregation units | + // | | + // | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | :...OPTIONAL RTP padding | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // The structure of aggregation unit + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // : DONL (conditional) | NALU size | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | NALU size | | + // +-+-+-+-+-+-+-+-+ NAL unit | + // | | + // | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | : + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + // Since sprop-max-don-diff != 0 is not supported, DONL won't present in the packet. + + int nalUnitsCount = 0; + data.setPosition(2); // skipping payload header (2 bytes) + while (data.bytesLeft() > 2) { + int nalUnitSize = data.readUnsignedShort(); // 2 bytes of NAL unit size + int nalHeaderType = NalUnitUtil.getH265NalUnitType(data.getData(), data.getPosition() - 3); + if (data.bytesLeft() < nalUnitSize) { + throw ParserException.createForMalformedManifest( + "Malformed Aggregation Packet. NAL unit size exceeds packet size.", /* cause= */ null); + } + + fragmentedSampleSizeBytes += writeStartCode(); + trackOutput.sampleData(data, nalUnitSize); + fragmentedSampleSizeBytes += nalUnitSize; + bufferFlags |= getBufferFlagsFromNalType(nalHeaderType); + nalUnitsCount++; + } + if (data.bytesLeft() > 0) { + throw ParserException.createForMalformedManifest( + "Malformed Aggregation Packet. Packet size exceeds NAL unit size.", /* cause= */ null); + } + if (nalUnitsCount < 2) { + throw ParserException.createForMalformedManifest( + "Aggregation Packet must contain at least 2 NAL units.", /* cause= */ null); + } + } + /** * Processes Fragmentation Unit packet (RFC7798 Section 4.4.3). * diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriodTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriodTest.java index 6e000cd6b7..2c54d3abd3 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriodTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriodTest.java @@ -48,7 +48,6 @@ public final class RtspMediaPeriodTest { private static final long DEFAULT_TIMEOUT_MS = 8000; private final AtomicReference trackGroupAtomicReference = new AtomicReference<>(); - ; private final MediaPeriod.Callback mediaPeriodCallback = new MediaPeriod.Callback() { @Override diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH265ReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH265ReaderTest.java new file mode 100644 index 0000000000..357bd4ae84 --- /dev/null +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH265ReaderTest.java @@ -0,0 +1,294 @@ +/* + * Copyright 2025 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.rtsp.reader; + +import static androidx.media3.common.util.Util.getBytesFromHexString; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.ParserException; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.Util; +import androidx.media3.container.NalUnitUtil; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.test.utils.FakeExtractorOutput; +import androidx.media3.test.utils.FakeTrackOutput; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.Bytes; +import java.util.Arrays; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link RtpH265Reader}. */ +@RunWith(AndroidJUnit4.class) +public class RtpH265ReaderTest { + + private static final long MEDIA_CLOCK_FREQUENCY = 90_000; + private static final long AP_PACKET_RTP_TIMESTAMP = 9_000_000; + private static final long AP_PACKET_2_RTP_TIMESTAMP = 9_000_040; + private static final int AP_PACKET_SEQUENCE_NUMBER = 12345; + private static final long SINGLE_NALU_PACKET_1_RTP_TIMESTAMP = 9_000_040; + private static final long SINGLE_NALU_PACKET_2_RTP_TIMESTAMP = 9_000_080; + + private static final byte[] AP_NALU_HEADER = getBytesFromHexString("6001"); + private static final byte[] NALU_1_LENGTH = getBytesFromHexString("000c"); + private static final byte[] NALU_1_INVALID_LENGTH = getBytesFromHexString("00ff"); + private static final byte[] NALU_1_HEADER = getBytesFromHexString("4001"); + private static final byte[] NALU_1_PAYLOAD = getBytesFromHexString("0102030405060708090a"); + private static final byte[] NALU_2_LENGTH = getBytesFromHexString("000e"); + private static final byte[] NALU_2_HEADER = getBytesFromHexString("4201"); + private static final byte[] NALU_2_PAYLOAD = getBytesFromHexString("1112131415161718191a1b1c"); + + private static final RtpPacket SINGLE_NALU_PACKET_1 = + new RtpPacket.Builder() + .setTimestamp(SINGLE_NALU_PACKET_1_RTP_TIMESTAMP) + .setSequenceNumber(AP_PACKET_SEQUENCE_NUMBER + 1) + .setMarker(true) + .setPayloadData(Bytes.concat(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + + private static final RtpPacket SINGLE_NALU_PACKET_2 = + new RtpPacket.Builder() + .setTimestamp(SINGLE_NALU_PACKET_2_RTP_TIMESTAMP) + .setSequenceNumber(AP_PACKET_SEQUENCE_NUMBER + 2) + .setMarker(true) + .setPayloadData(Bytes.concat(NALU_2_HEADER, NALU_2_PAYLOAD)) + .build(); + + private static final RtpPacket VALID_AP_PACKET = + createAggregationPacket( + AP_PACKET_SEQUENCE_NUMBER, + AP_PACKET_RTP_TIMESTAMP, + NALU_1_LENGTH, + NALU_1_HEADER, + NALU_1_PAYLOAD, + NALU_2_LENGTH, + NALU_2_HEADER, + NALU_2_PAYLOAD); + + private static final RtpPacket VALID_AP_PACKET_2 = + createAggregationPacket( + AP_PACKET_SEQUENCE_NUMBER + 1, + AP_PACKET_2_RTP_TIMESTAMP, + NALU_1_LENGTH, + NALU_1_HEADER, + NALU_1_PAYLOAD, + NALU_2_LENGTH, + NALU_2_HEADER, + NALU_2_PAYLOAD); + + private static final RtpPacket INVALID_AP_PACKET_EXTRA_BYTE = + createAggregationPacket( + AP_PACKET_SEQUENCE_NUMBER, + AP_PACKET_RTP_TIMESTAMP, + NALU_1_LENGTH, + NALU_1_HEADER, + NALU_1_PAYLOAD, + NALU_2_LENGTH, + NALU_2_HEADER, + NALU_2_PAYLOAD, + new byte[] {0x0a}); + + private static final RtpPacket INVALID_AP_PACKET_MISSING_BYTE = + createAggregationPacket( + AP_PACKET_SEQUENCE_NUMBER, + AP_PACKET_RTP_TIMESTAMP, + NALU_1_LENGTH, + NALU_1_HEADER, + NALU_1_PAYLOAD, + NALU_2_LENGTH, + NALU_2_HEADER, + Arrays.copyOf(NALU_2_PAYLOAD, NALU_2_PAYLOAD.length - 1)); + + private static final RtpPacket INVALID_AP_PACKET_INVALID_NALU_LENGTH = + createAggregationPacket( + AP_PACKET_SEQUENCE_NUMBER, + AP_PACKET_RTP_TIMESTAMP, + NALU_1_INVALID_LENGTH, + NALU_1_HEADER, + NALU_1_PAYLOAD, + NALU_2_LENGTH, + NALU_2_HEADER); + + private static final RtpPacket INVALID_AP_PACKET_SINGLE_NALU = + createAggregationPacket( + AP_PACKET_SEQUENCE_NUMBER, + AP_PACKET_RTP_TIMESTAMP, + NALU_1_LENGTH, + NALU_1_HEADER, + NALU_1_PAYLOAD); + + private static final RtpPayloadFormat H265_FORMAT = + new RtpPayloadFormat( + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H265) + .setWidth(1920) + .setHeight(1080) + .build(), + /* rtpPayloadType= */ 98, + /* clockRate= */ (int) MEDIA_CLOCK_FREQUENCY, + /* fmtpParameters= */ ImmutableMap.of( + "packetization-mode", "1", + "profile-level-id", "010101", + "sprop-pps", "RAHA4MisDBRSQA==", + "sprop-sps", "QgEBAUAAAAMAgAAAAwAAAwC0oAPAgBDlja5JG2a5cQB/FiU=", + "sprop-vps", "QAEMAf//AUAAAAMAgAAAAwAAAwC0rAk="), + RtpPayloadFormat.RTP_MEDIA_H265); + + private FakeExtractorOutput extractorOutput; + private RtpH265Reader rtpH265Reader; + + @Before + public void setUp() { + extractorOutput = new FakeExtractorOutput(); + rtpH265Reader = new RtpH265Reader(H265_FORMAT); + } + + @Test + public void consume_validPackets() throws ParserException { + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + rtpH265Reader.onReceivingFirstPacket(VALID_AP_PACKET.timestamp, VALID_AP_PACKET.sequenceNumber); + consume(rtpH265Reader, VALID_AP_PACKET); + consume(rtpH265Reader, VALID_AP_PACKET_2); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)) + .isEqualTo( + Bytes.concat( + NalUnitUtil.NAL_START_CODE, + NALU_1_HEADER, + NALU_1_PAYLOAD, + NalUnitUtil.NAL_START_CODE, + NALU_2_HEADER, + NALU_2_PAYLOAD)); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)) + .isEqualTo( + Bytes.concat( + NalUnitUtil.NAL_START_CODE, + NALU_1_HEADER, + NALU_1_PAYLOAD, + NalUnitUtil.NAL_START_CODE, + NALU_2_HEADER, + NALU_2_PAYLOAD)); + assertThat(trackOutput.getSampleTimeUs(1)) + .isEqualTo( + Util.scaleLargeTimestamp( + (AP_PACKET_2_RTP_TIMESTAMP - AP_PACKET_RTP_TIMESTAMP), + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ MEDIA_CLOCK_FREQUENCY)); + } + + @Test + public void consume_validPacketsMixedAggregationAndSingleNalu() throws ParserException { + long naluPacket1PresentationTimestampUs = + Util.scaleLargeTimestamp( + (SINGLE_NALU_PACKET_1_RTP_TIMESTAMP - AP_PACKET_RTP_TIMESTAMP), + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ MEDIA_CLOCK_FREQUENCY); + long naluPacket2PresentationTimestampUs = + Util.scaleLargeTimestamp( + (SINGLE_NALU_PACKET_2_RTP_TIMESTAMP - AP_PACKET_RTP_TIMESTAMP), + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ MEDIA_CLOCK_FREQUENCY); + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + rtpH265Reader.onReceivingFirstPacket(VALID_AP_PACKET.timestamp, VALID_AP_PACKET.sequenceNumber); + consume(rtpH265Reader, VALID_AP_PACKET); + consume(rtpH265Reader, SINGLE_NALU_PACKET_1); + consume(rtpH265Reader, SINGLE_NALU_PACKET_2); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(3); + assertThat(trackOutput.getSampleData(0)) + .isEqualTo( + Bytes.concat( + NalUnitUtil.NAL_START_CODE, + NALU_1_HEADER, + NALU_1_PAYLOAD, + NalUnitUtil.NAL_START_CODE, + NALU_2_HEADER, + NALU_2_PAYLOAD)); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)) + .isEqualTo(Bytes.concat(NalUnitUtil.NAL_START_CODE, NALU_1_HEADER, NALU_1_PAYLOAD)); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(naluPacket1PresentationTimestampUs); + assertThat(trackOutput.getSampleData(2)) + .isEqualTo(Bytes.concat(NalUnitUtil.NAL_START_CODE, NALU_2_HEADER, NALU_2_PAYLOAD)); + assertThat(trackOutput.getSampleTimeUs(2)).isEqualTo(naluPacket2PresentationTimestampUs); + } + + @Test + public void consume_invalidAggregationPacketwithExtraByte_throwsParserException() { + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + rtpH265Reader.onReceivingFirstPacket( + INVALID_AP_PACKET_EXTRA_BYTE.timestamp, INVALID_AP_PACKET_EXTRA_BYTE.sequenceNumber); + assertThrows(ParserException.class, () -> consume(rtpH265Reader, INVALID_AP_PACKET_EXTRA_BYTE)); + } + + @Test + public void consume_invalidAggregationPacketwithMissingByte_throwsParserException() { + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + rtpH265Reader.onReceivingFirstPacket( + INVALID_AP_PACKET_MISSING_BYTE.timestamp, INVALID_AP_PACKET_MISSING_BYTE.sequenceNumber); + assertThrows( + ParserException.class, () -> consume(rtpH265Reader, INVALID_AP_PACKET_MISSING_BYTE)); + } + + @Test + public void consume_invalidAggregationPacketWithInvalidNaluLength_throwsParserException() { + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + rtpH265Reader.onReceivingFirstPacket( + INVALID_AP_PACKET_INVALID_NALU_LENGTH.timestamp, + INVALID_AP_PACKET_INVALID_NALU_LENGTH.sequenceNumber); + assertThrows( + ParserException.class, () -> consume(rtpH265Reader, INVALID_AP_PACKET_INVALID_NALU_LENGTH)); + } + + @Test + public void consume_invalidAggregationPacketWithSingleNalu_throwsParserException() { + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + rtpH265Reader.onReceivingFirstPacket( + INVALID_AP_PACKET_SINGLE_NALU.timestamp, INVALID_AP_PACKET_SINGLE_NALU.sequenceNumber); + assertThrows( + ParserException.class, () -> consume(rtpH265Reader, INVALID_AP_PACKET_SINGLE_NALU)); + } + + private static RtpPacket createAggregationPacket( + int sequenceNumber, long timeStamp, byte[]... nalUnits) { + return new RtpPacket.Builder() + .setTimestamp(timeStamp) + .setSequenceNumber(sequenceNumber) + .setMarker(true) + .setPayloadData(Bytes.concat(AP_NALU_HEADER, Bytes.concat(nalUnits))) + .build(); + } + + private static void consume(RtpH265Reader h265Reader, RtpPacket rtpPacket) + throws ParserException { + h265Reader.consume( + new ParsableByteArray(rtpPacket.payloadData), + rtpPacket.timestamp, + rtpPacket.sequenceNumber, + rtpPacket.marker); + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac4Util.java b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac4Util.java index 16131bf9d8..5ccfbf66d5 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/Ac4Util.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/Ac4Util.java @@ -25,6 +25,7 @@ import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.ParserException; +import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableBitArray; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; @@ -118,6 +119,8 @@ private SyncFrameInfo( /** Maximum rate for an AC-4 audio stream, in bytes per second. */ public static final int MAX_RATE_BYTES_PER_SECOND = 2688 * 1000 / 8; + private static final String TAG = "Ac4Util"; + /** The channel count of AC-4 stream. */ // TODO: Parse AC-4 stream channel count. private static final int CHANNEL_COUNT_2 = 2; @@ -415,28 +418,61 @@ public static Format parseAc4AnnexEFormat( ac4Presentation.hasBackChannels, ac4Presentation.topChannelPairs); } else { - // The ETSI TS 103 190-2 V1.2.1 (2018-02) specification defines the parameter - // n_umx_objects_minus1 in Annex E (E.11.11) to specify the number of fullband objects. While - // the elementary stream specification (section 6.3.2.8.1 and 6.3.2.10.4) provides information - // about the presence of an LFE channel within the set of dynamic objects, this detail is not - // explicitly stated in the ISO Base Media File Format (Annex E). However, current - // implementation practices consistently include the LFE channel when creating an object-based - // substream. As a result, it has been decided that when interpreting the ISO Base Media File - // Format, the LFE channel should always be counted as part of the total channel count. - int lfeChannelCount = 1; - channelCount = ac4Presentation.numOfUmxObjects + lfeChannelCount; - // TODO: There is a bug in ETSI TS 103 190-2 V1.2.1 (2018-02), E.11.11 - // For AC-4 level 4 stream, the intention is to set 19 to n_umx_objects_minus1 but it is - // equal to 15 based on current specification. Dolby has filed a bug report to ETSI. - // The following sentence should be deleted after ETSI specification error is fixed. - if (ac4Presentation.level == 4) { - channelCount = channelCount == 17 ? 21 : channelCount; + if (ac4Presentation.numOfUmxObjects > 0) { + // The ETSI TS 103 190-2 V1.2.1 (2018-02) specification defines the parameter + // n_umx_objects_minus1 in Annex E (E.11.11) to specify the number of fullband objects. + // While the elementary stream specification (section 6.3.2.8.1 and 6.3.2.10.4) provides + // information about the presence of an LFE channel within the set of dynamic objects, this + // detail is not explicitly stated in the ISO Base Media File Format (Annex E). However, + // current implementation practices consistently include the LFE channel when creating an + // object-based substream. As a result, it has been decided that when interpreting the ISO + // Base Media File Format, the LFE channel should always be counted as part of the total + // channel count. + int lfeChannelCount = 1; + channelCount = ac4Presentation.numOfUmxObjects + lfeChannelCount; + // TODO: There is a bug in ETSI TS 103 190-2 V1.2.1 (2018-02), E.11.11 + // For AC-4 level 4 stream, the intention is to set 19 to n_umx_objects_minus1 but it is + // equal to 15 based on current specification. Dolby has filed a bug report to ETSI. + // The following sentence should be deleted after ETSI specification error is fixed. + if (ac4Presentation.level == 4) { + channelCount = channelCount == 17 ? 21 : channelCount; + } + } else { + // This presentation includes a substream with discrete objects. Due to limitations in the + // current AC-4 specification (ETSI TS 103 190-2 V1.2.1 (2018-02)), discrete object number + // information is not explicitly stated in the ISO Base Media File Format (Annex E). To + // prevent exceptions, "Maximum number of tracks if audio presentation includes object + // audio" in table 77 of ETSI TS 103 190-2 V1.2.1 (2018-02) is used as the channel count. + switch (ac4Presentation.level) { + case 0: + // This should not happen because level 0 is always channel coded. + channelCount = 2; + break; + case 1: + channelCount = 6; + break; + case 2: + channelCount = 8; + break; + case 3: + channelCount = 10; + break; + case 4: + channelCount = 12; + break; + default: + // For forward-compatibility, default to 2 channels for unknown future AC-4 levels + // rather than throwing an exception. This allows the device to attempt playback. + Log.w(TAG, "AC-4 level " + ac4Presentation.level + " has not been defined."); + channelCount = 2; + break; + } } } if (channelCount <= 0) { throw ParserException.createForUnsupportedContainerFeature( - "Can't determine channel count of presentation."); + "Cannot determine channel count of presentation."); } String codecString = 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 f75fe54c18..8b205f877a 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java @@ -440,7 +440,7 @@ public synchronized Extractor[] createExtractors( addExtractorsForFileType(fileType, extractors); } } - return extractors.toArray(new Extractor[extractors.size()]); + return extractors.toArray(new Extractor[0]); } private void addExtractorsForFileType(@FileTypes.Type int fileType, List extractors) { diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/JpegMotionPhotoExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/JpegMotionPhotoExtractor.java index 541f6af5d9..a6c02ffa1b 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/JpegMotionPhotoExtractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/JpegMotionPhotoExtractor.java @@ -43,7 +43,11 @@ import java.lang.annotation.Target; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** Extracts JPEG metadata and motion photo using the Exif format. */ +/** + * Extracts JPEG motion photos following the Android Motion Photo + * format 1.0. + */ /* package */ final class JpegMotionPhotoExtractor implements Extractor { /** Parser states. */ @@ -67,8 +71,6 @@ private static final int STATE_READING_MOTION_PHOTO_VIDEO = 5; private static final int STATE_ENDED = 6; - private static final int EXIF_ID_CODE_LENGTH = 6; - private static final long EXIF_HEADER = 0x45786966; // Exif private static final int MARKER_SOI = 0xFFD8; // Start of image marker private static final int MARKER_SOS = 0xFFDA; // Start of scan (image data) marker private static final int MARKER_APP0 = 0xFFE0; // Application data 0 marker @@ -90,31 +92,23 @@ @Nullable private Mp4Extractor mp4Extractor; public JpegMotionPhotoExtractor() { - scratch = new ParsableByteArray(EXIF_ID_CODE_LENGTH); + scratch = new ParsableByteArray(/* limit= */ 2); mp4StartPosition = C.INDEX_UNSET; } @Override public boolean sniff(ExtractorInput input) throws IOException { - // See ITU-T.81 (1992) subsection B.1.1.3 and Exif version 2.2 (2002) subsection 4.5.4. + // See ITU-T.81 (1992) subsection B.1.1.3. if (peekMarker(input) != MARKER_SOI) { return false; } marker = peekMarker(input); - // Even though JFIF and Exif standards are incompatible in theory, Exif files often contain a - // JFIF APP0 marker segment preceding the Exif APP1 marker segment. Skip the JFIF segment if - // present. + // Skip the JFIF segment if present. if (marker == MARKER_APP0) { advancePeekPositionToNextSegment(input); marker = peekMarker(input); } - if (marker != MARKER_APP1) { - return false; - } - input.advancePeekPosition(2); // Unused segment length - scratch.reset(/* limit= */ EXIF_ID_CODE_LENGTH); - input.peekFully(scratch.getData(), /* offset= */ 0, EXIF_ID_CODE_LENGTH); - return scratch.readUnsignedInt() == EXIF_HEADER && scratch.readUnsignedShort() == 0; // Exif\0\0 + return marker == MARKER_APP1; } @Override diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/BoxParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/BoxParser.java index fe15713e99..90435667bd 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/BoxParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/BoxParser.java @@ -17,6 +17,7 @@ import static androidx.media3.common.MimeTypes.getMimeTypeFromMp4ObjectType; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.castNonNull; import static java.lang.Math.max; import static java.nio.ByteOrder.BIG_ENDIAN; @@ -54,7 +55,9 @@ import androidx.media3.extractor.HevcConfig; import androidx.media3.extractor.OpusUtil; import androidx.media3.extractor.VorbisUtil; +import androidx.media3.extractor.text.vobsub.VobsubParser; import com.google.common.base.Function; +import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Ints; import java.math.RoundingMode; @@ -96,6 +99,9 @@ public final class BoxParser { @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_subt = 0x73756274; + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_subp = 0x73756270; + @SuppressWarnings("ConstantCaseForConstants") private static final int TYPE_text = 0x74657874; @@ -364,14 +370,7 @@ public static Track parseTrak( throw ParserException.createForMalformedContainer( "Malformed sample table (stbl) missing sample description (stsd)", /* cause= */ null); } - StsdData stsdData = - parseStsd( - stsd.data, - tkhdData.id, - tkhdData.rotationDegrees, - mdhdData.language, - drmInitData, - isQuickTime); + StsdData stsdData = parseStsd(stsd.data, tkhdData, mdhdData.language, drmInitData, isQuickTime); @Nullable long[] editListDurations = null; @Nullable long[] editListMediaTimes = null; if (!ignoreEditLists) { @@ -976,8 +975,14 @@ private static TkhdData parseTkhd(ParsableByteArray tkhd) { // Only 0, 90, 180 and 270 are supported. Treat anything else as 0. rotationDegrees = 0; } + // skip remaining 4 matrix entries + tkhd.skipBytes(16); + // ignore fractional part of width and height + int width = tkhd.readShort(); + tkhd.skipBytes(2); + int height = tkhd.readShort(); - return new TkhdData(trackId, duration, alternateGroup, rotationDegrees); + return new TkhdData(trackId, duration, alternateGroup, rotationDegrees, width, height); } /** @@ -997,7 +1002,11 @@ private static int parseHdlr(ParsableByteArray hdlr) { return C.TRACK_TYPE_AUDIO; } else if (hdlr == TYPE_vide) { return C.TRACK_TYPE_VIDEO; - } else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) { + } else if (hdlr == TYPE_text + || hdlr == TYPE_sbtl + || hdlr == TYPE_subt + || hdlr == TYPE_clcp + || hdlr == TYPE_subp) { return C.TRACK_TYPE_TEXT; } else if (hdlr == TYPE_meta) { return C.TRACK_TYPE_METADATA; @@ -1066,8 +1075,7 @@ private static String getLanguageFromCode(int languageCode) { * Parses a stsd atom (defined in ISO/IEC 14496-12). * * @param stsd The stsd atom to decode. - * @param trackId The track's identifier in its container. - * @param rotationDegrees The rotation of the track in degrees. + * @param tkhdData The track header data from the tkhd box. * @param language The language of the track, or {@code null} if unset. * @param drmInitData {@link DrmInitData} to be included in the format, or {@code null}. * @param isQuickTime True for QuickTime media. False otherwise. @@ -1075,8 +1083,7 @@ private static String getLanguageFromCode(int languageCode) { */ private static StsdData parseStsd( ParsableByteArray stsd, - int trackId, - int rotationDegrees, + TkhdData tkhdData, @Nullable String language, @Nullable DrmInitData drmInitData, boolean isQuickTime) @@ -1112,9 +1119,9 @@ private static StsdData parseStsd( childAtomType, childStartPosition, childAtomSize, - trackId, + tkhdData.id, language, - rotationDegrees, + tkhdData.rotationDegrees, drmInitData, out, i); @@ -1151,7 +1158,7 @@ private static StsdData parseStsd( childAtomType, childStartPosition, childAtomSize, - trackId, + tkhdData.id, language, isQuickTime, drmInitData, @@ -1161,15 +1168,16 @@ private static StsdData parseStsd( || childAtomType == Mp4Box.TYPE_tx3g || childAtomType == Mp4Box.TYPE_wvtt || childAtomType == Mp4Box.TYPE_stpp - || childAtomType == Mp4Box.TYPE_c608) { + || childAtomType == Mp4Box.TYPE_c608 + || childAtomType == Mp4Box.TYPE_mp4s) { parseTextSampleEntry( - stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, out); + stsd, childAtomType, childStartPosition, childAtomSize, tkhdData, language, out); } else if (childAtomType == Mp4Box.TYPE_mett) { - parseMetaDataSampleEntry(stsd, childAtomType, childStartPosition, trackId, out); + parseMetaDataSampleEntry(stsd, childAtomType, childStartPosition, tkhdData.id, out); } else if (childAtomType == Mp4Box.TYPE_camm) { out.format = new Format.Builder() - .setId(trackId) + .setId(tkhdData.id) .setSampleMimeType(MimeTypes.APPLICATION_CAMERA_MOTION) .build(); } @@ -1183,7 +1191,7 @@ private static void parseTextSampleEntry( int atomType, int position, int atomSize, - int trackId, + TkhdData tkhdData, @Nullable String language, StsdData out) { parent.setPosition(position + Mp4Box.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); @@ -1192,7 +1200,7 @@ private static void parseTextSampleEntry( @Nullable ImmutableList initializationData = null; long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE; - String mimeType; + @Nullable String mimeType = null; if (atomType == Mp4Box.TYPE_TTML) { mimeType = MimeTypes.APPLICATION_TTML; } else if (atomType == Mp4Box.TYPE_tx3g) { @@ -1210,19 +1218,68 @@ private static void parseTextSampleEntry( // Defined by the QuickTime File Format specification. mimeType = MimeTypes.APPLICATION_MP4CEA608; out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT; + } else if (atomType == Mp4Box.TYPE_mp4s) { + int pos = parent.getPosition(); + parent.skipBytes(4); // child atom size + int childAtomType = parent.readInt(); + if (childAtomType == Mp4Box.TYPE_esds) { + EsdsData esds = parseEsdsFromParent(parent, pos); + if (esds.initializationData == null || esds.initializationData.length != 64) { + return; + } + mimeType = MimeTypes.APPLICATION_VOBSUB; + String idx = formatVobsubIdx(esds.initializationData, tkhdData.width, tkhdData.height); + initializationData = ImmutableList.of(Util.getUtf8Bytes(idx)); + } } else { // Never happens. throw new IllegalStateException(); } - out.format = - new Format.Builder() - .setId(trackId) - .setSampleMimeType(mimeType) - .setLanguage(language) - .setSubsampleOffsetUs(subsampleOffsetUs) - .setInitializationData(initializationData) - .build(); + if (mimeType != null) { + out.format = + new Format.Builder() + .setId(tkhdData.id) + .setSampleMimeType(mimeType) + .setLanguage(language) + .setSubsampleOffsetUs(subsampleOffsetUs) + .setInitializationData(initializationData) + .build(); + } + } + + /** + * Format {@link EsdsData#initializationData} as a VobSub IDX string for consumption by {@link + * VobsubParser}. + */ + private static String formatVobsubIdx(byte[] src, int width, int height) { + checkState(src.length == 64); + List palette = new ArrayList<>(16); + for (int i = 0; i < src.length - 3; i += 4) { + int yuv = Ints.fromBytes(src[i], src[i + 1], src[i + 2], src[i + 3]); + palette.add(String.format("%06x", vobsubYuvToRgb(yuv))); + } + return "size: " + width + "x" + height + "\npalette: " + Joiner.on(", ").join(palette) + "\n"; + } + + /** + * Convert a VobSub YUV palette color (as stored in {@link EsdsData#initializationData}) to RGB + * (as consumed by {@link VobsubParser}). + * + *

This uses conversion coefficients derived from BT.601. + */ + private static int vobsubYuvToRgb(int yuv) { + int y = (yuv >> 16) & 0xFF; + int v = (yuv >> 8) & 0xFF; + int u = yuv & 0xFF; + + int r = y + 14075 * (v - 128) / 10000; + int g = y - 3455 * (u - 128) / 10000 - 7169 * (v - 128) / 10000; + int b = y + 17790 * (u - 128) / 10000; + + return (Util.constrainValue(r, 0, 255) << 16) + | (Util.constrainValue(g, 0, 255) << 8) + | Util.constrainValue(b, 0, 255); } // hdrStaticInfo is allocated using allocate() in allocateHdrStaticInfo(). @@ -2204,6 +2261,7 @@ private static void parseAudioSampleEntry( int configObusSize = parent.readUnsignedLeb128ToInt(); byte[] initializationDataBytes = new byte[configObusSize]; parent.readBytes(initializationDataBytes, /* offset= */ 0, configObusSize); + codecs = CodecSpecificDataUtil.buildIamfCodecString(initializationDataBytes); initializationData = ImmutableList.of(initializationDataBytes); } else if (childAtomType == Mp4Box.TYPE_pcmC) { // See ISO 23003-5 for the definition of the pcmC box. @@ -2614,12 +2672,17 @@ private static final class TkhdData { private final long duration; private final int alternateGroup; private final int rotationDegrees; + private final int width; + private final int height; - public TkhdData(int id, long duration, int alternateGroup, int rotationDegrees) { + public TkhdData( + int id, long duration, int alternateGroup, int rotationDegrees, int width, int height) { this.id = id; this.duration = duration; this.alternateGroup = alternateGroup; this.rotationDegrees = rotationDegrees; + this.width = width; + this.height = height; } } 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 e1a39e3fc2..4ec987c91b 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 @@ -32,6 +32,7 @@ import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData.SchemeData; import androidx.media3.common.Format; +import androidx.media3.common.Metadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.ParserException; import androidx.media3.common.util.Log; @@ -483,6 +484,8 @@ public void init(ExtractorOutput output) { enterReadingAtomHeaderState(); initExtraTracks(); if (sideloadedTrack != null) { + Format.Builder formatBuilder = sideloadedTrack.format.buildUpon(); + formatBuilder.setContainerMimeType(getContainerMimeType(sideloadedTrack.format)); TrackBundle bundle = new TrackBundle( extractorOutput.track(0, sideloadedTrack.type), @@ -499,7 +502,7 @@ public void init(ExtractorOutput output) { /* duration= */ 0, /* size= */ 0, /* flags= */ 0), - getContainerMimeType(sideloadedTrack.format)); + formatBuilder.build()); trackBundles.put(0, bundle); extractorOutput.endTracks(); } @@ -642,6 +645,9 @@ private boolean readAtomHeader(ExtractorInput input) throws IOException { if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - Mp4Box.HEADER_SIZE; + if (atomSize != atomHeaderBytesRead && atomType == Mp4Box.TYPE_meta) { + maybeSkipRemainingMetaAtomHeaderBytes(input); + } containerAtoms.push(new ContainerBox(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); @@ -674,6 +680,14 @@ private boolean readAtomHeader(ExtractorInput input) throws IOException { return true; } + private void maybeSkipRemainingMetaAtomHeaderBytes(ExtractorInput input) throws IOException { + scratch.reset(Mp4Box.HEADER_SIZE); + input.peekFully(scratch.getData(), 0, Mp4Box.HEADER_SIZE); + BoxParser.maybeSkipRemainingMetaBoxHeaderBytes(scratch); + input.skipFully(scratch.getPosition()); + input.resetPeekPosition(); + } + private void readAtomPayload(ExtractorInput input) throws IOException { int atomPayloadSize = (int) (atomSize - atomHeaderBytesRead); @Nullable ParsableByteArray atomData = this.atomData; @@ -743,11 +757,27 @@ private void onMoovContainerAtomRead(ContainerBox moov) throws ParserException { } } + @Nullable Metadata mdtaMetadata = null; + @Nullable Mp4Box.ContainerBox meta = moov.getContainerBoxOfType(Mp4Box.TYPE_meta); + if (meta != null) { + mdtaMetadata = BoxParser.parseMdtaFromMeta(meta); + } + GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); + @Nullable Metadata udtaMetadata = null; + @Nullable Mp4Box.LeafBox udta = moov.getLeafBoxOfType(Mp4Box.TYPE_udta); + if (udta != null) { + udtaMetadata = BoxParser.parseUdta(udta); + gaplessInfoHolder.setFromMetadata(udtaMetadata); + } + Metadata mvhdMetadata = + new Metadata( + BoxParser.parseMvhd(checkNotNull(moov.getLeafBoxOfType(Mp4Box.TYPE_mvhd)).data)); + // Construction of tracks and sample tables. List sampleTables = parseTraks( moov, - new GaplessInfoHolder(), + gaplessInfoHolder, duration, drmInitData, /* ignoreEditLists= */ (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, @@ -763,12 +793,22 @@ private void onMoovContainerAtomRead(ContainerBox moov) throws ParserException { Track track = sampleTable.track; TrackOutput output = extractorOutput.track(i, track.type); output.durationUs(track.durationUs); + Format.Builder formatBuilder = track.format.buildUpon(); + formatBuilder.setContainerMimeType(containerMimeType); + MetadataUtil.setFormatGaplessInfo(track.type, gaplessInfoHolder, formatBuilder); + MetadataUtil.setFormatMetadata( + track.type, + mdtaMetadata, + formatBuilder, + track.format.metadata, + udtaMetadata, + mvhdMetadata); TrackBundle trackBundle = new TrackBundle( output, sampleTable, getDefaultSampleValues(defaultSampleValuesArray, track.id), - containerMimeType); + formatBuilder.build()); trackBundles.put(track.id, trackBundle); durationUs = max(durationUs, track.durationUs); } @@ -1900,7 +1940,10 @@ private static boolean shouldParseLeafAtom(int atom) { || atom == Mp4Box.TYPE_sgpd || atom == Mp4Box.TYPE_elst || atom == Mp4Box.TYPE_mehd - || atom == Mp4Box.TYPE_emsg; + || atom == Mp4Box.TYPE_emsg + || atom == Mp4Box.TYPE_udta + || atom == Mp4Box.TYPE_keys + || atom == Mp4Box.TYPE_ilst; } /** Returns whether the extractor should decode a container atom with type {@code atom}. */ @@ -1913,7 +1956,8 @@ private static boolean shouldParseContainerAtom(int atom) { || atom == Mp4Box.TYPE_moof || atom == Mp4Box.TYPE_traf || atom == Mp4Box.TYPE_mvex - || atom == Mp4Box.TYPE_edts; + || atom == Mp4Box.TYPE_edts + || atom == Mp4Box.TYPE_meta; } /** Holds data corresponding to a metadata sample. */ @@ -1946,7 +1990,7 @@ private static final class TrackBundle { public int currentTrackRunIndex; public int firstSampleToOutputIndex; - private final String containerMimeType; + private final Format baseFormat; private final ParsableByteArray encryptionSignalByte; private final ParsableByteArray defaultInitializationVector; @@ -1956,11 +2000,11 @@ public TrackBundle( TrackOutput output, TrackSampleTable moovSampleTable, DefaultSampleValues defaultSampleValues, - String containerMimeType) { + Format baseFormat) { this.output = output; this.moovSampleTable = moovSampleTable; this.defaultSampleValues = defaultSampleValues; - this.containerMimeType = containerMimeType; + this.baseFormat = baseFormat; fragment = new TrackFragment(); scratch = new ParsableByteArray(); encryptionSignalByte = new ParsableByteArray(1); @@ -1971,9 +2015,7 @@ public TrackBundle( public void reset(TrackSampleTable moovSampleTable, DefaultSampleValues defaultSampleValues) { this.moovSampleTable = moovSampleTable; this.defaultSampleValues = defaultSampleValues; - Format format = - moovSampleTable.track.format.buildUpon().setContainerMimeType(containerMimeType).build(); - output.format(format); + output.format(baseFormat); resetFragmentInfo(); } @@ -1984,14 +2026,7 @@ public void updateDrmInitData(DrmInitData drmInitData) { castNonNull(fragment.header).sampleDescriptionIndex); @Nullable String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; DrmInitData updatedDrmInitData = drmInitData.copyWithSchemeType(schemeType); - Format format = - moovSampleTable - .track - .format - .buildUpon() - .setContainerMimeType(containerMimeType) - .setDrmInitData(updatedDrmInitData) - .build(); + Format format = baseFormat.buildUpon().setDrmInitData(updatedDrmInitData).build(); output.format(format); } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java index 8d8719e411..b1e0488f3e 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java @@ -1085,8 +1085,8 @@ private void processEndOfStreamReadingAtomHeader() { } private void maybeSkipRemainingMetaAtomHeaderBytes(ExtractorInput input) throws IOException { - scratch.reset(8); - input.peekFully(scratch.getData(), 0, 8); + scratch.reset(Mp4Box.HEADER_SIZE); + input.peekFully(scratch.getData(), 0, Mp4Box.HEADER_SIZE); BoxParser.maybeSkipRemainingMetaBoxHeaderBytes(scratch); input.skipFully(scratch.getPosition()); input.resetPeekPosition(); diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripParser.java index d404de9156..72999b4f17 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripParser.java @@ -112,7 +112,7 @@ public void parse( : null; @Nullable String currentLine; while ((currentLine = parsableByteArray.readLine(charset)) != null) { - if (currentLine.length() == 0) { + if (currentLine.isEmpty()) { // Skip blank lines. continue; } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttCssParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttCssParser.java index 4ed6508d2d..741839a0fc 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttCssParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/webvtt/WebvttCssParser.java @@ -168,7 +168,7 @@ private static void parseStyleDeclaration( ParsableByteArray input, WebvttCssStyle style, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); String property = parseIdentifier(input, stringBuilder); - if ("".equals(property)) { + if (property.isEmpty()) { return; } if (!":".equals(parseNextToken(input, stringBuilder))) { @@ -176,7 +176,7 @@ private static void parseStyleDeclaration( } skipWhitespaceAndComments(input); String value = parsePropertyValue(input, stringBuilder); - if (value == null || "".equals(value)) { + if (value == null || value.isEmpty()) { return; } int position = input.getPosition(); @@ -240,7 +240,7 @@ private static void parseStyleDeclaration( return null; } String identifier = parseIdentifier(input, stringBuilder); - if (!"".equals(identifier)) { + if (!identifier.isEmpty()) { return identifier; } // We found a delimiter. @@ -373,7 +373,7 @@ private static void parseFontSize(String fontSize, WebvttCssStyle style) { * ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. */ private void applySelectorToStyle(WebvttCssStyle style, String selector) { - if ("".equals(selector)) { + if (selector.isEmpty()) { return; // Universal selector. } int voiceStartIndex = selector.indexOf('['); diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/jpeg/JpegExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/jpeg/JpegExtractorTest.java index e7e758910c..1377d57a7b 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/jpeg/JpegExtractorTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/jpeg/JpegExtractorTest.java @@ -85,6 +85,14 @@ public void samplePixelMotionPhotoVideoRemovedShortened_extractMotionPhoto() thr simulationConfig); } + @Test + public void samplePixelMotionPhotoWithoutExifShortened_extractMotionPhoto() throws Exception { + ExtractorAsserts.assertBehavior( + JpegMotionPhotoExtractor::new, + "media/jpeg/pixel-motion-photo-without-exif-shortened.jpg", + simulationConfig); + } + @Test public void sampleSsMotionPhotoShortened_extractMotionPhoto() throws Exception { ExtractorAsserts.assertBehavior( diff --git a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java index bbbd16389b..b0e32f5a5e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java +++ b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java @@ -50,6 +50,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.annotation.SuppressLint; +import android.content.ContentResolver; import android.content.Context; import android.graphics.Bitmap; import android.media.AudioManager; @@ -95,6 +96,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; @@ -1538,13 +1540,25 @@ public static ImmutableList convertToMediaButtonPreferences( MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, /* defaultValue= */ CommandButton.ICON_UNDEFINED) : CommandButton.ICON_UNDEFINED; - CommandButton button = + CommandButton.Builder button = new CommandButton.Builder(icon, customAction.getIcon()) .setSessionCommand(new SessionCommand(action, extras == null ? Bundle.EMPTY : extras)) .setDisplayName(customAction.getName()) - .setEnabled(true) - .build(); - customLayout.add(button); + .setEnabled(true); + @Nullable + String iconUriString = + extras != null + ? extras.getString(MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_URI_COMPAT) + : null; + if (iconUriString != null) { + Uri iconUri = Uri.parse(iconUriString); + @Nullable String scheme = iconUri.getScheme(); + if (Objects.equals(scheme, ContentResolver.SCHEME_CONTENT) + || Objects.equals(scheme, ContentResolver.SCHEME_ANDROID_RESOURCE)) { + button.setIconUri(iconUri); + } + } + customLayout.add(button.build()); } return CommandButton.getMediaButtonPreferencesFromCustomLayout( customLayout.build(), availablePlayerCommands, sessionExtras); 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 2b329c5777..2b7a8befa5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java @@ -43,7 +43,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Objects; import org.checkerframework.checker.initialization.qual.UnderInitialization; /** Implementation of MediaBrowser with the {@link MediaBrowserCompat} for legacy support. */ @@ -374,9 +373,7 @@ public void onError(String query, @Nullable Bundle extrasSent) { @Override public ListenableFuture sendCustomCommand(SessionCommand command, Bundle args) { MediaBrowserCompat browserCompat = getBrowserCompat(); - if (browserCompat != null - && (instance.isSessionCommandAvailable(command) - || isContainedInCommandButtonsForMediaItems(command))) { + if (browserCompat != null) { SettableFuture settable = SettableFuture.create(); browserCompat.sendCustomAction( command.customAction, @@ -402,19 +399,6 @@ public void onError(String action, @Nullable Bundle extras, @Nullable Bundle dat return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED)); } - // Using this method as a proxy whether an browser is allowed to send a custom action can be - // justified because a MediaBrowserCompat can declare the custom browse actions in onGetRoot() - // specifically for each browser that connects. This is different to Media3 where the command - // buttons for media items are declared on the session level, and are constraint by the available - // session commands granted individually to a controller/browser in onConnect. - private boolean isContainedInCommandButtonsForMediaItems(SessionCommand command) { - if (command.commandCode != SessionCommand.COMMAND_CODE_CUSTOM) { - return false; - } - CommandButton commandButton = commandButtonsForMediaItems.get(command.customAction); - return commandButton != null && Objects.equals(commandButton.sessionCommand, command); - } - private MediaBrowserCompat getBrowserCompat(LibraryParams extras) { return browserCompats.get(extras); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaButtonReceiver.java b/libraries/session/src/main/java/androidx/media3/session/MediaButtonReceiver.java index 766a6f326a..a98309b78c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaButtonReceiver.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaButtonReceiver.java @@ -174,8 +174,10 @@ protected final void handleIntentAndMaybeStartTheService( for (String action : ACTIONS) { ComponentName mediaButtonServiceComponentName = getServiceComponentByAction(context, action); if (mediaButtonServiceComponentName != null) { - intent.setComponent(mediaButtonServiceComponentName); - if (!shouldStartForegroundService(context, intent)) { + Intent serviceIntent = new Intent(); + serviceIntent.setComponent(mediaButtonServiceComponentName); + serviceIntent.fillIn(intent, 0); + if (!shouldStartForegroundService(context, serviceIntent)) { Log.i( TAG, "onReceive(Intent) does not start the media button event target service into the" @@ -184,11 +186,11 @@ protected final void handleIntentAndMaybeStartTheService( return; } try { - ContextCompat.startForegroundService(context, intent); + ContextCompat.startForegroundService(context, serviceIntent); } catch (/* ForegroundServiceStartNotAllowedException */ IllegalStateException e) { if (SDK_INT >= 31 && Api31.instanceOfForegroundServiceStartNotAllowedException(e)) { onForegroundServiceStartNotAllowedException( - intent, Api31.castToForegroundServiceStartNotAllowedException(e)); + serviceIntent, Api31.castToForegroundServiceStartNotAllowedException(e)); } else { throw e; } @@ -214,8 +216,8 @@ protected final void handleIntentAndMaybeStartTheService( * * @param context The {@link Context} that {@linkplain #onReceive(Context, Intent) was received by * the media button event receiver}. - * @param intent The intent that {@linkplain #onReceive(Context, Intent) was received by the media - * button event receiver}. + * @param intent The intent that will be used by {@linkplain + * Context#startForegroundService(Intent) for starting the foreground service}. * @return true if the service should be {@linkplain ContextCompat#startForegroundService(Context, * Intent) started as a foreground service}. If false is returned the service is not started * and the receiver call is a no-op. 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 090836e95e..12cc9a738c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java @@ -480,14 +480,22 @@ public final class MediaConstants { "androidx.media3.session.EXTRAS_KEY_MEDIA_TYPE_COMPAT"; /** - * {@link Bundle} key used to indicate the {@link CommandButton.Icon} in the extras of the legacy - * {@link PlaybackStateCompat.CustomAction}. The corresponding value should be one of the {@code - * CommandButton.ICON_} integer constants. + * {@link Bundle} key used to indicate the {@link CommandButton.Icon} in the extras of the + * platform {@link android.media.session.PlaybackState.CustomAction}. The corresponding value + * should be one of the {@code CommandButton.ICON_} integer constants. */ @UnstableApi public static final String EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT = "androidx.media3.session.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT"; + /** + * {@link Bundle} key used to indicate the custom icon Uri of a {@link CommandButton} in the + * extras of the platform {@link android.media.session.PlaybackState.CustomAction}. + */ + @UnstableApi + public static final String EXTRAS_KEY_COMMAND_BUTTON_ICON_URI_COMPAT = + "androidx.media3.session.EXTRAS_KEY_COMMAND_BUTTON_ICON_URI_COMPAT"; + /** * {@link Bundle} key used to store the title in case there was a display title that was given * precedence when converting to a {@code MediaDescriptionCompat}. This key is only used to be 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 aef35e5607..ac190ca3f9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -46,7 +46,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; -import android.util.Pair; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -120,6 +119,7 @@ private final ListenerSet listeners; private final FlushCommandQueueHandler flushCommandQueueHandler; private final ArraySet pendingMaskingSequencedFutureNumbers; + private final Handler fallbackPlaybackInfoUpdateHandler; @Nullable private SessionToken connectedToken; @Nullable private SessionServiceConnection serviceConnection; @@ -144,7 +144,6 @@ private long currentPositionMs; private long lastSetPlayWhenReadyCalledTimeMs; @Nullable private PlayerInfo pendingPlayerInfo; - @Nullable private BundlingExclusions pendingBundlingExclusions; private Bundle sessionExtras; public MediaControllerImplBase( @@ -172,6 +171,7 @@ public MediaControllerImplBase( applicationLooper, Clock.DEFAULT, (listener, flags) -> listener.onEvents(getInstance(), new Events(flags))); + fallbackPlaybackInfoUpdateHandler = new Handler(applicationLooper); // Initialize members this.instance = instance; @@ -283,6 +283,7 @@ public void release() { } released = true; connectedToken = null; + fallbackPlaybackInfoUpdateHandler.removeCallbacksAndMessages(null); flushCommandQueueHandler.release(); this.iSession = null; if (iSession != null) { @@ -386,6 +387,10 @@ private ListenableFuture dispatchRemoteSessionTask( new SessionResult(SessionResult.RESULT_INFO_SKIPPED)); int sequenceNumber = result.getSequenceNumber(); if (addToPendingMaskingOperations) { + if (pendingMaskingSequencedFutureNumbers.isEmpty()) { + // First pending operation, start masking PlayerInfo. + pendingPlayerInfo = playerInfo; + } pendingMaskingSequencedFutureNumbers.add(sequenceNumber); } try { @@ -2556,12 +2561,15 @@ private boolean requestConnectToService() { // If a service wants to keep running, it should be either foreground service or // bound service. But there had been request for the feature for system apps // and using bindService() will be better fit with it. - boolean result = context.bindService(intent, serviceConnection, flags); - if (!result) { + try { + if (context.bindService(intent, serviceConnection, flags)) { + return true; + } Log.w(TAG, "bind to " + token + " failed"); - return false; + } catch (SecurityException e) { + Log.w(TAG, "bind to " + token + " not allowed", e); } - return true; + return false; } private boolean requestConnectToSession(Bundle connectionHints) { @@ -2642,7 +2650,24 @@ void notifyPeriodicSessionPositionInfoChanged(SessionPositionInfo sessionPositio // masking operation on the application looper to ensure it's executed in order with other // updates sent to the application looper. sequencedFutureManager.setFutureResult(seq, futureResult); - getInstance().runOnApplicationLooper(() -> pendingMaskingSequencedFutureNumbers.remove(seq)); + getInstance() + .runOnApplicationLooper( + () -> { + pendingMaskingSequencedFutureNumbers.remove(seq); + if (connectedToken != null + && connectedToken.getInterfaceVersion() < 5 + && pendingMaskingSequencedFutureNumbers.isEmpty()) { + // Older session versions didn't reliably send a final PlayerInfo update. As a + // fallback, assume no actual final update is coming after 500ms. + fallbackPlaybackInfoUpdateHandler.postDelayed( + () -> { + if (pendingPlayerInfo != null) { + onPlayerInfoChanged(pendingPlayerInfo, BundlingExclusions.NONE); + } + }, + 500); + } + }); } void onConnected(ConnectionState result) { @@ -2765,36 +2790,29 @@ void onPlayerInfoChanged(PlayerInfo newPlayerInfo, BundlingExclusions bundlingEx if (!isConnected()) { return; } - if (pendingPlayerInfo != null && pendingBundlingExclusions != null) { - Pair mergedPlayerInfoUpdate = + if (pendingPlayerInfo != null) { + pendingPlayerInfo = 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. - pendingPlayerInfo = newPlayerInfo; - pendingBundlingExclusions = bundlingExclusions; - return; + pendingPlayerInfo, newPlayerInfo, bundlingExclusions, intersectedPlayerCommands); + if (pendingMaskingSequencedFutureNumbers.isEmpty()) { + // Finish masking. + newPlayerInfo = pendingPlayerInfo; + bundlingExclusions = BundlingExclusions.NONE; + pendingPlayerInfo = null; + } else { + // We are still waiting for all pending masking operations to be handled. + return; + } } PlayerInfo oldPlayerInfo = playerInfo; // 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 = mergePlayerInfo( - oldPlayerInfo, - /* oldBundlingExclusions= */ BundlingExclusions.NONE, - newPlayerInfo, - /* newBundlingExclusions= */ bundlingExclusions, - intersectedPlayerCommands) - .first; + oldPlayerInfo, + newPlayerInfo, + /* newBundlingExclusions= */ bundlingExclusions, + intersectedPlayerCommands); PlayerInfo finalPlayerInfo = playerInfo; @Nullable 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 28b6361ef8..09b7e730bb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java @@ -631,7 +631,7 @@ public void onSearchResultChanged( this.searchRequests.remove(i); } } - if (searchRequests.size() == 0) { + if (searchRequests.isEmpty()) { return; } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java index 17730bd72e..3e5c8889d3 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -108,10 +108,14 @@ public MediaLibrarySessionImpl( public List getConnectedControllers() { List list = super.getConnectedControllers(); @Nullable MediaLibraryServiceLegacyStub legacyStub = getLegacyBrowserService(); - if (legacyStub != null) { - list.addAll(legacyStub.getConnectedControllersManager().getConnectedControllers()); + if (legacyStub == null) { + return list; } - return list; + ImmutableList legacyControllers = + legacyStub.getConnectedControllersManager().getConnectedControllers(); + ImmutableList.Builder combinedList = + ImmutableList.builderWithExpectedSize(list.size() + legacyControllers.size()); + return combinedList.addAll(list).addAll(legacyControllers).build(); } @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 d8d4f5f40c..4ea96900c4 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -2147,7 +2147,7 @@ default void onPeriodicSessionPositionInfoChanged( // Mostly matched with MediaController.ControllerCallback - default void onDisconnected(int seq) throws RemoteException {} + default void onDisconnected(int seq) {} default void setCustomLayout(int seq, List layout) throws RemoteException {} 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 5bd6778b6e..f8bf732e81 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -90,7 +90,6 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import java.lang.ref.WeakReference; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutionException; @@ -363,22 +362,29 @@ public SessionToken getToken() { } public List getConnectedControllers() { - List controllers = new ArrayList<>(); - controllers.addAll(sessionStub.getConnectedControllersManager().getConnectedControllers()); - if (isMediaNotificationControllerConnected) { - ImmutableList legacyControllers = - sessionLegacyStub.getConnectedControllersManager().getConnectedControllers(); - for (int i = 0; i < legacyControllers.size(); i++) { - ControllerInfo legacyController = legacyControllers.get(i); - if (!isSystemUiController(legacyController)) { - controllers.add(legacyController); - } + ImmutableList media3Controllers = + sessionStub.getConnectedControllersManager().getConnectedControllers(); + ImmutableList platformControllers = + sessionLegacyStub.getConnectedControllersManager().getConnectedControllers(); + ImmutableList.Builder controllers = + ImmutableList.builderWithExpectedSize( + media3Controllers.size() + platformControllers.size()); + if (!isMediaNotificationControllerConnected) { + return controllers.addAll(media3Controllers).addAll(platformControllers).build(); + } + for (int i = 0; i < media3Controllers.size(); i++) { + ControllerInfo controllerInfo = media3Controllers.get(i); + if (!isSystemUiController(controllerInfo)) { + controllers.add(controllerInfo); + } + } + for (int i = 0; i < platformControllers.size(); i++) { + ControllerInfo controllerInfo = platformControllers.get(i); + if (!isSystemUiController(controllerInfo)) { + controllers.add(controllerInfo); } - } else { - controllers.addAll( - sessionLegacyStub.getConnectedControllersManager().getConnectedControllers()); } - return controllers; + return controllers.build(); } @Nullable @@ -401,7 +407,6 @@ public boolean isConnected(ControllerInfo controller) { */ protected boolean isSystemUiController(@Nullable MediaSession.ControllerInfo controllerInfo) { return controllerInfo != null - && controllerInfo.getControllerVersion() == ControllerInfo.LEGACY_CONTROLLER_VERSION && Objects.equals(controllerInfo.getPackageName(), SYSTEM_UI_PACKAGE_NAME); } @@ -427,9 +432,8 @@ public boolean isMediaNotificationController(MediaSession.ControllerInfo control * @return Whether the given controller info belongs to an Automotive OS controller. */ public boolean isAutomotiveController(ControllerInfo controllerInfo) { - return controllerInfo.getControllerVersion() == ControllerInfo.LEGACY_CONTROLLER_VERSION - && (controllerInfo.getPackageName().equals(ANDROID_AUTOMOTIVE_MEDIA_PACKAGE_NAME) - || controllerInfo.getPackageName().equals(ANDROID_AUTOMOTIVE_LAUNCHER_PACKAGE_NAME)); + return (controllerInfo.getPackageName().equals(ANDROID_AUTOMOTIVE_MEDIA_PACKAGE_NAME) + || controllerInfo.getPackageName().equals(ANDROID_AUTOMOTIVE_LAUNCHER_PACKAGE_NAME)); } /** @@ -440,8 +444,7 @@ public boolean isAutomotiveController(ControllerInfo controllerInfo) { * @return Whether the given controller info belongs to an Android Auto companion app controller. */ public boolean isAutoCompanionController(ControllerInfo controllerInfo) { - return controllerInfo.getControllerVersion() == ControllerInfo.LEGACY_CONTROLLER_VERSION - && controllerInfo.getPackageName().equals(ANDROID_AUTO_PACKAGE_NAME); + return controllerInfo.getPackageName().equals(ANDROID_AUTO_PACKAGE_NAME); } /** @@ -458,6 +461,13 @@ protected ControllerInfo getSystemUiControllerInfo() { return controllerInfo; } } + connectedControllers = sessionStub.getConnectedControllersManager().getConnectedControllers(); + for (int i = 0; i < connectedControllers.size(); i++) { + ControllerInfo controllerInfo = connectedControllers.get(i); + if (isSystemUiController(controllerInfo)) { + return controllerInfo; + } + } return null; } @@ -677,13 +687,16 @@ public void setAvailableCommands( if (sessionStub.getConnectedControllersManager().isConnected(controller)) { if (isMediaNotificationController(controller)) { sessionLegacyStub.setAvailableCommands(sessionCommands, playerCommands); - ControllerInfo systemUiControllerInfo = getSystemUiControllerInfo(); - if (systemUiControllerInfo != null) { + ControllerInfo systemUiInfo = getSystemUiControllerInfo(); + if (systemUiInfo != null) { // Set the available commands of the proxy controller to the ConnectedControllerRecord of // the hidden System UI controller. - sessionLegacyStub - .getConnectedControllersManager() - .updateCommandsFromSession(systemUiControllerInfo, sessionCommands, playerCommands); + ConnectedControllersManager controllersManager = + systemUiInfo.getControllerVersion() == ControllerInfo.LEGACY_CONTROLLER_VERSION + ? sessionLegacyStub.getConnectedControllersManager() + : sessionStub.getConnectedControllersManager(); + controllersManager.updateCommandsFromSession( + systemUiInfo, sessionCommands, playerCommands); } } sessionStub @@ -770,7 +783,7 @@ private void dispatchOnPlayerInfoChanged( // - TransactionTooLargeException means that we may need to fix our code. // (e.g. add pagination or special way to deliver Bitmap) // - DeadSystemException means that errors around it can be ignored. - Log.w(TAG, "Exception in " + controller.toString(), e); + Log.w(TAG, "Exception in " + controller, e); } } } @@ -836,7 +849,7 @@ public void sendError(SessionError sessionError) { public MediaSession.ConnectionResult onConnectOnHandler(ControllerInfo controller) { if (isMediaNotificationControllerConnected && isSystemUiController(controller)) { - // Hide System UI and provide the connection result from the `PlayerWrapper` state. + // Hide System UI and provide the connection result from the platform state. return sessionLegacyStub.getPlatformConnectionResult(instance); } MediaSession.ConnectionResult connectionResult = @@ -1170,6 +1183,11 @@ public void onFailure(Throwable t) { } } + /* package */ void triggerPlayerInfoUpdate() { + onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); + } + private void dispatchRemoteControllerTaskToLegacyStub(RemoteControllerTask task) { try { task.run(sessionLegacyStub.getControllerLegacyCbForBroadcast(), /* seq= */ 0); @@ -1259,7 +1277,7 @@ protected void dispatchRemoteControllerTaskWithoutReturn( // - TransactionTooLargeException means that we may need to fix our code. // (e.g. add pagination or special way to deliver Bitmap) // - DeadSystemException means that errors around it can be ignored. - Log.w(TAG, "Exception in " + controller.toString(), e); + Log.w(TAG, "Exception in " + controller, e); } } @@ -1297,7 +1315,7 @@ private ListenableFuture dispatchRemoteControllerTask( // - TransactionTooLargeException means that we may need to fix our code. // (e.g. add pagination or special way to deliver Bitmap) // - DeadSystemException means that errors around it can be ignored. - Log.w(TAG, "Exception in " + controller.toString(), e); + Log.w(TAG, "Exception in " + controller, e); } return Futures.immediateFuture(new SessionResult(ERROR_UNKNOWN)); } 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 0a1fe34483..37a9a6811e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -148,7 +148,7 @@ private ImmutableList mediaButtonPreferences; private SessionCommands availableSessionCommands; private Player.Commands availablePlayerCommands; - @Nullable private PlaybackException playbackException; + @Nullable private PlaybackException customPlaybackException; @Nullable private Player.Commands playerCommandsForErrorState; @SuppressWarnings({ @@ -269,7 +269,7 @@ public MediaSessionLegacyStub( */ public void setAvailableCommands( SessionCommands sessionCommands, Player.Commands playerCommands) { - if (playbackException != null) { + if (customPlaybackException != null) { return; } boolean commandGetTimelineChanged = @@ -361,7 +361,7 @@ public void setPlatformMediaButtonPreferences( } /** - * Sets or clears an playback exception override for the platform session. + * Sets or clears a playback exception override for the platform session. * * @param playbackException The {@link PlaybackException} or null. * @param playerCommandsForErrorState The available {@link Player.Commands} while the exception @@ -373,7 +373,7 @@ public void setPlaybackException( checkArgument( (playbackException == null && playerCommandsForErrorState == null) || (playbackException != null && playerCommandsForErrorState != null)); - this.playbackException = playbackException; + customPlaybackException = playbackException; this.playerCommandsForErrorState = playerCommandsForErrorState; if (playbackException != null) { updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); @@ -1006,11 +1006,7 @@ private ControllerInfo tryGetController(RemoteUserInfo remoteUserInfo) { /* maxCommandsForMediaItems= */ 0); MediaSession.ConnectionResult connectionResult = sessionImpl.onConnectOnHandler(controller); if (!connectionResult.isAccepted) { - try { - controllerCb.onDisconnected(/* seq= */ 0); - } catch (RemoteException e) { - // Controller may have died prematurely. - } + controllerCb.onDisconnected(/* seq= */ 0); return null; } connectedControllersManager.addController( @@ -1286,7 +1282,7 @@ public ControllerLegacyCbForBroadcast() { * @return True if updates should be skipped. */ public boolean skipLegacySessionPlaybackStateUpdates() { - return playbackException != null; + return customPlaybackException != null; } @Override @@ -1300,7 +1296,7 @@ public void onAvailableCommandsChangedFromPlayer(int seq, Player.Commands availa } @Override - public void onDisconnected(int seq) throws RemoteException { + public void onDisconnected(int seq) { // Calling MediaSessionCompat#release() is already done in release(). } @@ -1724,11 +1720,7 @@ public ConnectionTimeoutHandler( public void handleMessage(Message msg) { ControllerInfo controller = (ControllerInfo) msg.obj; if (connectedControllersManager.isConnected(controller)) { - try { - checkStateNotNull(controller.getControllerCb()).onDisconnected(/* seq= */ 0); - } catch (RemoteException e) { - // Controller may have died prematurely. - } + checkStateNotNull(controller.getControllerCb()).onDisconnected(/* seq= */ 0); connectedControllersManager.removeController(controller); } } @@ -1761,7 +1753,7 @@ private static ComponentName getServiceComponentByAction(Context context, String private PlaybackStateCompat createPlaybackStateCompat(PlayerWrapper player) { LegacyError legacyError = this.legacyError; - if (playbackException == null && legacyError != null && legacyError.isFatal) { + if (customPlaybackException == null && legacyError != null && legacyError.isFatal) { // A fatal legacy error automatically set by Media3 upon a calling // MediaLibrarySession.Callback according to the configured LibraryErrorReplicationMode. Bundle extras = new Bundle(legacyError.extras); @@ -1779,17 +1771,17 @@ private PlaybackStateCompat createPlaybackStateCompat(PlayerWrapper player) { .setExtras(legacyError.extras) .build(); } - if (playbackException == null) { - // The actual error from the player, if any. - playbackException = player.getPlayerError(); - } + // The custom error from the session if present, or the actual error from the player, if any. + @Nullable + PlaybackException publicPlaybackException = + customPlaybackException != null ? customPlaybackException : player.getPlayerError(); boolean canReadPositions = player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) && !player.isCurrentMediaItemLive(); boolean shouldShowPlayButton = - playbackException != null || Util.shouldShowPlayButton(player, playIfSuppressed); + publicPlaybackException != null || Util.shouldShowPlayButton(player, playIfSuppressed); int state = - playbackException != null + publicPlaybackException != null ? PlaybackStateCompat.STATE_ERROR : LegacyConversions.convertToPlaybackStateCompatState(player, shouldShowPlayButton); // Always advertise ACTION_SET_RATING. @@ -1820,8 +1812,9 @@ private PlaybackStateCompat createPlaybackStateCompat(PlayerWrapper player) { : MediaSessionCompat.QueueItem.UNKNOWN_ID; float playbackSpeed = player.getPlaybackParameters().speed; float sessionPlaybackSpeed = player.isPlaying() && canReadPositions ? playbackSpeed : 0f; - Bundle extras = playbackException != null ? new Bundle(playbackException.extras) : new Bundle(); - if (playbackException == null && legacyError != null) { + Bundle extras = + publicPlaybackException != null ? new Bundle(publicPlaybackException.extras) : new Bundle(); + if (publicPlaybackException == null && legacyError != null) { extras.putAll(legacyError.extras); } extras.putAll(legacyExtras); @@ -1853,12 +1846,21 @@ private PlaybackStateCompat createPlaybackStateCompat(PlayerWrapper player) { && sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM && CommandButton.isButtonCommandAvailable( commandButton, availableSessionCommands, availableCommands)) { - Bundle actionExtras = sessionCommand.customExtras; - if (commandButton.icon != CommandButton.ICON_UNDEFINED) { - actionExtras = new Bundle(sessionCommand.customExtras); + boolean hasIcon = commandButton.icon != CommandButton.ICON_UNDEFINED; + boolean hasIconUri = commandButton.iconUri != null; + Bundle actionExtras = + hasIcon || hasIconUri + ? new Bundle(sessionCommand.customExtras) + : sessionCommand.customExtras; + if (hasIcon) { actionExtras.putInt( MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, commandButton.icon); } + if (hasIconUri) { + actionExtras.putString( + MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_URI_COMPAT, + checkNotNull(commandButton.iconUri).toString()); + } builder.addCustomAction( new PlaybackStateCompat.CustomAction.Builder( sessionCommand.customAction, commandButton.displayName, commandButton.iconResId) @@ -1866,10 +1868,10 @@ private PlaybackStateCompat createPlaybackStateCompat(PlayerWrapper player) { .build()); } } - if (playbackException != null) { + if (publicPlaybackException != null) { builder.setErrorMessage( - LegacyConversions.convertToLegacyErrorCode(playbackException), - playbackException.getMessage()); + LegacyConversions.convertToLegacyErrorCode(publicPlaybackException), + publicPlaybackException.getMessage()); } else if (legacyError != null) { builder.setErrorMessage(legacyError.code, legacyError.message); } 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 a7527db07e..803bb7f1e5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -34,7 +34,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.os.RemoteException; import androidx.annotation.CallSuper; import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; @@ -818,7 +817,7 @@ public void connect( @Nullable IMediaController caller, @Nullable Bundle connectionRequestBundle) { if (caller == null || connectionRequestBundle == null) { // Malformed call from potentially malicious controller. - // No need to notify that we're ignoring call. + SessionUtil.disconnectIMediaController(caller); return; } ConnectionRequest request; @@ -826,18 +825,13 @@ public void connect( request = ConnectionRequest.fromBundle(connectionRequestBundle); } catch (RuntimeException e) { // Malformed call from potentially malicious controller. - // No need to notify that we're ignoring call. Log.w(TAG, "Ignoring malformed Bundle for ConnectionRequest", e); + SessionUtil.disconnectIMediaController(caller); return; } @Nullable MediaSessionService mediaSessionService = serviceReference.get(); if (mediaSessionService == null) { - try { - caller.onDisconnected(/* seq= */ 0); - } catch (RemoteException e) { - // Controller may be died prematurely. - // Not an issue because we'll ignore it anyway. - } + SessionUtil.disconnectIMediaController(caller); return; } int callingPid = Binder.getCallingPid(); @@ -854,7 +848,7 @@ public void connect( handler.post( () -> { pendingControllers.remove(caller); - boolean shouldNotifyDisconnected = true; + boolean connected = false; try { @Nullable MediaSessionService service = serviceReference.get(); if (service == null) { @@ -871,30 +865,19 @@ public void connect( request.connectionHints, request.maxCommandsForMediaItems); - @Nullable MediaSession session; - try { - session = service.onGetSession(controllerInfo); - if (session == null) { - return; - } - - service.addSession(session); - shouldNotifyDisconnected = false; - - session.handleControllerConnectionFromService(caller, controllerInfo); - } catch (Exception e) { - // Don't propagate exception in service to the controller. - Log.w(TAG, "Failed to add a session to session service", e); + @Nullable MediaSession session = service.onGetSession(controllerInfo); + if (session == null) { + return; } + service.addSession(session); + session.handleControllerConnectionFromService(caller, controllerInfo); + connected = true; + } catch (Exception e) { + // Don't propagate exception in service to the controller. + Log.w(TAG, "Failed to add a session to session service", e); } finally { - // Trick to call onDisconnected() in one place. - if (shouldNotifyDisconnected) { - try { - caller.onDisconnected(/* seq= */ 0); - } catch (RemoteException e) { - // Controller may be died prematurely. - // Not an issue because we'll ignore it anyway. - } + if (!connected) { + SessionUtil.disconnectIMediaController(caller); } } }); @@ -907,11 +890,7 @@ public void release() { serviceReference.clear(); handler.removeCallbacksAndMessages(null); for (IMediaController controller : pendingControllers) { - try { - controller.onDisconnected(/* seq= */ 0); - } catch (RemoteException e) { - // Ignore. We're releasing. - } + SessionUtil.disconnectIMediaController(controller); } pendingControllers.clear(); } 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 d92642d478..9617ce5fe7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -123,7 +123,7 @@ private static final String TAG = "MediaSessionStub"; /** The version of the IMediaSession interface. */ - public static final int VERSION_INT = 4; + public static final int VERSION_INT = 5; /** * Sequence number used when a controller method is triggered on the sesison side that wasn't @@ -152,9 +152,16 @@ public ConnectedControllersManager getConnectedControllersManager() { } private static void sendSessionResult( - ControllerInfo controller, int sequenceNumber, SessionResult result) { + MediaSessionImpl sessionImpl, + ControllerInfo controller, + int sequenceNumber, + SessionResult result) { try { checkStateNotNull(controller.getControllerCb()).onSessionResult(sequenceNumber, result); + // Make sure the session sends out a new PlayerInfo update in any case, even if the controller + // command we just handled didn't change anything. This is needed to end any masking states + // in the controllers waiting to acknowledge this command. + sessionImpl.triggerPlayerInfoUpdate(); } catch (RemoteException e) { Log.w(TAG, "Failed to send result to controller " + controller, e); } @@ -174,7 +181,7 @@ SessionTask, K> sendSessionResultSuccess(ControllerPlayer } task.run(sessionImpl.getPlayerWrapper(), controller); sendSessionResult( - controller, sequenceNumber, new SessionResult(SessionResult.RESULT_SUCCESS)); + sessionImpl, controller, sequenceNumber, new SessionResult(SessionResult.RESULT_SUCCESS)); return Futures.immediateVoidFuture(); }; } @@ -203,7 +210,7 @@ SessionTask, K> sendSessionResultWhenReady( ? ERROR_NOT_SUPPORTED : ERROR_UNKNOWN); } - sendSessionResult(controller, sequenceNumber, result); + sendSessionResult(sessionImpl, controller, sequenceNumber, result); }); } @@ -319,15 +326,16 @@ private void queueSessionTaskWithPlayerCommandForCo sessionImpl.getApplicationHandler(), () -> { if (!connectedControllersManager.isPlayerCommandAvailable(controller, command)) { - sendSessionResult( - controller, sequenceNumber, new SessionResult(ERROR_PERMISSION_DENIED)); + SessionResult deniedResult = new SessionResult(ERROR_PERMISSION_DENIED); + sendSessionResult(sessionImpl, controller, sequenceNumber, deniedResult); return; } @SessionResult.Code int resultCode = sessionImpl.onPlayerCommandRequestOnHandler(controller, command); if (resultCode != SessionResult.RESULT_SUCCESS) { // Don't run rejected command. - sendSessionResult(controller, sequenceNumber, new SessionResult(resultCode)); + sendSessionResult( + sessionImpl, controller, sequenceNumber, new SessionResult(resultCode)); return; } if (command == COMMAND_SET_VIDEO_SURFACE) { @@ -396,14 +404,14 @@ private void dispatchSessionTaskWithSessionCommand( if (sessionCommand != null) { if (!connectedControllersManager.isSessionCommandAvailable( controller, sessionCommand)) { - sendSessionResult( - controller, sequenceNumber, new SessionResult(ERROR_PERMISSION_DENIED)); + SessionResult deniedResult = new SessionResult(ERROR_PERMISSION_DENIED); + sendSessionResult(sessionImpl, controller, sequenceNumber, deniedResult); return; } } else { if (!connectedControllersManager.isSessionCommandAvailable(controller, commandCode)) { - sendSessionResult( - controller, sequenceNumber, new SessionResult(ERROR_PERMISSION_DENIED)); + SessionResult deniedResult = new SessionResult(ERROR_PERMISSION_DENIED); + sendSessionResult(sessionImpl, controller, sequenceNumber, deniedResult); return; } } @@ -459,16 +467,12 @@ private int maybeCorrectMediaItemIndex( @SuppressWarnings("UngroupedOverloads") // Overload belongs to AIDL interface that is separated. public void connect(@Nullable IMediaController caller, @Nullable ControllerInfo controllerInfo) { if (caller == null || controllerInfo == null) { + SessionUtil.disconnectIMediaController(caller); return; } @Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get(); if (sessionImpl == null || sessionImpl.isReleased()) { - try { - caller.onDisconnected(/* seq= */ 0); - } catch (RemoteException e) { - // Controller may be died prematurely. - // Not an issue because we'll ignore it anyway. - } + SessionUtil.disconnectIMediaController(caller); return; } pendingControllers.add(controllerInfo); @@ -588,12 +592,7 @@ public void connect(@Nullable IMediaController caller, @Nullable ControllerInfo } } finally { if (!connected) { - try { - caller.onDisconnected(/* seq= */ 0); - } catch (RemoteException e) { - // Controller may be died prematurely. - // Not an issue because we'll ignore it anyway. - } + SessionUtil.disconnectIMediaController(caller); } } }); @@ -605,21 +604,13 @@ public void release() { connectedControllersManager.removeController(controller); ControllerCb cb = controller.getControllerCb(); if (cb != null) { - try { - cb.onDisconnected(/* seq= */ 0); - } catch (RemoteException e) { - // Ignore. We're releasing. - } + cb.onDisconnected(/* seq= */ 0); } } for (ControllerInfo controller : pendingControllers) { ControllerCb cb = controller.getControllerCb(); if (cb != null) { - try { - cb.onDisconnected(/* seq= */ 0); - } catch (RemoteException e) { - // Ignore. We're releasing. - } + cb.onDisconnected(/* seq= */ 0); } } pendingControllers.clear(); @@ -2172,8 +2163,8 @@ public void onSearchResultChanged( } @Override - public void onDisconnected(int sequenceNumber) throws RemoteException { - iController.onDisconnected(sequenceNumber); + public void onDisconnected(int sequenceNumber) { + SessionUtil.disconnectIMediaController(iController); } @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 1a5a0f2d61..18589ea920 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -23,7 +23,6 @@ import android.os.Parcelable; import android.os.SystemClock; import android.text.TextUtils; -import android.util.Pair; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Player; @@ -128,39 +127,28 @@ public static Commands intersect(@Nullable Commands commands1, @Nullable Command * previousPlayerInfo} and taking into account the passed available commands. * * @param oldPlayerInfo The old {@link PlayerInfo}. - * @param oldBundlingExclusions The bundling exclusions in the old {@link PlayerInfo}. * @param newPlayerInfo The new {@link PlayerInfo}. * @param newBundlingExclusions The bundling exclusions 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}. + * @return The resulting merged {@link PlayerInfo}. */ - public static Pair mergePlayerInfo( + public static PlayerInfo 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) { + && availablePlayerCommands.contains(Player.COMMAND_GET_TIMELINE)) { // 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) { + && availablePlayerCommands.contains(Player.COMMAND_GET_TRACKS)) { // 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); + return mergedPlayerInfo; } /** Generates an array of {@code n} indices. */ 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 2996afb600..846022ffa4 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionCommands.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionCommands.java @@ -269,5 +269,4 @@ public static SessionCommands fromBundle(Bundle bundle) { } return builder.build(); } - ; } 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 396e3bf719..1fd41323cb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java @@ -411,7 +411,7 @@ protected void onReceiveResult(int resultCode, Bundle resultData) { // Remove timeout callback. handler.removeCallbacksAndMessages(null); try { - future.set(SessionToken.fromBundle(resultData, (Token) compatToken.getToken())); + future.set(SessionToken.fromBundle(resultData, compatToken.getToken())); } 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. 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 e452d3ba81..86baa24819 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java @@ -172,7 +172,7 @@ public Object getBinder() { @Nullable @Override public MediaSession.Token getPlatformToken() { - return legacyToken == null ? null : (MediaSession.Token) legacyToken.getToken(); + return legacyToken == null ? null : legacyToken.getToken(); } private static final String FIELD_LEGACY_TOKEN = Util.intToStringMaxRadix(0); diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionUtil.java b/libraries/session/src/main/java/androidx/media3/session/SessionUtil.java new file mode 100644 index 0000000000..ecbf8de953 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/SessionUtil.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 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 android.os.RemoteException; +import androidx.annotation.Nullable; + +/** Static utilities for the session module. */ +/* package */ class SessionUtil { + + private SessionUtil() {} + + /** + * Disconnect a {@link IMediaController}, ignoring remote exceptions in case the controller is + * broken or already died. + */ + public static void disconnectIMediaController(@Nullable IMediaController controller) { + try { + if (controller != null) { + controller.onDisconnected(/* seq= */ 0); + } + } catch (RemoteException e) { + // Intentionally ignored. Controller may have died already or is malformed. + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompat.java index a1feaf0682..99bc6fc765 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompat.java @@ -31,7 +31,6 @@ import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS; import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID; import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST; -import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN; import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS; import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_OPTIONS; import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_PACKAGE_NAME; @@ -44,8 +43,6 @@ import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER; import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_SERVICE_VERSION; import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_SESSION_BINDER; -import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT; -import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED; import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN; import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_VERSION_2; @@ -844,14 +841,6 @@ void sendCustomAction( } interface MediaBrowserServiceCallbackImpl { - void onServiceConnected( - Messenger callback, - @Nullable String root, - @Nullable MediaSessionCompat.Token session, - @Nullable Bundle extra); - - void onConnectionFailed(Messenger callback); - void onLoadChildren( Messenger callback, @Nullable String parentId, @@ -991,7 +980,7 @@ public void unsubscribe(String parentId, @Nullable SubscriptionCallback callback optionsList.remove(i); } } - if (callbacks.size() == 0) { + if (callbacks.isEmpty()) { browserFwk.unsubscribe(parentId); } } @@ -1200,20 +1189,6 @@ public void onConnectionFailed() { // Do noting } - @Override - public void onServiceConnected( - final Messenger callback, - @Nullable final String root, - @Nullable final MediaSessionCompat.Token session, - @Nullable Bundle extra) { - // This method will not be called. - } - - @Override - public void onConnectionFailed(Messenger callback) { - // This method will not be called. - } - @Override @SuppressWarnings({"ReferenceEquality"}) public void onLoadChildren( @@ -1394,23 +1369,6 @@ public void handleMessage(Message msg) { try { switch (msg.what) { - case SERVICE_MSG_ON_CONNECT: - { - Bundle rootHints = data.getBundle(DATA_ROOT_HINTS); - MediaSessionCompat.ensureClassLoader(rootHints); - - serviceCallback.onServiceConnected( - callbacksMessenger, - data.getString(DATA_MEDIA_ITEM_ID), - LegacyParcelableUtil.convert( - data.getParcelable(DATA_MEDIA_SESSION_TOKEN), - MediaSessionCompat.Token.CREATOR), - rootHints); - break; - } - case SERVICE_MSG_ON_CONNECT_FAILED: - serviceCallback.onConnectionFailed(callbacksMessenger); - break; case SERVICE_MSG_ON_LOAD_CHILDREN: { Bundle options = data.getBundle(DATA_OPTIONS); @@ -1442,10 +1400,6 @@ public void handleMessage(Message msg) { } catch (BadParcelableException e) { // Do not print the exception here, since it is already done by the Parcel class. Log.e(TAG, "Could not unparcel the data."); - // If an error happened while connecting, disconnect from the service. - if (msg.what == SERVICE_MSG_ON_CONNECT) { - serviceCallback.onConnectionFailed(callbacksMessenger); - } } } diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserProtocol.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserProtocol.java index 0c4bfb225a..d7bace7194 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserProtocol.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserProtocol.java @@ -31,7 +31,6 @@ public class MediaBrowserProtocol { public static final String DATA_CALLING_PID = "data_calling_pid"; public static final String DATA_MEDIA_ITEM_ID = "data_media_item_id"; public static final String DATA_MEDIA_ITEM_LIST = "data_media_item_list"; - public static final String DATA_MEDIA_SESSION_TOKEN = "data_media_session_token"; public static final String DATA_OPTIONS = "data_options"; public static final String DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS = "data_notify_children_changed_options"; @@ -65,20 +64,6 @@ public class MediaBrowserProtocol { * DO NOT RENUMBER THESE! */ - /** - * (service v1) Sent after {@link MediaBrowserCompat#connect()} when the request has successfully - * completed. - arg1 : The service version - data DATA_MEDIA_ITEM_ID : A string for the root media - * item id DATA_MEDIA_SESSION_TOKEN : Media session token DATA_ROOT_HINTS : An optional root hints - * bundle of service-specific arguments - */ - public static final int SERVICE_MSG_ON_CONNECT = 1; - - /** - * (service v1) Sent after {@link MediaBrowserCompat#connect()} when the connection to the media - * browser failed. - arg1 : service version - */ - public static final int SERVICE_MSG_ON_CONNECT_FAILED = 2; - /** * (service v1) Sent when the list of children is loaded or updated. - arg1 : The service version * - data DATA_MEDIA_ITEM_ID : A string for the parent media item id DATA_MEDIA_ITEM_LIST : An @@ -103,19 +88,6 @@ public class MediaBrowserProtocol { * DO NOT RENUMBER THESE! */ - /** - * (client v1) Sent to connect to the media browse service compat. - arg1 : The client version - - * data DATA_PACKAGE_NAME : A string for the package name of MediaBrowserCompat DATA_ROOT_HINTS : - * An optional root hints bundle of service-specific arguments - replyTo : Callback messenger - */ - public static final int CLIENT_MSG_CONNECT = 1; - - /** - * (client v1) Sent to disconnect from the media browse service compat. - arg1 : The client - * version - replyTo : Callback messenger - */ - public static final int CLIENT_MSG_DISCONNECT = 2; - /** * (client v1) Sent to subscribe for changes to the children of the specified media id. - arg1 : * The client version - data DATA_MEDIA_ITEM_ID : A string for a media item id DATA_OPTIONS : A diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserServiceCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserServiceCompat.java index bb9d805ada..ab44305a31 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserServiceCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserServiceCompat.java @@ -18,8 +18,6 @@ import static androidx.annotation.RestrictTo.Scope.LIBRARY; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION; -import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_CONNECT; -import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT; import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM; import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER; import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION; @@ -33,7 +31,6 @@ import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS; import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID; import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST; -import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN; import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS; import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_OPTIONS; import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_PACKAGE_NAME; @@ -46,8 +43,6 @@ import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER; import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_SERVICE_VERSION; import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_SESSION_BINDER; -import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT; -import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED; import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN; import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_VERSION_CURRENT; import static androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; @@ -58,7 +53,6 @@ import android.app.Service; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.media.browse.MediaBrowser; import android.os.Binder; import android.os.Build; @@ -93,7 +87,6 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; -import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -641,7 +634,6 @@ private class ConnectionRecord implements IBinder.DeathRecipient { public final HashMap< @NullableType String, List>> subscriptions = new HashMap<>(); - @Nullable public BrowserRoot root; ConnectionRecord( @Nullable String pkg, @@ -772,74 +764,6 @@ void onErrorSent(@Nullable Bundle extras) { private class ServiceBinderImpl { ServiceBinderImpl() {} - public void connect( - @Nullable String pkg, - int pid, - int uid, - @Nullable Bundle rootHints, - ServiceCallbacks callbacks) { - - if (!isValidPackage(pkg, uid)) { - throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid + " package=" + pkg); - } - - handler.postOrRun( - new Runnable() { - @Override - public void run() { - final IBinder b = callbacks.asBinder(); - - // Clear out the old subscriptions. We are getting new ones. - connections.remove(b); - - ConnectionRecord connection = - new ConnectionRecord(pkg, pid, uid, rootHints, callbacks); - curConnection = connection; - BrowserRoot root = MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints); - connection.root = root; - curConnection = null; - - // If they didn't return something, don't allow this client. - if (root == null) { - Log.i(TAG, "No root for client " + pkg + " from service " + getClass().getName()); - try { - callbacks.onConnectFailed(); - } catch (RemoteException ex) { - Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. " + "pkg=" + pkg); - } - } else { - try { - connections.put(b, connection); - b.linkToDeath(connection, 0); - if (session != null) { - callbacks.onConnect(root.getRootId(), checkNotNull(session), root.getExtras()); - } - } catch (RemoteException ex) { - Log.w(TAG, "Calling onConnect() failed. Dropping client. " + "pkg=" + pkg); - connections.remove(b); - } - } - } - }); - } - - public void disconnect(ServiceCallbacks callbacks) { - handler.postOrRun( - new Runnable() { - @Override - public void run() { - final IBinder b = callbacks.asBinder(); - - // Clear out the old subscriptions. We are getting new ones. - final ConnectionRecord old = connections.remove(b); - if (old != null) { - // TODO - checkNotNull(old.callbacks).asBinder().unlinkToDeath(old, 0); - } - } - }); - } - public void addSubscription( @Nullable String id, @Nullable IBinder token, @@ -1032,11 +956,6 @@ public void run() { private interface ServiceCallbacks { IBinder asBinder(); - void onConnect(String root, MediaSessionCompat.Token session, @Nullable Bundle extras) - throws RemoteException; - - void onConnectFailed() throws RemoteException; - void onLoadChildren( @Nullable String mediaId, @Nullable List list, @@ -1057,28 +976,6 @@ public IBinder asBinder() { return callbacks.getBinder(); } - @Override - public void onConnect(String root, MediaSessionCompat.Token session, @Nullable Bundle extras) - throws RemoteException { - if (extras == null) { - extras = new Bundle(); - } - extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); - Bundle data = new Bundle(); - data.putString(DATA_MEDIA_ITEM_ID, root); - data.putParcelable( - DATA_MEDIA_SESSION_TOKEN, - LegacyParcelableUtil.convert( - session, android.support.v4.media.session.MediaSessionCompat.Token.CREATOR)); - data.putBundle(DATA_ROOT_HINTS, extras); - sendRequest(SERVICE_MSG_ON_CONNECT, data); - } - - @Override - public void onConnectFailed() throws RemoteException { - sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null); - } - @Override public void onLoadChildren( @Nullable String mediaId, @@ -1465,22 +1362,6 @@ public void notifyChildrenChanged( void handleMessageInternal(Message msg) { Bundle data = msg.getData(); switch (msg.what) { - case CLIENT_MSG_CONNECT: - { - Bundle rootHints = data.getBundle(DATA_ROOT_HINTS); - MediaSessionCompat.ensureClassLoader(rootHints); - - serviceBinderImpl.connect( - data.getString(DATA_PACKAGE_NAME), - data.getInt(DATA_CALLING_PID), - data.getInt(DATA_CALLING_UID), - rootHints, - new ServiceCallbacksCompat(msg.replyTo)); - break; - } - case CLIENT_MSG_DISCONNECT: - serviceBinderImpl.disconnect(new ServiceCallbacksCompat(msg.replyTo)); - break; case CLIENT_MSG_ADD_SUBSCRIPTION: { Bundle options = data.getBundle(DATA_OPTIONS); @@ -1557,26 +1438,6 @@ void handleMessageInternal(Message msg) { } } - /** Return whether the given package is one of the ones that is owned by the uid. */ - @EnsuresNonNullIf(result = true, expression = "#1") - boolean isValidPackage(@Nullable String pkg, int uid) { - if (pkg == null) { - return false; - } - final PackageManager pm = getPackageManager(); - final String[] packages = pm.getPackagesForUid(uid); - if (packages == null) { - return false; - } - final int N = packages.length; - for (int i = 0; i < N; i++) { - if (packages[i].equals(pkg)) { - return true; - } - } - return false; - } - /** Save the subscription and if it is a new subscription send the results. */ void addSubscription( @Nullable String id, @@ -1623,7 +1484,7 @@ boolean removeSubscription( iter.remove(); } } - if (callbackList.size() == 0) { + if (callbackList.isEmpty()) { connection.subscriptions.remove(id); } } diff --git a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java index 3b36271b9b..966b9d500e 100644 --- a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java @@ -16,6 +16,7 @@ package androidx.media3.session; import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT; +import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_URI_COMPAT; import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMPLETION_STATUS; import static androidx.media3.session.MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT; import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED; @@ -307,8 +308,7 @@ public void convertToQueueItemId() { @Test public void convertToMediaMetadata_withoutTitle() { - assertThat(LegacyConversions.convertToMediaMetadata((CharSequence) null)) - .isEqualTo(MediaMetadata.EMPTY); + assertThat(LegacyConversions.convertToMediaMetadata(null)).isEqualTo(MediaMetadata.EMPTY); } @Test @@ -321,7 +321,7 @@ public void convertToMediaMetadata_withTitle() { public void convertToMediaMetadata_withCustomKey() { MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, "title"); - builder.putLong(EXTRAS_KEY_MEDIA_TYPE_COMPAT, (long) MediaMetadata.MEDIA_TYPE_MUSIC); + builder.putLong(EXTRAS_KEY_MEDIA_TYPE_COMPAT, MediaMetadata.MEDIA_TYPE_MUSIC); builder.putString("custom_key", "value"); MediaMetadataCompat testMediaMetadataCompat = builder.build(); @@ -680,7 +680,7 @@ public void convertToSessionCommands_withCustomAction_containsCustomAction() { .isTrue(); } - @Config(minSdk = Config.OLDEST_SDK) + @Config(minSdk = 21) @Test public void convertToSessionCommands_whenSessionIsNotReadyOnSdk21_disallowsRating() { SessionCommands sessionCommands = @@ -1176,12 +1176,13 @@ public void convertToMediaButtonPreferences_withoutIconConstantInExtras() { } @Test - public void convertToMediaButtonPreferences_withIconConstantInExtras() { + public void convertToMediaButtonPreferences_withIconConstantAndUriInExtras() { String actionStr = "action"; String displayName = "display_name"; int iconRes = 21; Bundle extras = new Bundle(); extras.putInt(EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, CommandButton.ICON_FAST_FORWARD); + extras.putString(EXTRAS_KEY_COMMAND_BUTTON_ICON_URI_COMPAT, "content://my_icon"); PlaybackStateCompat.CustomAction action = new PlaybackStateCompat.CustomAction.Builder(actionStr, displayName, iconRes) .setExtras(extras) @@ -1207,6 +1208,7 @@ public void convertToMediaButtonPreferences_withIconConstantInExtras() { assertThat(button.iconResId).isEqualTo(iconRes); assertThat(button.sessionCommand.customAction).isEqualTo(actionStr); assertThat(button.icon).isEqualTo(CommandButton.ICON_FAST_FORWARD); + assertThat(button.iconUri).isEqualTo(Uri.parse("content://my_icon")); } @Test diff --git a/libraries/session/src/test/java/androidx/media3/session/MediaMetadataCompatTest.java b/libraries/session/src/test/java/androidx/media3/session/MediaMetadataCompatTest.java index 449bb17df5..a5869c365e 100644 --- a/libraries/session/src/test/java/androidx/media3/session/MediaMetadataCompatTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/MediaMetadataCompatTest.java @@ -61,12 +61,11 @@ public void convertToMediaMetadata_withBitmap_keepSameBitmapInstance() { MediaMetadata mediaMetadata = testMediaMetadataCompat.getMediaMetadata(); // Verify that the long/text/string/rating values are set correctly. - assertThat(mediaMetadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)).isEqualTo(1000); - assertThat(mediaMetadata.getText(MediaMetadataCompat.METADATA_KEY_ARTIST).toString()) + assertThat(mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION)).isEqualTo(1000); + assertThat(mediaMetadata.getText(MediaMetadata.METADATA_KEY_ARTIST).toString()) .isEqualTo("artist"); - assertThat(mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)).isEqualTo("title"); - assertThat(mediaMetadata.getRating(MediaMetadataCompat.METADATA_KEY_RATING).hasHeart()) - .isTrue(); + assertThat(mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE)).isEqualTo("title"); + assertThat(mediaMetadata.getRating(MediaMetadata.METADATA_KEY_RATING).hasHeart()).isTrue(); // Verify that the bitmap instances are the same as the original. Bitmap bitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART); diff --git a/libraries/session/src/test/java/androidx/media3/session/MediaSessionUnitTest.java b/libraries/session/src/test/java/androidx/media3/session/MediaSessionUnitTest.java index 1c619d237d..971338c064 100644 --- a/libraries/session/src/test/java/androidx/media3/session/MediaSessionUnitTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/MediaSessionUnitTest.java @@ -72,7 +72,7 @@ public void isAutomotiveController_automotiveMedia_returnsTrue() { } @Test - public void isAutomotiveController_automotiveMediaMedia3Version_returnsFalse() { + public void isAutomotiveController_automotiveMediaMedia3Version_returnsTrue() { MediaSessionManager.RemoteUserInfo remoteUserInfo = new MediaSessionManager.RemoteUserInfo( /* packageName= */ "com.android.car.media", @@ -83,12 +83,12 @@ public void isAutomotiveController_automotiveMediaMedia3Version_returnsFalse() { remoteUserInfo, MediaLibraryInfo.VERSION_INT, MediaControllerStub.VERSION_INT, - /* trusted= */ false, + /* trusted= */ true, /* cb= */ null, /* connectionHints= */ Bundle.EMPTY, /* maxCommandsForMediaItems= */ 0); - assertThat(session.isAutomotiveController(controllerInfo)).isFalse(); + assertThat(session.isAutomotiveController(controllerInfo)).isTrue(); } @Test @@ -128,7 +128,7 @@ public void isAutoCompanionController_randomPackage_returnsFalse() { } @Test - public void isAutoCompanionController_media3version_returnsFalse() { + public void isAutoCompanionController_media3version_returnsTrue() { MediaSessionManager.RemoteUserInfo remoteUserInfo = new MediaSessionManager.RemoteUserInfo( /* packageName= */ "com.google.android.projection.gearhead", @@ -139,12 +139,12 @@ public void isAutoCompanionController_media3version_returnsFalse() { remoteUserInfo, MediaLibraryInfo.VERSION_INT, MediaControllerStub.VERSION_INT, - /* trusted= */ false, + /* trusted= */ true, /* cb= */ null, /* connectionHints= */ Bundle.EMPTY, /* maxCommandsForMediaItems= */ 0); - assertThat(session.isAutoCompanionController(controllerInfo)).isFalse(); + assertThat(session.isAutoCompanionController(controllerInfo)).isTrue(); } @Test @@ -161,7 +161,7 @@ public void isMediaNotificationController_applicationPackage_returnsTrue() { remoteUserInfo, MediaLibraryInfo.VERSION_INT, MediaControllerStub.VERSION_INT, - /* trusted= */ false, + /* trusted= */ true, /* cb= */ null, connectionHints, /* maxCommandsForMediaItems= */ 0); @@ -203,7 +203,7 @@ public void isMediaNotificationController_applicationPackageMissingBundle_return remoteUserInfo, MediaLibraryInfo.VERSION_INT, MediaControllerStub.VERSION_INT, - /* trusted= */ false, + /* trusted= */ true, /* cb= */ null, /* connectionHints= */ Bundle.EMPTY, /* maxCommandsForMediaItems= */ 0); @@ -225,7 +225,7 @@ public void isMediaNotificationController_applicationPackageLegacyVersion_return remoteUserInfo, MediaSession.ControllerInfo.LEGACY_CONTROLLER_VERSION, MediaSession.ControllerInfo.LEGACY_CONTROLLER_INTERFACE_VERSION, - /* trusted= */ false, + /* trusted= */ true, /* cb= */ null, connectionHints, /* maxCommandsForMediaItems= */ 0); diff --git a/libraries/session/src/test/java/androidx/media3/session/SessionCommandTest.java b/libraries/session/src/test/java/androidx/media3/session/SessionCommandTest.java index 8919800c8d..eadd3ba9f9 100644 --- a/libraries/session/src/test/java/androidx/media3/session/SessionCommandTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/SessionCommandTest.java @@ -90,8 +90,8 @@ public void codes_valueContinuous() throws IllegalAccessException { + (values.get(j - 1) + 1) + " in " + PREFIX_COMMAND_CODES.get(i)) - .that((int) values.get(j)) - .isEqualTo(((int) values.get(j - 1)) + 1); + .that(values.get(j)) + .isEqualTo(values.get(j - 1) + 1); } } } diff --git a/libraries/test_data/src/test/assets/extractordumps/jpeg/pixel-motion-photo-without-exif-shortened.jpg.0.dump b/libraries/test_data/src/test/assets/extractordumps/jpeg/pixel-motion-photo-without-exif-shortened.jpg.0.dump new file mode 100644 index 0000000000..d2deaa06a7 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/jpeg/pixel-motion-photo-without-exif-shortened.jpg.0.dump @@ -0,0 +1,13 @@ +seekMap: + isSeekable = true + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=130611]] + getPosition(1) = [[timeUs=0, position=130611]] +numberOfTracks = 1 +track 1024: + total output bytes = 0 + sample count = 0 + format 0: + containerMimeType = image/jpeg + metadata = entries=[Motion photo metadata: photoStartPosition=0, photoSize=130611, photoPresentationTimestampUs=0, videoStartPosition=130611, videoSize=8730] +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/jpeg/pixel-motion-photo-without-exif-shortened.jpg.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/jpeg/pixel-motion-photo-without-exif-shortened.jpg.unknown_length.dump new file mode 100644 index 0000000000..62796e9eec --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/jpeg/pixel-motion-photo-without-exif-shortened.jpg.unknown_length.dump @@ -0,0 +1,6 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 0 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.0.dump index 7d9542365c..32a6d4504d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.0.dump @@ -21,6 +21,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf60.16.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 27, hash 9F13E633 data = length 8, hash 94643657 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.reading_within_gop_sample_dependencies.0.dump index 7d9542365c..32a6d4504d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.reading_within_gop_sample_dependencies.0.dump @@ -21,6 +21,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf60.16.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 27, hash 9F13E633 data = length 8, hash 94643657 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index 7d9542365c..32a6d4504d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -21,6 +21,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf60.16.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 27, hash 9F13E633 data = length 8, hash 94643657 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.unknown_length.dump index 7d9542365c..32a6d4504d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions.mp4.unknown_length.dump @@ -21,6 +21,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf60.16.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 27, hash 9F13E633 data = length 8, hash 94643657 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.0.dump index 07c4357fb8..120fade274 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.0.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf60.16.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 2426, hash 25737613 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.reading_within_gop_sample_dependencies.0.dump index bf1be0d44c..4bf67ec62f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.reading_within_gop_sample_dependencies.0.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf60.16.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 2426, hash 25737613 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index bf1be0d44c..4bf67ec62f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf60.16.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 2426, hash 25737613 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.unknown_length.dump index 07c4357fb8..120fade274 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/fragmented_captions_h265.mp4.unknown_length.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf60.16.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 2426, hash 25737613 sample 0: 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 b3e044ee9f..52053cffe4 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 @@ -19,7 +19,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3664420004, modification time=3664420004, timescale=1000] sample 0: time = 0 flags = 1 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 fc107b6158..d2da815e3c 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 @@ -19,7 +19,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3664420004, modification time=3664420004, timescale=1000] sample 0: time = 96000 flags = 1 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 003b6bc7f6..5f0f1c8fa3 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 @@ -19,7 +19,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3664420004, modification time=3664420004, timescale=1000] sample 0: time = 192000 flags = 1 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 d97d2cc020..9c39840645 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 @@ -19,7 +19,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3664420004, modification time=3664420004, timescale=1000] sample 0: time = 256000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index b3e044ee9f..52053cffe4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -19,7 +19,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3664420004, modification time=3664420004, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump index fc107b6158..d2da815e3c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump @@ -19,7 +19,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3664420004, modification time=3664420004, timescale=1000] sample 0: time = 96000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump index 003b6bc7f6..5f0f1c8fa3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump @@ -19,7 +19,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3664420004, modification time=3664420004, timescale=1000] sample 0: time = 192000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump index d97d2cc020..9c39840645 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump @@ -19,7 +19,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3664420004, modification time=3664420004, timescale=1000] sample 0: time = 256000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index b3e044ee9f..52053cffe4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac3_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -19,7 +19,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3664420004, modification time=3664420004, timescale=1000] sample 0: time = 0 flags = 1 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 b3e044ee9f..52053cffe4 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 @@ -19,7 +19,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3664420004, modification time=3664420004, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.0.dump index cdf4aa3053..588305a5b2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.0.dump @@ -18,7 +18,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3661133014, modification time=3661133014, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.1.dump index 86d40ffa7f..86af426914 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.1.dump @@ -18,7 +18,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3661133014, modification time=3661133014, timescale=1000] sample 0: time = 240000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.2.dump index 717fa74e80..9a1d34b6d9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.2.dump @@ -18,7 +18,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3661133014, modification time=3661133014, timescale=1000] sample 0: time = 480000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.3.dump index fb480c8ee4..5ae57d2634 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.3.dump @@ -18,7 +18,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3661133014, modification time=3661133014, timescale=1000] sample 0: time = 720000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index cdf4aa3053..588305a5b2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -18,7 +18,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3661133014, modification time=3661133014, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump index 86d40ffa7f..86af426914 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump @@ -18,7 +18,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3661133014, modification time=3661133014, timescale=1000] sample 0: time = 240000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump index 717fa74e80..9a1d34b6d9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump @@ -18,7 +18,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3661133014, modification time=3661133014, timescale=1000] sample 0: time = 480000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump index fb480c8ee4..5ae57d2634 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump @@ -18,7 +18,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3661133014, modification time=3661133014, timescale=1000] sample 0: time = 720000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index cdf4aa3053..588305a5b2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -18,7 +18,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3661133014, modification time=3661133014, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.unknown_length.dump index cdf4aa3053..588305a5b2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_fragmented.mp4.unknown_length.dump @@ -18,7 +18,7 @@ track 0: channelCount = 2 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3661133014, modification time=3661133014, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.0.dump index cce9ceddb0..9a5d576902 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.0.dump @@ -18,7 +18,7 @@ track 0: channelCount = 21 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3785301070, modification time=3785301070, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.1.dump index cce9ceddb0..9a5d576902 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.1.dump @@ -18,7 +18,7 @@ track 0: channelCount = 21 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3785301070, modification time=3785301070, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.2.dump index 3fdfce936a..7bc2c9b361 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.2.dump @@ -18,7 +18,7 @@ track 0: channelCount = 21 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3785301070, modification time=3785301070, timescale=1000] sample 0: time = 426666 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.3.dump index 3fdfce936a..7bc2c9b361 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.3.dump @@ -18,7 +18,7 @@ track 0: channelCount = 21 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3785301070, modification time=3785301070, timescale=1000] sample 0: time = 426666 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index cce9ceddb0..9a5d576902 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -18,7 +18,7 @@ track 0: channelCount = 21 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3785301070, modification time=3785301070, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump index cce9ceddb0..9a5d576902 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump @@ -18,7 +18,7 @@ track 0: channelCount = 21 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3785301070, modification time=3785301070, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump index 3fdfce936a..7bc2c9b361 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump @@ -18,7 +18,7 @@ track 0: channelCount = 21 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3785301070, modification time=3785301070, timescale=1000] sample 0: time = 426666 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump index 3fdfce936a..7bc2c9b361 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump @@ -18,7 +18,7 @@ track 0: channelCount = 21 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3785301070, modification time=3785301070, timescale=1000] sample 0: time = 426666 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index cce9ceddb0..9a5d576902 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -18,7 +18,7 @@ track 0: channelCount = 21 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3785301070, modification time=3785301070, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.unknown_length.dump index cce9ceddb0..9a5d576902 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_level4_fragmented.mp4.unknown_length.dump @@ -18,7 +18,7 @@ track 0: channelCount = 21 sampleRate = 48000 language = und - metadata = entries=[Mp4AlternateGroup: 2] + metadata = entries=[Mp4AlternateGroup: 2, Mp4Timestamp: creation time=3785301070, modification time=3785301070, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.0.dump index d7b596505d..28dc7131d3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.0.dump @@ -19,6 +19,7 @@ track 0: sampleRate = 48000 language = und drmInitData = -1683793742 + metadata = entries=[Mp4Timestamp: creation time=3661481125, modification time=3661481125, timescale=48000] sample 0: time = 0 flags = 1073741825 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.1.dump index 9ea1782d60..21ef543eba 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.1.dump @@ -19,6 +19,7 @@ track 0: sampleRate = 48000 language = und drmInitData = -1683793742 + metadata = entries=[Mp4Timestamp: creation time=3661481125, modification time=3661481125, timescale=48000] sample 0: time = 240000 flags = 1073741825 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.2.dump index 1a86b42315..3863b62a71 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.2.dump @@ -19,6 +19,7 @@ track 0: sampleRate = 48000 language = und drmInitData = -1683793742 + metadata = entries=[Mp4Timestamp: creation time=3661481125, modification time=3661481125, timescale=48000] sample 0: time = 480000 flags = 1073741825 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.3.dump index b6e2962bf0..d7e79703a8 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.3.dump @@ -19,6 +19,7 @@ track 0: sampleRate = 48000 language = und drmInitData = -1683793742 + metadata = entries=[Mp4Timestamp: creation time=3661481125, modification time=3661481125, timescale=48000] sample 0: time = 720000 flags = 1073741825 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.0.dump index d7b596505d..28dc7131d3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.0.dump @@ -19,6 +19,7 @@ track 0: sampleRate = 48000 language = und drmInitData = -1683793742 + metadata = entries=[Mp4Timestamp: creation time=3661481125, modification time=3661481125, timescale=48000] sample 0: time = 0 flags = 1073741825 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.1.dump index 9ea1782d60..21ef543eba 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.1.dump @@ -19,6 +19,7 @@ track 0: sampleRate = 48000 language = und drmInitData = -1683793742 + metadata = entries=[Mp4Timestamp: creation time=3661481125, modification time=3661481125, timescale=48000] sample 0: time = 240000 flags = 1073741825 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.2.dump index 1a86b42315..3863b62a71 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.2.dump @@ -19,6 +19,7 @@ track 0: sampleRate = 48000 language = und drmInitData = -1683793742 + metadata = entries=[Mp4Timestamp: creation time=3661481125, modification time=3661481125, timescale=48000] sample 0: time = 480000 flags = 1073741825 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.3.dump index b6e2962bf0..d7e79703a8 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.3.dump @@ -19,6 +19,7 @@ track 0: sampleRate = 48000 language = und drmInitData = -1683793742 + metadata = entries=[Mp4Timestamp: creation time=3661481125, modification time=3661481125, timescale=48000] sample 0: time = 720000 flags = 1073741825 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index d7b596505d..28dc7131d3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -19,6 +19,7 @@ track 0: sampleRate = 48000 language = und drmInitData = -1683793742 + metadata = entries=[Mp4Timestamp: creation time=3661481125, modification time=3661481125, timescale=48000] sample 0: time = 0 flags = 1073741825 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.unknown_length.dump index d7b596505d..28dc7131d3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_ac4_protected.mp4.unknown_length.dump @@ -19,6 +19,7 @@ track 0: sampleRate = 48000 language = und drmInitData = -1683793742 + metadata = entries=[Mp4Timestamp: creation time=3661481125, modification time=3661481125, timescale=48000] sample 0: time = 0 flags = 1073741825 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 ff4e78a870..d667a5b6bd 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 @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086719, modification time=3662086719, timescale=1000] sample 0: time = 0 flags = 1 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 816c58012c..b2fe4e1581 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 @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086719, modification time=3662086719, timescale=1000] sample 0: time = 576000 flags = 1 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 a50bf3a4ec..9b79dddd87 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 @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086719, modification time=3662086719, timescale=1000] sample 0: time = 1152000 flags = 1 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 8a6f4e592f..e05add6a36 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 @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086719, modification time=3662086719, timescale=1000] sample 0: time = 1696000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index ff4e78a870..d667a5b6bd 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086719, modification time=3662086719, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump index 816c58012c..b2fe4e1581 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086719, modification time=3662086719, timescale=1000] sample 0: time = 576000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump index a50bf3a4ec..9b79dddd87 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086719, modification time=3662086719, timescale=1000] sample 0: time = 1152000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump index 8a6f4e592f..e05add6a36 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086719, modification time=3662086719, timescale=1000] sample 0: time = 1696000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index ff4e78a870..d667a5b6bd 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086719, modification time=3662086719, timescale=1000] sample 0: time = 0 flags = 1 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 ff4e78a870..d667a5b6bd 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 @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086719, modification time=3662086719, timescale=1000] sample 0: time = 0 flags = 1 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 e3b2165cb4..37ca0ee67a 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 @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086977, modification time=3662086977, timescale=1000] sample 0: time = 0 flags = 1 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 dec9e3be4f..a95b4b0033 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 @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086977, modification time=3662086977, timescale=1000] sample 0: time = 672000 flags = 1 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 0df5e48778..3f5ff4f738 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 @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086977, modification time=3662086977, timescale=1000] sample 0: time = 1344000 flags = 1 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 0f5044e631..e4372fd837 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 @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086977, modification time=3662086977, timescale=1000] sample 0: time = 2016000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index e3b2165cb4..37ca0ee67a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086977, modification time=3662086977, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump index dec9e3be4f..a95b4b0033 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.1.dump @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086977, modification time=3662086977, timescale=1000] sample 0: time = 672000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump index 0df5e48778..3f5ff4f738 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.2.dump @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086977, modification time=3662086977, timescale=1000] sample 0: time = 1344000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump index 0f5044e631..e4372fd837 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.3.dump @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086977, modification time=3662086977, timescale=1000] sample 0: time = 2016000 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index e3b2165cb4..37ca0ee67a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_eac3joc_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086977, modification time=3662086977, timescale=1000] sample 0: time = 0 flags = 1 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 e3b2165cb4..37ca0ee67a 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 @@ -18,6 +18,7 @@ track 0: channelCount = 6 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3662086977, modification time=3662086977, timescale=1000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump index a5288ae424..57e75bf47f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -153,7 +154,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index 4bacee8139..00ba717aed 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -153,7 +154,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index 4bacee8139..00ba717aed 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -153,7 +154,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump index a5288ae424..57e75bf47f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -153,7 +154,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.0.dump index 5c44f2581c..aba1424c48 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.0.dump @@ -14,7 +14,9 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm language = und + metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] initializationData: data = length 119, hash 99A80807 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.1.dump index 6bcf994bbd..6e373b293f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.1.dump @@ -14,7 +14,9 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm language = und + metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] initializationData: data = length 119, hash 99A80807 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.2.dump index aeeefd0d83..67fdf1ac4e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.2.dump @@ -14,7 +14,9 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm language = und + metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] initializationData: data = length 119, hash 99A80807 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.3.dump index fbe7a690dd..fd986cf01a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.3.dump @@ -14,7 +14,9 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm language = und + metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] initializationData: data = length 119, hash 99A80807 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.0.dump index 5c44f2581c..aba1424c48 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.0.dump @@ -14,7 +14,9 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm language = und + metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] initializationData: data = length 119, hash 99A80807 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.1.dump index 6bcf994bbd..6e373b293f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.1.dump @@ -14,7 +14,9 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm language = und + metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] initializationData: data = length 119, hash 99A80807 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.2.dump index aeeefd0d83..67fdf1ac4e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.2.dump @@ -14,7 +14,9 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm language = und + metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] initializationData: data = length 119, hash 99A80807 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.3.dump index fbe7a690dd..fd986cf01a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.3.dump @@ -14,7 +14,9 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm language = und + metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] initializationData: data = length 119, hash 99A80807 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index 5c44f2581c..aba1424c48 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -14,7 +14,9 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm language = und + metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] initializationData: data = length 119, hash 99A80807 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.unknown_length.dump index 5c44f2581c..aba1424c48 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_iamf.mp4.unknown_length.dump @@ -14,7 +14,9 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm language = und + metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] initializationData: data = length 119, hash 99A80807 sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump index fc7c2c2000..fda14506ad 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump index f294618902..4047ff7f67 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump index 9c851f7fbb..f4197d97e0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump index 7bf2fae912..93c59b6d83 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.0.dump index 7e2e113b77..7688f94414 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.0.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.1.dump index e6bd7f50ee..18701e4eb9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.1.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.2.dump index 7722180b5b..b45ed003e8 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.2.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.3.dump index 46b57d4134..a6bc6aa2d5 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.3.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index 7e2e113b77..7688f94414 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump index fc7c2c2000..fda14506ad 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump index e01c12b188..7bdbe3f804 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump index 4270f1998e..24c374cc1f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump index 8bc7c2ab78..41fbe6af61 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump index 5567019179..5335d0d2b3 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.0.dump index a17acc1364..49d7705b37 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.0.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.1.dump index 6495092e0a..3b142fbcb7 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.1.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.2.dump index fb79e31973..063cd02cf5 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.2.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.3.dump index 2b16c30918..e88f6f245d 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.3.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index a17acc1364..49d7705b37 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump index e01c12b188..7bdbe3f804 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -161,7 +162,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, Mp4Timestamp: creation time=3604480265, modification time=3604480265, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.0.dump index fe1a854aca..f97a5b458b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.0.dump @@ -23,6 +23,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf61.7.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 30, hash 90F06712 data = length 10, hash 7A0D0F2B diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.1.dump index fe1a854aca..f97a5b458b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.1.dump @@ -23,6 +23,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf61.7.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 30, hash 90F06712 data = length 10, hash 7A0D0F2B diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.2.dump index f0545b8b6d..f6faa08ecb 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.2.dump @@ -23,6 +23,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf61.7.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 30, hash 90F06712 data = length 10, hash 7A0D0F2B diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.3.dump index 59dc04aa11..d47b135412 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.3.dump @@ -23,6 +23,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf61.7.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 30, hash 90F06712 data = length 10, hash 7A0D0F2B diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.unknown_length.dump index fe1a854aca..f97a5b458b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.unknown_length.dump @@ -23,6 +23,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf61.7.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 30, hash 90F06712 data = length 10, hash 7A0D0F2B diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.0.dump index d406a5a205..cf0b315ef6 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.0.dump @@ -23,6 +23,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf61.7.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 30, hash 90F06712 data = length 10, hash 7A0D0F2B diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.1.dump index d406a5a205..cf0b315ef6 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.1.dump @@ -23,6 +23,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf61.7.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 30, hash 90F06712 data = length 10, hash 7A0D0F2B diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.2.dump index d406a5a205..cf0b315ef6 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.2.dump @@ -23,6 +23,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf61.7.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 30, hash 90F06712 data = length 10, hash 7A0D0F2B diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.3.dump index d406a5a205..cf0b315ef6 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.3.dump @@ -23,6 +23,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf61.7.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 30, hash 90F06712 data = length 10, hash 7A0D0F2B diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.unknown_length.dump index d406a5a205..cf0b315ef6 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.unknown_length.dump @@ -23,6 +23,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf61.7.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 30, hash 90F06712 data = length 10, hash 7A0D0F2B diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump index c28fc789e0..8cccba0e03 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -153,7 +154,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.reading_within_gop_sample_dependencies.0.dump index 16f3db49d8..46ba95874f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.reading_within_gop_sample_dependencies.0.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -153,7 +154,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index 16f3db49d8..46ba95874f 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -153,7 +154,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump index c28fc789e0..8cccba0e03 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -153,7 +154,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf54.20.4], Mp4Timestamp: creation time=3547558895, modification time=3547558895, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.0.dump index dc01361772..5f54fe2cf5 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.0.dump @@ -15,6 +15,7 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm maxInputSize = 299 language = und metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.1.dump index b594e72e49..339ec6e3e9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.1.dump @@ -15,6 +15,7 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm maxInputSize = 299 language = und metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.2.dump index 6359704b97..988fef858b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.2.dump @@ -15,6 +15,7 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm maxInputSize = 299 language = und metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.3.dump index 83f90dc495..e7b2217254 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.3.dump @@ -15,6 +15,7 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm maxInputSize = 299 language = und metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.0.dump index dc01361772..5f54fe2cf5 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.0.dump @@ -15,6 +15,7 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm maxInputSize = 299 language = und metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.1.dump index b594e72e49..339ec6e3e9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.1.dump @@ -15,6 +15,7 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm maxInputSize = 299 language = und metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.2.dump index 6359704b97..988fef858b 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.2.dump @@ -15,6 +15,7 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm maxInputSize = 299 language = und metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.3.dump index 83f90dc495..e7b2217254 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.3.dump @@ -15,6 +15,7 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm maxInputSize = 299 language = und metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index dc01361772..5f54fe2cf5 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -15,6 +15,7 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm maxInputSize = 299 language = und metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.unknown_length.dump index dc01361772..5f54fe2cf5 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_iamf.mp4.unknown_length.dump @@ -15,6 +15,7 @@ track 0: id = 1 containerMimeType = audio/mp4 sampleMimeType = audio/iamf + codecs = iamf.000.000.ipcm maxInputSize = 299 language = und metadata = entries=[Mp4Timestamp: creation time=3764707200, modification time=3764707200, timescale=16000] diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.0.dump index 284df2f026..39b7c3eb7e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.0.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733150, modification time=3782733150, timescale=600] initializationData: data = length 60, hash C05CB07B sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index 284df2f026..39b7c3eb7e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733150, modification time=3782733150, timescale=600] initializationData: data = length 60, hash C05CB07B sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index 284df2f026..39b7c3eb7e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733150, modification time=3782733150, timescale=600] initializationData: data = length 60, hash C05CB07B sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.unknown_length.dump index 284df2f026..39b7c3eb7e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_cicp1_fragmented.mp4.unknown_length.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733150, modification time=3782733150, timescale=600] initializationData: data = length 60, hash C05CB07B sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.0.dump index 5decd316e8..e067bb935e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.0.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733090, modification time=3782733090, timescale=600] initializationData: data = length 64, hash DB1F936C sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index 5decd316e8..e067bb935e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733090, modification time=3782733090, timescale=600] initializationData: data = length 64, hash DB1F936C sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index 5decd316e8..e067bb935e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733090, modification time=3782733090, timescale=600] initializationData: data = length 64, hash DB1F936C sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.unknown_length.dump index 5decd316e8..e067bb935e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_bl_configchange_fragmented.mp4.unknown_length.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733090, modification time=3782733090, timescale=600] initializationData: data = length 64, hash DB1F936C sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.0.dump index 719f5229f0..b4f4cfa264 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.0.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733150, modification time=3782733150, timescale=600] initializationData: data = length 63, hash 7D954866 data = length 1, hash 2F diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index 719f5229f0..b4f4cfa264 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733150, modification time=3782733150, timescale=600] initializationData: data = length 63, hash 7D954866 data = length 1, hash 2F diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index 719f5229f0..b4f4cfa264 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733150, modification time=3782733150, timescale=600] initializationData: data = length 63, hash 7D954866 data = length 1, hash 2F diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.unknown_length.dump index 719f5229f0..b4f4cfa264 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_cicp1_fragmented.mp4.unknown_length.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733150, modification time=3782733150, timescale=600] initializationData: data = length 63, hash 7D954866 data = length 1, hash 2F diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.0.dump index 7e77e2768c..d2401112de 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.0.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733091, modification time=3782733091, timescale=600] initializationData: data = length 67, hash 3CF14937 data = length 1, hash 2F diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index 7e77e2768c..d2401112de 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733091, modification time=3782733091, timescale=600] initializationData: data = length 67, hash 3CF14937 data = length 1, hash 2F diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index 7e77e2768c..d2401112de 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733091, modification time=3782733091, timescale=600] initializationData: data = length 67, hash 3CF14937 data = length 1, hash 2F diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.unknown_length.dump index 7e77e2768c..d2401112de 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mhm1_lcbl_configchange_fragmented.mp4.unknown_length.dump @@ -14,6 +14,7 @@ track 0: channelCount = 0 sampleRate = 48000 language = und + metadata = entries=[Mp4Timestamp: creation time=3782733091, modification time=3782733091, timescale=600] initializationData: data = length 67, hash 3CF14937 data = length 1, hash 2F diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.0.dump index c26cdc1f2a..cbf6269565 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.0.dump @@ -12,6 +12,7 @@ track 0: sampleMimeType = audio/opus channelCount = 2 sampleRate = 16000 + metadata = entries=[Mp4Timestamp: creation time=0, modification time=0, timescale=1000000] initializationData: data = length 19, hash 4034F23B data = length 8, hash 94446F01 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index c26cdc1f2a..cbf6269565 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -12,6 +12,7 @@ track 0: sampleMimeType = audio/opus channelCount = 2 sampleRate = 16000 + metadata = entries=[Mp4Timestamp: creation time=0, modification time=0, timescale=1000000] initializationData: data = length 19, hash 4034F23B data = length 8, hash 94446F01 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index c26cdc1f2a..cbf6269565 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -12,6 +12,7 @@ track 0: sampleMimeType = audio/opus channelCount = 2 sampleRate = 16000 + metadata = entries=[Mp4Timestamp: creation time=0, modification time=0, timescale=1000000] initializationData: data = length 19, hash 4034F23B data = length 8, hash 94446F01 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.unknown_length.dump index c26cdc1f2a..cbf6269565 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_opus_fragmented.mp4.unknown_length.dump @@ -12,6 +12,7 @@ track 0: sampleMimeType = audio/opus channelCount = 2 sampleRate = 16000 + metadata = entries=[Mp4Timestamp: creation time=0, modification time=0, timescale=1000000] initializationData: data = length 19, hash 4034F23B data = length 8, hash 94446F01 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump index 90bc77cd09..ff16eb3506 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump @@ -21,6 +21,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -158,7 +159,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf58.29.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump index 35e39b7df4..c4ea2a7ec9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.reading_within_gop_sample_dependencies.0.dump @@ -21,6 +21,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -158,7 +159,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf58.29.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump index 35e39b7df4..c4ea2a7ec9 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.reading_within_gop_sample_dependencies.unknown_length.dump @@ -21,6 +21,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -158,7 +159,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf58.29.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump index 90bc77cd09..ff16eb3506 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump @@ -21,6 +21,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[TSSE: description=null: values=[Lavf58.29.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B @@ -158,7 +159,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und - metadata = entries=[Mp4AlternateGroup: 1] + metadata = entries=[Mp4AlternateGroup: 1, TSSE: description=null: values=[Lavf58.29.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/media/jpeg/pixel-motion-photo-without-exif-shortened.jpg b/libraries/test_data/src/test/assets/media/jpeg/pixel-motion-photo-without-exif-shortened.jpg new file mode 100644 index 0000000000..0080b1df12 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/jpeg/pixel-motion-photo-without-exif-shortened.jpg differ diff --git a/libraries/test_data/src/test/assets/media/mp4/sample_with_vobsub.mp4 b/libraries/test_data/src/test/assets/media/mp4/sample_with_vobsub.mp4 new file mode 100644 index 0000000000..6606420ed3 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/sample_with_vobsub.mp4 differ diff --git a/libraries/test_data/src/test/assets/media/mpd/sample_mpd_trick_play_property_incompatible b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_trick_play_property_incompatible new file mode 100644 index 0000000000..8a103719aa --- /dev/null +++ b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_trick_play_property_incompatible @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_176x144_128kbps_15fps_h263.3gp_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_176x144_128kbps_15fps_h263.3gp_fragmented.dump index 69a3225001..16e8d544ab 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_176x144_128kbps_15fps_h263.3gp_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_176x144_128kbps_15fps_h263.3gp_fragmented.dump @@ -16,6 +16,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_176x144_192kbps_15fps_mpeg4.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_176x144_192kbps_15fps_mpeg4.mp4_fragmented.dump index 814ace0b79..eec3a8b0bd 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_176x144_192kbps_15fps_mpeg4.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_176x144_192kbps_15fps_mpeg4.mp4_fragmented.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 47, hash DC4DD041 sample 0: diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_1ch_16kHz_q10_vorbis.ogg_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_1ch_16kHz_q10_vorbis.ogg_fragmented.dump index 72b436ab2c..acf79fb750 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_1ch_16kHz_q10_vorbis.ogg_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_1ch_16kHz_q10_vorbis.ogg_fragmented.dump @@ -13,6 +13,7 @@ track 0: sampleMimeType = audio/vorbis channelCount = 1 sampleRate = 16000 + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 30, hash C22462B1 data = length 3539, hash F8106892 diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_2ch_44kHz.wav_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_2ch_44kHz.wav_fragmented.dump index 4a67081e33..24c75597c6 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_2ch_44kHz.wav_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_2ch_44kHz.wav_fragmented.dump @@ -13,6 +13,7 @@ track 0: channelCount = 2 sampleRate = 44100 pcmEncoding = 2 + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_642x642_768kbps_30fps_vp9.webm_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_642x642_768kbps_30fps_vp9.webm_fragmented.dump index f9d0c78e1c..a96dc242eb 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_642x642_768kbps_30fps_vp9.webm_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_642x642_768kbps_30fps_vp9.webm_fragmented.dump @@ -19,6 +19,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 12, hash 31B20D86 sample 0: diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_6ch_8kHz_opus.ogg_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_6ch_8kHz_opus.ogg_fragmented.dump index b581cf1ee5..85f39bc218 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_6ch_8kHz_opus.ogg_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_6ch_8kHz_opus.ogg_fragmented.dump @@ -12,6 +12,7 @@ track 0: sampleMimeType = audio/opus channelCount = 6 sampleRate = 48000 + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 27, hash 9EE6F879 data = length 8, hash CA22068C diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_2b_firstpts_10_sec.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_2b_firstpts_10_sec.mp4_fragmented.dump index ba12635ead..350e9565cc 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_2b_firstpts_10_sec.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_2b_firstpts_10_sec.mp4_fragmented.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 33, hash E354B60D data = length 10, hash 7A0D0F2B diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_non_reference_3b.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_non_reference_3b.mp4_fragmented.dump index 3aebffdc10..eb00bb762c 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_non_reference_3b.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_non_reference_3b.mp4_fragmented.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 33, hash 800C8D16 data = length 9, hash FBAE9B2D diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_pyramid_3b.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_pyramid_3b.mp4_fragmented.dump index 3c637f074d..8efd9ec8d8 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_pyramid_3b.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_avc_pyramid_3b.mp4_fragmented.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 33, hash A51E6AF9 data = length 9, hash FBAE9B2D diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_vp9.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_vp9.mp4_fragmented.dump index 8b45ca10e9..c513a27abf 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_vp9.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_800x640_768kbps_30fps_vp9.mp4_fragmented.dump @@ -19,6 +19,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 12, hash 53AEAE9A sample 0: diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_mono_16kHz_23.05kbps_amrwb.3gp_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_mono_16kHz_23.05kbps_amrwb.3gp_fragmented.dump index 3d8675a2de..6c86bdfabc 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_mono_16kHz_23.05kbps_amrwb.3gp_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_mono_16kHz_23.05kbps_amrwb.3gp_fragmented.dump @@ -13,6 +13,7 @@ track 0: channelCount = 1 sampleRate = 16000 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/muxerdumps/bbb_mono_8kHz_12.2kbps_amrnb.3gp_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/bbb_mono_8kHz_12.2kbps_amrnb.3gp_fragmented.dump index 4d7986d12c..60683dfaf2 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/bbb_mono_8kHz_12.2kbps_amrnb.3gp_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/bbb_mono_8kHz_12.2kbps_amrnb.3gp_fragmented.dump @@ -13,6 +13,7 @@ track 0: channelCount = 1 sampleRate = 8000 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/muxerdumps/h265_with_metadata_track.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/h265_with_metadata_track.mp4_fragmented.dump index 20af9685cf..249947fae2 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/h265_with_metadata_track.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/h265_with_metadata_track.mp4_fragmented.dump @@ -10,6 +10,7 @@ track 0: id = 1 containerMimeType = video/mp4 sampleMimeType = application/meta + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] sample 0: time = 0 flags = 1 @@ -34,6 +35,7 @@ track 1: codecs = mp4a.40.2 channelCount = 2 sampleRate = 48000 + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 2, hash 560 sample 0: @@ -68,6 +70,7 @@ track 2: colorTransfer = 3 lumaBitdepth = 8 chromaBitdepth = 8 + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 85, hash 6F3CAA16 sample 0: diff --git a/libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented.dump index 9b5414e9fc..371f00e9af 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented.dump @@ -23,6 +23,7 @@ track 0: colorTransfer = 6 lumaBitdepth = 10 chromaBitdepth = 10 + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 99, hash 99842E5A sample 0: @@ -545,6 +546,7 @@ track 1: codecs = mp4a.40.2 channelCount = 2 sampleRate = 48000 + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 2, hash 560 sample 0: diff --git a/libraries/test_data/src/test/assets/muxerdumps/sample_av1.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/sample_av1.mp4_fragmented.dump index cac20bfeed..760308250a 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/sample_av1.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/sample_av1.mp4_fragmented.dump @@ -16,6 +16,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 17, hash 54AC4E6D sample 0: @@ -151,6 +152,7 @@ track 1: channelCount = 1 sampleRate = 44100 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 5, hash 2B7623A sample 0: diff --git a/libraries/test_data/src/test/assets/muxerdumps/sample_edit_list.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/sample_edit_list.mp4_fragmented.dump index 2fb23a4f94..2d670e7120 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/sample_edit_list.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/sample_edit_list.mp4_fragmented.dump @@ -24,6 +24,7 @@ track 0: lumaBitdepth = 10 chromaBitdepth = 10 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 97, hash 32FB3D18 data = length 24, hash A31E9935 @@ -388,6 +389,7 @@ track 1: lumaBitdepth = 10 chromaBitdepth = 10 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 97, hash 32FB3D18 sample 0: @@ -743,6 +745,7 @@ track 2: channelCount = 2 sampleRate = 44100 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 2, hash 5FF sample 0: diff --git a/libraries/test_data/src/test/assets/muxerdumps/sample_no_bframes.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/sample_no_bframes.mp4_fragmented.dump index 294a642914..97798e69d5 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/sample_no_bframes.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/sample_no_bframes.mp4_fragmented.dump @@ -15,6 +15,7 @@ track 0: channelCount = 1 sampleRate = 44100 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 2, hash 5F7 sample 0: @@ -214,6 +215,7 @@ track 1: colorTransfer = 3 lumaBitdepth = 8 chromaBitdepth = 8 + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 23, hash 33E412EE data = length 9, hash FBAFBC0C diff --git a/libraries/test_data/src/test/assets/muxerdumps/sample_with_apvc.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/sample_with_apvc.mp4_fragmented.dump index 7c1c6c0aac..ee36354682 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/sample_with_apvc.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/sample_with_apvc.mp4_fragmented.dump @@ -15,6 +15,7 @@ track 0: colorInfo: lumaBitdepth = 10 chromaBitdepth = 10 + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 18, hash 77EBC81 sample 0: diff --git a/libraries/test_data/src/test/assets/muxerdumps/video_dovi_1920x1080_60fps_dvav_09.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/video_dovi_1920x1080_60fps_dvav_09.mp4_fragmented.dump index c38ebab68e..0cc27c52fe 100644 --- a/libraries/test_data/src/test/assets/muxerdumps/video_dovi_1920x1080_60fps_dvav_09.mp4_fragmented.dump +++ b/libraries/test_data/src/test/assets/muxerdumps/video_dovi_1920x1080_60fps_dvav_09.mp4_fragmented.dump @@ -18,6 +18,7 @@ track 0: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 41, hash CD082D34 data = length 8, hash 9481C0CD @@ -345,6 +346,7 @@ track 1: lumaBitdepth = 8 chromaBitdepth = 8 language = und + metadata = entries=[Mp4Timestamp: creation time=100000000, modification time=500000000, timescale=10000] initializationData: data = length 41, hash CD082D34 data = length 8, hash 9481C0CD diff --git a/libraries/test_data/src/test/assets/playbackdumps/mp4/sample_with_vobsub.mp4.dump b/libraries/test_data/src/test/assets/playbackdumps/mp4/sample_with_vobsub.mp4.dump new file mode 100644 index 0000000000..fef259ef0c --- /dev/null +++ b/libraries/test_data/src/test/assets/playbackdumps/mp4/sample_with_vobsub.mp4.dump @@ -0,0 +1,539 @@ +MediaCodecAdapter (media3.audio.ac3): + inputBuffers: + count = 30 + input buffer #0: + timeUs = 1000000000000 + contents = length 416, hash 211F2286 + input buffer #1: + timeUs = 1000000034512 + contents = length 418, hash 77425A86 + input buffer #2: + timeUs = 1000000069342 + contents = length 418, hash A0FE5CA1 + input buffer #3: + timeUs = 1000000104172 + contents = length 418, hash 2309B066 + input buffer #4: + timeUs = 1000000139002 + contents = length 418, hash 928A653B + input buffer #5: + timeUs = 1000000173514 + contents = length 418, hash 3422F0CB + input buffer #6: + timeUs = 1000000208344 + contents = length 418, hash EFF43D5B + input buffer #7: + timeUs = 1000000243174 + contents = length 418, hash FC8093C7 + input buffer #8: + timeUs = 1000000278480 + contents = length 418, hash CCC08A16 + input buffer #9: + timeUs = 1000000313310 + contents = length 418, hash 2A6EE863 + input buffer #10: + timeUs = 1000000348140 + contents = length 418, hash D69A9251 + input buffer #11: + timeUs = 1000000382970 + contents = length 418, hash BCFB758D + input buffer #12: + timeUs = 1000000417800 + contents = length 418, hash 11B66799 + input buffer #13: + timeUs = 1000000452517 + contents = length 418, hash C824D392 + input buffer #14: + timeUs = 1000000487346 + contents = length 418, hash C167D872 + input buffer #15: + timeUs = 1000000522176 + contents = length 418, hash 4221C855 + input buffer #16: + timeUs = 1000000557006 + contents = length 418, hash 4D4FF934 + input buffer #17: + timeUs = 1000000591519 + contents = length 418, hash 984AA025 + input buffer #18: + timeUs = 1000000626349 + contents = length 418, hash BB788B46 + input buffer #19: + timeUs = 1000000661179 + contents = length 418, hash 9EFBFD97 + input buffer #20: + timeUs = 1000000696009 + contents = length 418, hash DF1A460C + input buffer #21: + timeUs = 1000000730521 + contents = length 418, hash 2BDB56A + input buffer #22: + timeUs = 1000000765351 + contents = length 418, hash CA230060 + input buffer #23: + timeUs = 1000000800181 + contents = length 418, hash D2F19F41 + input buffer #24: + timeUs = 1000000835487 + contents = length 418, hash AF392D79 + input buffer #25: + timeUs = 1000000870317 + contents = length 418, hash C5D7F2A3 + input buffer #26: + timeUs = 1000000905147 + contents = length 418, hash 733A35AE + input buffer #27: + timeUs = 1000000939977 + contents = length 418, hash DE46E5D3 + input buffer #28: + timeUs = 1000000974807 + contents = length 418, hash 56AB8D37 + input buffer #29: + timeUs = 0 + flags = 4 + contents = length 0, hash 1 + outputBuffers: + count = 29 + output buffer #0: + timeUs = 1000000000000 + size = 0 + rendered = false + output buffer #1: + timeUs = 1000000034512 + size = 0 + rendered = false + output buffer #2: + timeUs = 1000000069342 + size = 0 + rendered = false + output buffer #3: + timeUs = 1000000104172 + size = 0 + rendered = false + output buffer #4: + timeUs = 1000000139002 + size = 0 + rendered = false + output buffer #5: + timeUs = 1000000173514 + size = 0 + rendered = false + output buffer #6: + timeUs = 1000000208344 + size = 0 + rendered = false + output buffer #7: + timeUs = 1000000243174 + size = 0 + rendered = false + output buffer #8: + timeUs = 1000000278480 + size = 0 + rendered = false + output buffer #9: + timeUs = 1000000313310 + size = 0 + rendered = false + output buffer #10: + timeUs = 1000000348140 + size = 0 + rendered = false + output buffer #11: + timeUs = 1000000382970 + size = 0 + rendered = false + output buffer #12: + timeUs = 1000000417800 + size = 0 + rendered = false + output buffer #13: + timeUs = 1000000452517 + size = 0 + rendered = false + output buffer #14: + timeUs = 1000000487346 + size = 0 + rendered = false + output buffer #15: + timeUs = 1000000522176 + size = 0 + rendered = false + output buffer #16: + timeUs = 1000000557006 + size = 0 + rendered = false + output buffer #17: + timeUs = 1000000591519 + size = 0 + rendered = false + output buffer #18: + timeUs = 1000000626349 + size = 0 + rendered = false + output buffer #19: + timeUs = 1000000661179 + size = 0 + rendered = false + output buffer #20: + timeUs = 1000000696009 + size = 0 + rendered = false + output buffer #21: + timeUs = 1000000730521 + size = 0 + rendered = false + output buffer #22: + timeUs = 1000000765351 + size = 0 + rendered = false + output buffer #23: + timeUs = 1000000800181 + size = 0 + rendered = false + output buffer #24: + timeUs = 1000000835487 + size = 0 + rendered = false + output buffer #25: + timeUs = 1000000870317 + size = 0 + rendered = false + output buffer #26: + timeUs = 1000000905147 + size = 0 + rendered = false + output buffer #27: + timeUs = 1000000939977 + size = 0 + rendered = false + output buffer #28: + timeUs = 1000000974807 + size = 0 + rendered = false +MediaCodecAdapter (media3.video.avc): + inputBuffers: + count = 31 + input buffer #0: + timeUs = 1000000000000 + contents = length 36517, hash B334DF25 + input buffer #1: + timeUs = 1000000003000 + contents = length 5341, hash 40B85E2 + input buffer #2: + timeUs = 1000000002000 + contents = length 596, hash 357B4D92 + input buffer #3: + timeUs = 1000000010000 + contents = length 7704, hash A39EDA06 + input buffer #4: + timeUs = 1000000007000 + contents = length 989, hash 2813C72D + input buffer #5: + timeUs = 1000000005000 + contents = length 721, hash C50D1C73 + input buffer #6: + timeUs = 1000000008000 + contents = length 519, hash 65FE1911 + input buffer #7: + timeUs = 1000000017000 + contents = length 6160, hash E1CAC0EC + input buffer #8: + timeUs = 1000000013000 + contents = length 953, hash 7160C661 + input buffer #9: + timeUs = 1000000012000 + contents = length 620, hash 7A7AE07C + input buffer #10: + timeUs = 1000000015000 + contents = length 405, hash 5CC7F4E7 + input buffer #11: + timeUs = 1000000022000 + contents = length 4852, hash 9DB6979D + input buffer #12: + timeUs = 1000000020000 + contents = length 547, hash E31A6979 + input buffer #13: + timeUs = 1000000018000 + contents = length 570, hash FEC40D00 + input buffer #14: + timeUs = 1000000028000 + contents = length 5525, hash 7C478F7E + input buffer #15: + timeUs = 1000000025000 + contents = length 1082, hash DA07059A + input buffer #16: + timeUs = 1000000023000 + contents = length 807, hash 93478E6B + input buffer #17: + timeUs = 1000000027000 + contents = length 744, hash 9A8E6026 + input buffer #18: + timeUs = 1000000035000 + contents = length 4732, hash C73B23C0 + input buffer #19: + timeUs = 1000000032000 + contents = length 1004, hash 8A19A228 + input buffer #20: + timeUs = 1000000030000 + contents = length 794, hash 8126022C + input buffer #21: + timeUs = 1000000033000 + contents = length 645, hash F08300E5 + input buffer #22: + timeUs = 1000000042000 + contents = length 2684, hash 727FE378 + input buffer #23: + timeUs = 1000000038000 + contents = length 787, hash 419A7821 + input buffer #24: + timeUs = 1000000037000 + contents = length 649, hash 5C159346 + input buffer #25: + timeUs = 1000000040000 + contents = length 509, hash F912D655 + input buffer #26: + timeUs = 1000000048000 + contents = length 1226, hash 29815C21 + input buffer #27: + timeUs = 1000000045000 + contents = length 898, hash D997AD0A + input buffer #28: + timeUs = 1000000043000 + contents = length 476, hash A0423645 + input buffer #29: + timeUs = 1000000047000 + contents = length 486, hash DDF32CBB + input buffer #30: + timeUs = 0 + flags = 4 + contents = length 0, hash 1 + outputBuffers: + count = 30 + output buffer #0: + timeUs = 1000000000000 + size = 36517 + rendered = true + output buffer #1: + timeUs = 1000000003000 + size = 5341 + rendered = true + output buffer #2: + timeUs = 1000000002000 + size = 596 + rendered = true + output buffer #3: + timeUs = 1000000010000 + size = 7704 + rendered = true + output buffer #4: + timeUs = 1000000007000 + size = 989 + rendered = true + output buffer #5: + timeUs = 1000000005000 + size = 721 + rendered = true + output buffer #6: + timeUs = 1000000008000 + size = 519 + rendered = true + output buffer #7: + timeUs = 1000000017000 + size = 6160 + rendered = true + output buffer #8: + timeUs = 1000000013000 + size = 953 + rendered = true + output buffer #9: + timeUs = 1000000012000 + size = 620 + rendered = true + output buffer #10: + timeUs = 1000000015000 + size = 405 + rendered = true + output buffer #11: + timeUs = 1000000022000 + size = 4852 + rendered = true + output buffer #12: + timeUs = 1000000020000 + size = 547 + rendered = true + output buffer #13: + timeUs = 1000000018000 + size = 570 + rendered = true + output buffer #14: + timeUs = 1000000028000 + size = 5525 + rendered = true + output buffer #15: + timeUs = 1000000025000 + size = 1082 + rendered = true + output buffer #16: + timeUs = 1000000023000 + size = 807 + rendered = true + output buffer #17: + timeUs = 1000000027000 + size = 744 + rendered = true + output buffer #18: + timeUs = 1000000035000 + size = 4732 + rendered = true + output buffer #19: + timeUs = 1000000032000 + size = 1004 + rendered = true + output buffer #20: + timeUs = 1000000030000 + size = 794 + rendered = true + output buffer #21: + timeUs = 1000000033000 + size = 645 + rendered = true + output buffer #22: + timeUs = 1000000042000 + size = 2684 + rendered = true + output buffer #23: + timeUs = 1000000038000 + size = 787 + rendered = true + output buffer #24: + timeUs = 1000000037000 + size = 649 + rendered = true + output buffer #25: + timeUs = 1000000040000 + size = 509 + rendered = true + output buffer #26: + timeUs = 1000000048000 + size = 1226 + rendered = true + output buffer #27: + timeUs = 1000000045000 + size = 898 + rendered = true + output buffer #28: + timeUs = 1000000043000 + size = 476 + rendered = true + output buffer #29: + timeUs = 1000000047000 + size = 486 + rendered = true +AudioSink: + buffer count = 29 + config: + pcmEncoding = 2 + channelCount = 1 + sampleRate = 44100 + buffer #0: + time = 1000000000000 + data = empty + buffer #1: + time = 1000000034512 + data = empty + buffer #2: + time = 1000000069342 + data = empty + buffer #3: + time = 1000000104172 + data = empty + buffer #4: + time = 1000000139002 + data = empty + buffer #5: + time = 1000000173514 + data = empty + buffer #6: + time = 1000000208344 + data = empty + buffer #7: + time = 1000000243174 + data = empty + buffer #8: + time = 1000000278480 + data = empty + buffer #9: + time = 1000000313310 + data = empty + buffer #10: + time = 1000000348140 + data = empty + buffer #11: + time = 1000000382970 + data = empty + buffer #12: + time = 1000000417800 + data = empty + buffer #13: + time = 1000000452517 + data = empty + buffer #14: + time = 1000000487346 + data = empty + buffer #15: + time = 1000000522176 + data = empty + buffer #16: + time = 1000000557006 + data = empty + buffer #17: + time = 1000000591519 + data = empty + buffer #18: + time = 1000000626349 + data = empty + buffer #19: + time = 1000000661179 + data = empty + buffer #20: + time = 1000000696009 + data = empty + buffer #21: + time = 1000000730521 + data = empty + buffer #22: + time = 1000000765351 + data = empty + buffer #23: + time = 1000000800181 + data = empty + buffer #24: + time = 1000000835487 + data = empty + buffer #25: + time = 1000000870317 + data = empty + buffer #26: + time = 1000000905147 + data = empty + buffer #27: + time = 1000000939977 + data = empty + buffer #28: + time = 1000000974807 + data = empty +TextOutput: + Subtitle[0]: + presentationTimeUs = 0 + Cues = [] + Subtitle[1]: + presentationTimeUs = 0 + Cue[0]: + bitmap = length 296960, hash 6C5C74A1 + line = 0.88611114 + lineType = 0 + lineAnchor = 0 + position = 0.0 + positionAnchor = 0 + size = 1.0 + bitmapHeight = 0.08055556 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 88baaa301c..d77e053ffb 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 @@ -283,8 +283,7 @@ public void sendCustomCommandWithMediaItem_mediaItemIdConvertedCorrectly() throw } @Test - public void sendCustomCommandWithMediaItem_commandButtonNotAvailable_permissionDenied() - throws Exception { + public void sendCustomCommandWithMediaItem_commandButtonNotAvailable_succeeds() throws Exception { remoteService.setProxyForTest(TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS); MediaBrowser mediaBrowser = createBrowser( @@ -312,7 +311,7 @@ public void sendCustomCommandWithMediaItem_commandButtonNotAvailable_permissionD /* args= */ Bundle.EMPTY)) .get(TIMEOUT_MS, MILLISECONDS); - assertThat(sessionResult.resultCode).isEqualTo(SessionResult.RESULT_ERROR_PERMISSION_DENIED); + assertThat(sessionResult.resultCode).isEqualTo(SessionResult.RESULT_SUCCESS); } @Test 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 a4ce28f7a9..30a2291545 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 @@ -236,7 +236,7 @@ public void onSessionReady() { } @Test - public void playerError_notified() throws Exception { + public void playerError_errorEmittedAndResolved_correctPlaybackStatesReceived() throws Exception { Bundle extras = new Bundle(); extras.putString("key-1", "value-1"); PlaybackException testPlayerError = @@ -245,10 +245,9 @@ public void playerError_notified() throws Exception { /* cause= */ null, PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED, extras); - CountDownLatch latch = new CountDownLatch(1); AtomicReference playbackStateCompatRef = new AtomicReference<>(); - MediaControllerCompat.Callback callback = + MediaControllerCompat.Callback errorCallback = new MediaControllerCompat.Callback() { @Override public void onPlaybackStateChanged(PlaybackStateCompat state) { @@ -256,16 +255,39 @@ public void onPlaybackStateChanged(PlaybackStateCompat state) { latch.countDown(); } }; - controllerCompat.registerCallback(callback, handler); + controllerCompat.registerCallback(errorCallback, handler); session.getMockPlayer().notifyPlayerError(testPlayerError); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - PlaybackStateCompat state = playbackStateCompatRef.get(); - assertThat(state.getState()).isEqualTo(PlaybackStateCompat.STATE_ERROR); - assertThat(state.getErrorCode()) + PlaybackStateCompat errorState = playbackStateCompatRef.get(); + assertThat(errorState.getState()).isEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat(errorState.getErrorCode()) .isEqualTo(PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED); - assertThat(state.getErrorMessage().toString()).isEqualTo(testPlayerError.getMessage()); - assertThat(state.getExtras().getString("key-1")).isEqualTo("value-1"); + assertThat(errorState.getErrorMessage().toString()).isEqualTo(testPlayerError.getMessage()); + assertThat(errorState.getExtras().getString("key-1")).isEqualTo("value-1"); + // Resolve the exception and assert the playback state transition + controllerCompat.unregisterCallback(errorCallback); + CountDownLatch bufferingLatch = new CountDownLatch(1); + AtomicReference bufferingPlaybackStateCompatRef = new AtomicReference<>(); + MediaControllerCompat.Callback bufferingCallback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + bufferingPlaybackStateCompatRef.set(state); + bufferingLatch.countDown(); + } + }; + controllerCompat.registerCallback(bufferingCallback, handler); + + session.getMockPlayer().notifyPlaybackStateChanged(Player.STATE_BUFFERING); + + assertThat(bufferingLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + PlaybackStateCompat bufferingState = bufferingPlaybackStateCompatRef.get(); + assertThat(bufferingState.getState()).isEqualTo(PlaybackStateCompat.STATE_PAUSED); + assertThat(bufferingState.getErrorCode()).isEqualTo(0); + assertThat(bufferingState.getErrorMessage()).isNull(); + assertThat(bufferingState.getExtras().getString("key-1")).isNull(); } @SuppressWarnings("deprecation") // Using PlaybackStateCompat 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 4d736823df..d02e2b9de3 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 @@ -2030,6 +2030,63 @@ public void onPlaybackStateChanged(PlaybackStateCompat state) { assertThat(actions2 & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS).isNotEqualTo(0); } + @Test + public void + playerWithMediaButtonPreferences_commandButtonCustomIconUri_playbackStateChangedWithIconUriInExtras() + throws Exception { + Player player = createDefaultPlayer(); + SessionCommand command = new SessionCommand("command1", Bundle.EMPTY); + ImmutableList mediaButtonPreferences = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_PLAY) + .setDisplayName("button") + .setSessionCommand(command) + .setIconUri(Uri.parse("content://my_icon")) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build()); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + return new ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands( + ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(command).build()) + .build(); + } + }; + MediaSession mediaSession = createMediaSession(player, callback); + connectMediaNotificationController(mediaSession); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + AtomicReference> reportedCustomActions = + new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + controllerCompat.registerCallback( + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + reportedCustomActions.set(state.getCustomActions()); + latch.countDown(); + } + }, + threadTestRule.getHandler()); + + getInstrumentation() + .runOnMainSync(() -> mediaSession.setMediaButtonPreferences(mediaButtonPreferences)); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(reportedCustomActions.get()).hasSize(1); + assertThat( + reportedCustomActions + .get() + .get(0) + .getExtras() + .getString(MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_URI_COMPAT)) + .isEqualTo("content://my_icon"); + mediaSession.release(); + releasePlayer(player); + } + /** * Connect a controller that mimics the media notification controller that is connected by {@link * MediaNotificationManager} when the session is running in the service. diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java index 2eefbf10d6..34cd9cd971 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java @@ -24,6 +24,7 @@ import android.content.Context; import android.media.AudioManager; +import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.support.v4.media.session.MediaControllerCompat; @@ -574,6 +575,7 @@ public void getMediaButtonPreferences() throws Exception { .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) .setEnabled(true) .setSlots(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW) + .setIconUri(Uri.parse("content://my_icon")) .build(); ConditionVariable onMediaButtonPreferencesChangedCalled = new ConditionVariable(); List> onMediaButtonPreferencesChangedArguments = new ArrayList<>(); @@ -600,6 +602,8 @@ public void onMediaButtonPreferencesChanged( extras2.putString("key", "value-2"); extras2.putInt( MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, CommandButton.ICON_FAST_FORWARD); + extras2.putString( + MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_URI_COMPAT, "content://my_icon"); PlaybackStateCompat.CustomAction customAction2 = new PlaybackStateCompat.CustomAction.Builder( "command2", "button2", /* icon= */ R.drawable.media3_icon_fast_forward) 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 972f08330a..702e9ad30a 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 @@ -120,26 +120,34 @@ public void setPlayWhenReady() throws Exception { @Override public void onPlayWhenReadyChanged( boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { - playWhenReadyFromCallbackRef.set(playWhenReady); - latch.countDown(); + if (latch.getCount() > 0) { + playWhenReadyFromCallbackRef.set(playWhenReady); + latch.countDown(); + } } @Override public void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason) { - playbackSuppressionReasonFromCallbackRef.set(playbackSuppressionReason); - latch.countDown(); + if (latch.getCount() > 0) { + playbackSuppressionReasonFromCallbackRef.set(playbackSuppressionReason); + latch.countDown(); + } } @Override public void onIsPlayingChanged(boolean isPlaying) { - isPlayingFromCallbackRef.set(isPlaying); - latch.countDown(); + if (latch.getCount() > 0) { + isPlayingFromCallbackRef.set(isPlaying); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -247,11 +255,13 @@ private Player.Listener getPlayerListenerToCapturePlaybackSuppression( return new Player.Listener() { @Override public void onEvents(Player player, Player.Events events) { - eventsRef.set(events); - if (events.contains(Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)) { - playbackSuppressionReasonChangedRef.set(player.getPlaybackSuppressionReason()); + if (countDownLatchForOnEventCalls.getCount() > 0) { + eventsRef.set(events); + if (events.contains(Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)) { + playbackSuppressionReasonChangedRef.set(player.getPlaybackSuppressionReason()); + } + countDownLatchForOnEventCalls.countDown(); } - countDownLatchForOnEventCalls.countDown(); } }; } @@ -283,14 +293,18 @@ public void setShuffleModeEnabled() throws Exception { new Player.Listener() { @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - shuffleModeEnabledFromCallbackRef.set(shuffleModeEnabled); - latch.countDown(); + if (latch.getCount() > 0) { + shuffleModeEnabledFromCallbackRef.set(shuffleModeEnabled); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -328,14 +342,18 @@ public void setRepeatMode() throws Exception { new Player.Listener() { @Override public void onRepeatModeChanged(int repeatMode) { - repeatModeFromCallbackRef.set(repeatMode); - latch.countDown(); + if (latch.getCount() > 0) { + repeatModeFromCallbackRef.set(repeatMode); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -373,14 +391,18 @@ public void setPlaybackParameters() throws Exception { new Player.Listener() { @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - playbackParametersFromCallbackRef.set(playbackParameters); - latch.countDown(); + if (latch.getCount() > 0) { + playbackParametersFromCallbackRef.set(playbackParameters); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -418,14 +440,18 @@ public void setPlaybackSpeed() throws Exception { new Player.Listener() { @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - playbackParametersFromCallbackRef.set(playbackParameters); - latch.countDown(); + if (latch.getCount() > 0) { + playbackParametersFromCallbackRef.set(playbackParameters); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -463,14 +489,18 @@ public void setPlaylistMetadata() throws Exception { new Player.Listener() { @Override public void onPlaylistMetadataChanged(MediaMetadata mediaMetadata) { - playlistMetadataFromCallbackRef.set(mediaMetadata); - latch.countDown(); + if (latch.getCount() > 0) { + playlistMetadataFromCallbackRef.set(mediaMetadata); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -505,14 +535,18 @@ public void setVolume() throws Exception { new Player.Listener() { @Override public void onVolumeChanged(float volume) { - volumeFromCallbackRef.set(volume); - latch.countDown(); + if (latch.getCount() > 0) { + volumeFromCallbackRef.set(volume); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -548,14 +582,18 @@ public void setDeviceVolume() throws Exception { new Player.Listener() { @Override public void onDeviceVolumeChanged(int volume, boolean muted) { - deviceVolumeFromCallbackRef.set(volume); - latch.countDown(); + if (latch.getCount() > 0) { + deviceVolumeFromCallbackRef.set(volume); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -596,14 +634,18 @@ public void increaseDeviceVolume() throws Exception { new Player.Listener() { @Override public void onDeviceVolumeChanged(int volume, boolean muted) { - deviceVolumeFromCallbackRef.set(volume); - latch.countDown(); + if (latch.getCount() > 0) { + deviceVolumeFromCallbackRef.set(volume); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -639,14 +681,18 @@ public void decreaseDeviceVolume() throws Exception { new Player.Listener() { @Override public void onDeviceVolumeChanged(int volume, boolean muted) { - deviceVolumeFromCallbackRef.set(volume); - latch.countDown(); + if (latch.getCount() > 0) { + deviceVolumeFromCallbackRef.set(volume); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -684,14 +730,18 @@ public void setDeviceMuted() throws Exception { new Player.Listener() { @Override public void onDeviceVolumeChanged(int volume, boolean muted) { - deviceMutedFromCallbackRef.set(muted); - latch.countDown(); + if (latch.getCount() > 0) { + deviceMutedFromCallbackRef.set(muted); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -728,14 +778,18 @@ public void setAudioAttributes() throws Exception { new Player.Listener() { @Override public void onAudioAttributesChanged(AudioAttributes audioAttributes) { - audioAttributesFromCallbackRef.set(originalAttrs); - latch.countDown(); + if (latch.getCount() > 0) { + audioAttributesFromCallbackRef.set(originalAttrs); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -782,20 +836,26 @@ public void prepare() throws Exception { new Player.Listener() { @Override public void onPlaybackStateChanged(int playbackState) { - playbackStateFromCallbackRef.set(playbackState); - latch.countDown(); + if (latch.getCount() > 0) { + playbackStateFromCallbackRef.set(playbackState); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } @Override public void onPlayerErrorChanged(@Nullable PlaybackException error) { - playerErrorFromCallbackRef.set(error); - latch.countDown(); + if (latch.getCount() > 0) { + playerErrorFromCallbackRef.set(error); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -834,14 +894,18 @@ public void setTrackSelectionParameters() throws Exception { new Player.Listener() { @Override public void onTrackSelectionParametersChanged(TrackSelectionParameters parameters) { - trackSelectionParametersCallbackRef.set(parameters); - latch.countDown(); + if (latch.getCount() > 0) { + trackSelectionParametersCallbackRef.set(parameters); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -893,22 +957,28 @@ public void seekToNextMediaItem() throws Exception { new Player.Listener() { @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - newMediaItemRef.set(mediaItem); - latch.countDown(); + if (latch.getCount() > 0) { + newMediaItemRef.set(mediaItem); + latch.countDown(); + } } @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - oldPositionInfoRef.set(oldPosition); - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + oldPositionInfoRef.set(oldPosition); + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -959,22 +1029,28 @@ public void seekToPreviousMediaItem() throws Exception { new Player.Listener() { @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - newMediaItemRef.set(mediaItem); - latch.countDown(); + if (latch.getCount() > 0) { + newMediaItemRef.set(mediaItem); + latch.countDown(); + } } @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - oldPositionInfoRef.set(oldPosition); - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + oldPositionInfoRef.set(oldPosition); + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -1030,14 +1106,18 @@ public void seekTo_forwardsInSamePeriod() throws Exception { @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -1100,14 +1180,18 @@ public void seekTo_forwardsInSamePeriod_beyondBufferedData() throws Exception { @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -1170,14 +1254,18 @@ public void seekTo_backwardsInSamePeriod() throws Exception { @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -1241,14 +1329,18 @@ public void seekTo_toDifferentPeriodInSameWindow() throws Exception { @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -1348,14 +1440,18 @@ public void seekTo_toDifferentPeriodInDifferentWindow() throws Exception { @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -1484,14 +1580,18 @@ public void seekTo_withEmptyTimeline() throws Exception { @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -1568,15 +1668,19 @@ public void seekBack_seeksToOffsetBySeekBackIncrement() throws Exception { @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - oldPositionRef.set(oldPosition.positionMs); - newPositionRef.set(newPosition.positionMs); - latch.countDown(); + if (latch.getCount() > 0) { + oldPositionRef.set(oldPosition.positionMs); + newPositionRef.set(newPosition.positionMs); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; controller.addListener(listener); @@ -1612,15 +1716,19 @@ public void seekForward_seeksToOffsetBySeekForwardIncrement() throws Exception { @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - oldPositionRef.set(oldPosition.positionMs); - newPositionRef.set(newPosition.positionMs); - latch.countDown(); + if (latch.getCount() > 0) { + oldPositionRef.set(oldPosition.positionMs); + newPositionRef.set(newPosition.positionMs); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; controller.addListener(listener); @@ -1669,20 +1777,26 @@ public void setMediaItems_withResetPosition() throws Exception { @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -1796,26 +1910,34 @@ public void setMediaItems_withStartMediaItemIndexAndStartPosition() throws Excep @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - newMediaItemRef.set(mediaItem); - latch.countDown(); + if (latch.getCount() > 0) { + newMediaItemRef.set(mediaItem); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -1897,26 +2019,34 @@ public void setMediaItems_withEmptyList() throws Exception { @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - newMediaItemRef.set(mediaItem); - latch.countDown(); + if (latch.getCount() > 0) { + newMediaItemRef.set(mediaItem); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -2008,20 +2138,26 @@ public void addMediaItems_withIdleStateAndEmptyTimeline() throws Exception { new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - newMediaItemRef.set(mediaItem); - latch.countDown(); + if (latch.getCount() > 0) { + newMediaItemRef.set(mediaItem); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -2087,20 +2223,26 @@ public void addMediaItems_withEndedStateAndEmptyTimeline() throws Exception { new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - newMediaItemRef.set(mediaItem); - latch.countDown(); + if (latch.getCount() > 0) { + newMediaItemRef.set(mediaItem); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -2168,14 +2310,18 @@ public void addMediaItems_toEndOfTimeline() throws Exception { new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -2285,14 +2431,18 @@ private void assertAddMediaItems( new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -2362,27 +2512,35 @@ public void removeMediaItems_currentItemRemoved() throws Exception { new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - newMediaItemRef.set(mediaItem); - latch.countDown(); + if (latch.getCount() > 0) { + newMediaItemRef.set(mediaItem); + latch.countDown(); + } } @Override public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, int reason) { - newPositionInfoRef.set(newPosition); - latch.countDown(); + if (latch.getCount() > 0) { + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -2453,14 +2611,18 @@ public void removeMediaItems_currentItemNotRemoved() throws Exception { new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -2528,14 +2690,18 @@ public void removeMediaItems_removePreviousItemWithMultiplePeriods() throws Exce new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -2603,26 +2769,34 @@ public void removeMediaItems_removeAllItems() throws Exception { new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - newMediaItemRef.set(mediaItem); - latch.countDown(); + if (latch.getCount() > 0) { + newMediaItemRef.set(mediaItem); + latch.countDown(); + } } @Override public void onPlaybackStateChanged(int playbackState) { - newPlaybackStateRef.set(playbackState); - latch.countDown(); + if (latch.getCount() > 0) { + newPlaybackStateRef.set(playbackState); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -2691,14 +2865,18 @@ public void removeMediaItems_removedTailIncludesCurrentItem_callsOnPlaybackState new Player.Listener() { @Override public void onPlaybackStateChanged(int playbackState) { - newPlaybackStateRef.set(playbackState); - latch.countDown(); + if (latch.getCount() > 0) { + newPlaybackStateRef.set(playbackState); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -2944,14 +3122,18 @@ public void moveMediaItems_callsOnTimelineChanged() throws Exception { new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -3069,14 +3251,18 @@ private void assertMoveMediaItems_whenMovingBetweenWindowsWithMultiplePeriods( new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -3189,8 +3375,6 @@ public void onEvents(Player player, Player.Events events) { } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); - - // Step 1: Report a discontinuity from item 0 to item 1 in the session. PositionInfo oldPositionInfo = new PositionInfo( /* windowUid= */ timeline.getWindow(/* windowIndex= */ 0, new Window()).uid, @@ -3217,22 +3401,126 @@ public void onEvents(Player player, Player.Events events) { /* contentPositionMs= */ 0, /* adGroupIndex= */ C.INDEX_UNSET, /* adIndexInAdGroup= */ C.INDEX_UNSET); - remoteSession.getMockPlayer().setCurrentMediaItemIndex(1); + + threadTestRule + .getHandler() + .postAndSync( + () -> { + // Step 1: Report a discontinuity from item 0 to item 1 in the session. And then give + // it some time to propagate so that it results in an update independent from step 2. + remoteSession.getMockPlayer().setCurrentMediaItemIndex(1); + remoteSession + .getMockPlayer() + .notifyPositionDiscontinuity( + oldPositionInfo, + newPositionInfo, + Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + Thread.sleep(100); + // Step 2: Before step 1 can be handled by the controller, remove item 1 and trigger + // player updates for the item removal. + remoteSession.getMockPlayer().setCurrentMediaItemIndex(0); + remoteSession + .getMockPlayer() + .setTimeline(MediaTestUtils.createTimeline(/* windowCount= */ 2)); + remoteSession.getMockPlayer().notifyPlaybackStateChanged(Player.STATE_ENDED); + remoteSession + .getMockPlayer() + .notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + controller.removeMediaItem(/* index= */ 1); + }); + + assertThat(positionDiscontinuityReported.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(reportedStateChangeToEndedAtSameTimeAsDiscontinuity.get()).isTrue(); + } + + @Test + public void timelineUpdatesDuringMasking_withNoPlayerInfoUpdateFromSession_areResolvedCorrectly() + throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 5); + remoteSession.getMockPlayer().setTimeline(timeline); + remoteSession.getMockPlayer().setCurrentMediaItemIndex(4); remoteSession .getMockPlayer() - .notifyPositionDiscontinuity( - oldPositionInfo, newPositionInfo, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); - // Step 2: Before step 1 can be handled by the controller, remove item 1. - threadTestRule.getHandler().postAndSync(() -> controller.removeMediaItem(/* index= */ 1)); - remoteSession.getMockPlayer().setCurrentMediaItemIndex(0); - remoteSession.getMockPlayer().setTimeline(MediaTestUtils.createTimeline(/* windowCount= */ 1)); - remoteSession.getMockPlayer().notifyPlaybackStateChanged(Player.STATE_ENDED); + .notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch timelineChangeReported = new CountDownLatch(2); + ArrayList reportedTimelineChanges = new ArrayList<>(); + ArrayList reportedCurrentIndex = new ArrayList<>(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + if (events.contains(Player.EVENT_TIMELINE_CHANGED)) { + reportedTimelineChanges.add(player.getCurrentTimeline()); + reportedCurrentIndex.add(player.getCurrentMediaItemIndex()); + timelineChangeReported.countDown(); + } + } + }; + threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); + + // Change masked Timeline in controller by assigning new single item, but don't change Timeline + // of the remote session. This means the timeline change needs to be reversed once the update is + // handled even if the remote session doesn't send any further update. + threadTestRule + .getHandler() + .postAndSync( + () -> + controller.setMediaItem( + new MediaItem.Builder().setMediaId("placeholder_id").build())); + + assertThat(timelineChangeReported.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(reportedTimelineChanges.get(0).getWindowCount()).isEqualTo(1); + assertThat(reportedTimelineChanges.get(1).getWindowCount()).isEqualTo(5); + assertThat(reportedCurrentIndex.get(0)).isEqualTo(0); + assertThat(reportedCurrentIndex.get(1)).isEqualTo(4); + } + + @Test + public void + timelineUpdatesDuringMasking_withPlayerInfoUpdateExcludingTimeline_areResolvedCorrectly() + throws Exception { + Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 5); + remoteSession.getMockPlayer().setPlaybackState(Player.STATE_READY); + remoteSession.getMockPlayer().setTimeline(timeline); + remoteSession.getMockPlayer().setCurrentMediaItemIndex(4); remoteSession .getMockPlayer() .notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch timelineChangeReported = new CountDownLatch(2); + ArrayList reportedTimelineChanges = new ArrayList<>(); + ArrayList reportedCurrentIndex = new ArrayList<>(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + if (events.contains(Player.EVENT_TIMELINE_CHANGED)) { + reportedTimelineChanges.add(player.getCurrentTimeline()); + reportedCurrentIndex.add(player.getCurrentMediaItemIndex()); + timelineChangeReported.countDown(); + } + } + }; + threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); - assertThat(positionDiscontinuityReported.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(reportedStateChangeToEndedAtSameTimeAsDiscontinuity.get()).isTrue(); + // Change masked Timeline in controller by assigning new single item, but don't change Timeline + // of the remote session. This means the timeline change needs to be reversed once the update is + // handled even if the remote session doesn't send any further Timeline updates. + threadTestRule + .getHandler() + .postAndSync( + () -> + controller.setMediaItem( + new MediaItem.Builder().setMediaId("placeholder_id").build())); + // Update session with new PlayerInfo, without changing Timeline. + remoteSession.getMockPlayer().notifyPlaybackStateChanged(Player.STATE_BUFFERING); + + assertThat(timelineChangeReported.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(reportedTimelineChanges.get(0).getWindowCount()).isEqualTo(1); + assertThat(reportedTimelineChanges.get(1).getWindowCount()).isEqualTo(5); + assertThat(reportedCurrentIndex.get(0)).isEqualTo(0); + assertThat(reportedCurrentIndex.get(1)).isEqualTo(4); } @Test @@ -3407,14 +3695,18 @@ public void replaceMediaItems_notReplacingCurrentItem_correctMasking() throws Ex new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -3445,7 +3737,7 @@ public void replaceMediaItems_replacingCurrentItem_correctMasking() throws Excep .build(); remoteSession.setPlayer(playerConfig); MediaController controller = controllerTestRule.createController(remoteSession.getToken()); - CountDownLatch latch = new CountDownLatch(2); + CountDownLatch latch = new CountDownLatch(3); AtomicReference newTimelineRef = new AtomicReference<>(); AtomicReference onEventsRef = new AtomicReference<>(); AtomicReference newPositionInfoRef = new AtomicReference<>(); @@ -3453,14 +3745,18 @@ public void replaceMediaItems_replacingCurrentItem_correctMasking() throws Excep new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } @Override @@ -3468,7 +3764,10 @@ public void onPositionDiscontinuity( PositionInfo oldPosition, PositionInfo newPosition, @Player.DiscontinuityReason int reason) { - newPositionInfoRef.set(newPosition); + if (latch.getCount() > 0) { + newPositionInfoRef.set(newPosition); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -3510,14 +3809,18 @@ public void replaceMediaItems_replacingCurrentItemWithEmptyListAndSubsequentItem new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -3561,14 +3864,18 @@ public void onEvents(Player player, Player.Events events) { new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -3614,14 +3921,18 @@ public void replaceMediaItems_fromPreparedEmpty_correctMasking() throws Exceptio new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -3698,14 +4009,18 @@ public void replaceMediaItems_withInvalidToIndex_correctMasking() throws Excepti new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - newTimelineRef.set(timeline); - latch.countDown(); + if (latch.getCount() > 0) { + newTimelineRef.set(timeline); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); @@ -3806,14 +4121,18 @@ public void stop() throws Exception { new Player.Listener() { @Override public void onPlaybackStateChanged(int playbackState) { - playbackStateFromCallbackRef.set(playbackState); - latch.countDown(); + if (latch.getCount() > 0) { + playbackStateFromCallbackRef.set(playbackState); + latch.countDown(); + } } @Override public void onEvents(Player player, Player.Events events) { - onEventsRef.set(events); - latch.countDown(); + if (latch.getCount() > 0) { + onEventsRef.set(events); + latch.countDown(); + } } }; threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); 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 2cbe5d7e16..462b513fc1 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,27 +20,21 @@ import static androidx.media3.common.MimeTypes.VIDEO_H265; import static com.google.common.truth.Truth.assertThat; -import android.content.Context; import android.os.Bundle; import android.os.Parcel; -import android.util.Pair; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; import androidx.media3.common.Tracks; -import androidx.media3.common.util.BitmapLoader; -import androidx.media3.datasource.DataSourceBitmapLoader; import androidx.media3.session.PlayerInfo.BundlingExclusions; -import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,15 +43,6 @@ @SmallTest public final class MediaUtilsTest { - private Context context; - private BitmapLoader bitmapLoader; - - @Before - public void setUp() { - context = ApplicationProvider.getApplicationContext(); - bitmapLoader = new CacheBitmapLoader(new DataSourceBitmapLoader(context)); - } - @Test public void truncateListBySize() { List bundleList = new ArrayList<>(); @@ -108,19 +93,16 @@ public void mergePlayerInfo_timelineAndTracksExcluded_correctMerge() { .add(Player.COMMAND_GET_TRACKS) .build(); - Pair mergeResult = + PlayerInfo mergeResult = MediaUtils.mergePlayerInfo( oldPlayerInfo, - BundlingExclusions.NONE, newPlayerInfo, new BundlingExclusions( /* isTimelineExcluded= */ true, /* areCurrentTracksExcluded= */ 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(); + assertThat(mergeResult.timeline).isSameInstanceAs(oldPlayerInfo.timeline); + assertThat(mergeResult.currentTracks).isSameInstanceAs(oldPlayerInfo.currentTracks); } @Test @@ -151,19 +133,16 @@ public void mergePlayerInfo_getTimelineCommandNotAvailable_emptyTimeline() { Player.Commands availableCommands = Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_GET_TRACKS).build(); - Pair mergeResult = + PlayerInfo mergeResult = MediaUtils.mergePlayerInfo( oldPlayerInfo, - BundlingExclusions.NONE, newPlayerInfo, new BundlingExclusions( /* isTimelineExcluded= */ true, /* areCurrentTracksExcluded= */ 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(); + assertThat(mergeResult.timeline).isSameInstanceAs(Timeline.EMPTY); + assertThat(mergeResult.currentTracks).isSameInstanceAs(oldPlayerInfo.currentTracks); } @Test @@ -194,18 +173,15 @@ public void mergePlayerInfo_getTracksCommandNotAvailable_emptyTracks() { Player.Commands availableCommands = Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_GET_TIMELINE).build(); - Pair mergeResult = + PlayerInfo mergeResult = MediaUtils.mergePlayerInfo( oldPlayerInfo, - BundlingExclusions.NONE, newPlayerInfo, new BundlingExclusions( /* isTimelineExcluded= */ true, /* areCurrentTracksExcluded= */ 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(); + assertThat(mergeResult.timeline).isSameInstanceAs(oldPlayerInfo.timeline); + assertThat(mergeResult.currentTracks).isSameInstanceAs(Tracks.EMPTY); } } 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 57154c9f6b..ae6babb6c0 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 @@ -587,7 +587,7 @@ private void assertLibraryParams(@Nullable LibraryParams params) { private List getPaginatedResult(List items, int page, int pageSize) { if (items == null) { return null; - } else if (items.size() == 0) { + } else if (items.isEmpty()) { return new ArrayList<>(); } 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 025526880c..ab3fb04b7e 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 @@ -631,6 +631,9 @@ public void notifyPlaybackStateChanged(@State int playbackState) { return; } boolean wasPlaying = isPlaying(); + if (playbackState != STATE_IDLE) { + this.playerError = null; + } this.playbackState = playbackState; boolean isPlaying = isPlaying(); for (Listener listener : listeners) { diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java index cf3395c7c4..c678b91b77 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java @@ -414,7 +414,7 @@ public long seekToUs(long positionUs) { boolean seekedInsideStreams = true; for (FakeSampleStream sampleStream : sampleStreams) { seekedInsideStreams &= - sampleStream.seekToUs(seekPositionUs, /* allowTimeBeyondBuffer= */ false); + sampleStream.seekToUs(seekPositionUs, /* allowTimeBeyondBuffer= */ isLoadingFinished()); } if (!seekedInsideStreams) { for (FakeSampleStream sampleStream : sampleStreams) { diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/OggFileAudioBufferSink.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/OggFileAudioBufferSink.java index 83b417eb77..d96496fdec 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/OggFileAudioBufferSink.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/OggFileAudioBufferSink.java @@ -132,7 +132,7 @@ private void writeOggPacketHeader(int pageSequenceNumber, boolean isIdHeaderPack scratchByteBuffer.put(isIdHeaderPacket ? (byte) 0x02 : (byte) 0x00); // granule_position - scratchByteBuffer.putLong((long) 0); + scratchByteBuffer.putLong(0L); // bitstream_serial_number scratchByteBuffer.putInt(0); diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java index 89d3bd58c1..6b22d5d95c 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java @@ -176,6 +176,11 @@ public void setShuffleOrder(ShuffleOrder shuffleOrder) { throw new UnsupportedOperationException(); } + @Override + public ShuffleOrder getShuffleOrder() { + throw new UnsupportedOperationException(); + } + @Override public void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) { throw new UnsupportedOperationException(); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrCapabilitiesUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrCapabilitiesUtil.java index fbd258af57..082f5bd82c 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrCapabilitiesUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrCapabilitiesUtil.java @@ -107,9 +107,6 @@ public static void assumeDeviceDoesNotSupportHdrEditing(String testId, Format fo public static void assumeDeviceSupportsHdrColorTransfer(String testId, Format format) throws JSONException, IOException, GlException { checkStateNotNull(format.colorInfo); - // Required to ensure EGL extensions are initialised. - @SuppressWarnings("unused") - EGLDisplay eglDisplay = GlUtil.getDefaultEglDisplay(); if (!GlUtil.isColorTransferSupported(format.colorInfo.colorTransfer)) { String skipReason = "HDR display not supported for sampleMimeType " diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java index 32f56358ce..6416d49ff3 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java @@ -186,12 +186,9 @@ private TestConfig( } public String getTestId() { - StringBuilder testIdBuilder = new StringBuilder(); - testIdBuilder.append( - String.format( - "analyzePerformance_%s_Fallback_%d_OpRate_%d_Priority_%d_Profile_%d_Level_%d", - getFilename(), enableFallback ? 1 : 0, operatingRate, priority, profile, level)); - return testIdBuilder.toString(); + return String.format( + "analyzePerformance_%s_Fallback_%d_OpRate_%d_Priority_%d_Profile_%d_Level_%d", + getFilename(), enableFallback ? 1 : 0, operatingRate, priority, profile, level); } public Map getInputValues() { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java index 983103832a..05b3b1a209 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java @@ -652,8 +652,7 @@ protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutpu protected ListenableFuture handleSetVideoOutput(Object videoOutput) { if (!(videoOutput instanceof SurfaceHolder || videoOutput instanceof SurfaceView)) { throw new UnsupportedOperationException( - videoOutput.getClass().toString() - + ". Use CompositionPlayer.setVideoSurface() for Surface output."); + videoOutput.getClass() + ". Use CompositionPlayer.setVideoSurface() for Surface output."); } this.videoOutput = videoOutput; return maybeSetVideoOutput(); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java index 9d3f9f426e..be5b4e4bce 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExoPlayerAssetLoader.java @@ -353,7 +353,7 @@ public Renderer[] createRenderers( assetLoaderListener, logSessionId)); } - return renderers.toArray(new Renderer[renderers.size()]); + return renderers.toArray(new Renderer[0]); } } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java index ea07f710ae..6aea122b99 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/MediaItemExportTest.java @@ -1517,7 +1517,7 @@ public void transmux_audioWithEditList_api30_correctDuration() throws Exception } @Test - @Config(minSdk = Config.OLDEST_SDK, maxSdk = 29) + @Config(minSdk = 21, maxSdk = 29) // This test requires Android SDK < 30 with no MediaMuxer negative PTS support. public void transmux_audioWithEditList_api29_frameworkMuxerDoesNotThrow() throws Exception { // Do not use CapturingMuxer.Factory(), as this test checks for a workaround in diff --git a/libraries/ui/src/main/java/androidx/media3/ui/DefaultTrackNameProvider.java b/libraries/ui/src/main/java/androidx/media3/ui/DefaultTrackNameProvider.java index e05e39bdbc..ebfe7a8c5a 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/DefaultTrackNameProvider.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/DefaultTrackNameProvider.java @@ -56,7 +56,7 @@ public String getTrackName(Format format) { } else { trackName = buildLanguageOrLabelString(format); } - if (trackName.length() != 0) { + if (!trackName.isEmpty()) { return trackName; } @Nullable String language = format.language; @@ -153,7 +153,7 @@ private String buildRoleString(Format format) { private String joinWithSeparator(String... items) { String itemList = ""; for (String item : items) { - if (item.length() > 0) { + if (!item.isEmpty()) { if (TextUtils.isEmpty(itemList)) { itemList = item; } else { diff --git a/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionView.java b/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionView.java index 12fa828726..f46620ccdc 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionView.java @@ -318,7 +318,7 @@ private void updateViews() { private void updateViewStates() { disableView.setChecked(isDisabled); - defaultView.setChecked(!isDisabled && overrides.size() == 0); + defaultView.setChecked(!isDisabled && overrides.isEmpty()); for (int i = 0; i < trackViews.length; i++) { @Nullable TrackSelectionOverride override = overrides.get(trackGroups.get(i).getMediaTrackGroup()); @@ -365,7 +365,7 @@ private void onTrackViewClicked(View view) { @Nullable TrackSelectionOverride override = overrides.get(mediaTrackGroup); if (override == null) { // Start new override. - if (!allowMultipleOverrides && overrides.size() > 0) { + if (!allowMultipleOverrides && !overrides.isEmpty()) { // Removed other overrides if we don't allow multiple overrides. overrides.clear(); } diff --git a/libraries/ui/src/main/java/androidx/media3/ui/WebViewSubtitleOutput.java b/libraries/ui/src/main/java/androidx/media3/ui/WebViewSubtitleOutput.java index b2c954fa80..3e0fb0d2d2 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/WebViewSubtitleOutput.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/WebViewSubtitleOutput.java @@ -326,7 +326,7 @@ private void updateWebView() { htmlHead.append(cssSelector).append("{").append(cssRuleSets.get(cssSelector)).append("}"); } htmlHead.append(""); - html.insert(0, htmlHead.toString()); + html.insert(0, htmlHead); webView.loadData( Base64.encodeToString(html.toString().getBytes(StandardCharsets.UTF_8), Base64.NO_PADDING), diff --git a/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/PlayerSurface.kt b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/PlayerSurface.kt index a60215d955..b58769475b 100644 --- a/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/PlayerSurface.kt +++ b/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/PlayerSurface.kt @@ -84,21 +84,27 @@ private fun PlayerSurfaceInternal( clearVideoView: Player.(T) -> Unit, ) { var view by remember { mutableStateOf(null) } - var registeredPlayer by remember { mutableStateOf(null) } - AndroidView(factory = { createView(it).apply { view = this } }, onReset = {}, modifier = modifier) + AndroidView( + modifier = modifier, + factory = { createView(it) }, + onReset = {}, + update = { view = it }, + ) view?.let { view -> LaunchedEffect(view, player) { if (player != null) { - registeredPlayer?.let { previousPlayer -> - if (previousPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) + view.attachedPlayer?.let { previousPlayer -> + if ( + previousPlayer != player && + previousPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE) + ) previousPlayer.clearVideoView(view) - registeredPlayer = null } if (player.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) { player.setVideoView(view) - registeredPlayer = player + view.attachedPlayer = player } } else { // Now that our player got null'd, we are not in a rush to get the old view from the @@ -106,10 +112,10 @@ private fun PlayerSurfaceInternal( // since that player might have a new view attached to it in the meantime. This will avoid // unnecessarily creating a Surface placeholder. withContext(Dispatchers.Main) { - registeredPlayer?.let { previousPlayer -> + view.attachedPlayer?.let { previousPlayer -> if (previousPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) previousPlayer.clearVideoView(view) - registeredPlayer = null + view.attachedPlayer = null } } } @@ -117,6 +123,12 @@ private fun PlayerSurfaceInternal( } } +private var View.attachedPlayer: Player? + get() = tag as? Player + set(player) { + tag = player + } + /** * The type of surface used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW] or * [SURFACE_TYPE_TEXTURE_VIEW]. diff --git a/libraries/ui_compose/src/test/java/androidx/media3/ui/compose/PlayerSurfaceTest.kt b/libraries/ui_compose/src/test/java/androidx/media3/ui/compose/PlayerSurfaceTest.kt index dae15d8deb..6ba82aab72 100644 --- a/libraries/ui_compose/src/test/java/androidx/media3/ui/compose/PlayerSurfaceTest.kt +++ b/libraries/ui_compose/src/test/java/androidx/media3/ui/compose/PlayerSurfaceTest.kt @@ -15,21 +15,34 @@ */ package androidx.media3.ui.compose +import android.os.Looper import android.view.SurfaceView import android.view.TextureView +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.MutableIntState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performScrollToIndex +import androidx.compose.ui.unit.dp import androidx.media3.common.ForwardingPlayer import androidx.media3.common.Player +import androidx.media3.common.SimpleBasePlayer import androidx.media3.ui.compose.utils.TestPlayer import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.inOrder import org.mockito.Mockito.spy @@ -148,76 +161,459 @@ class PlayerSurfaceTest { } @Test - fun twoPlayerSurfaces_exchangePlayers_onlyOnePlayerClearsTheSurfaceNotBoth() { - val player0 = TestPlayer() - val player1 = TestPlayer() - val spyPlayer0 = spy(ForwardingPlayer(player0)) - val spyPlayer1 = spy(ForwardingPlayer(player1)) - val argCaptor = ArgumentCaptor.forClass(SurfaceView::class.java) + fun twoPlayerSurfaces_exchangePlayers_neverAssignsSurfaceSimultaneouslyAndAvoidsUnnecessaryRemoval() { + val tracker = PlayerSurfaceTracker() + val player0 = tracker.createPlayer() + val player1 = tracker.createPlayer() + lateinit var playerIndex: MutableIntState + composeTestRule.setContent { + playerIndex = remember { mutableIntStateOf(0) } + PlayerSurface(player = if (playerIndex.intValue == 0) player0 else player1) + PlayerSurface(player = if (playerIndex.intValue == 0) player1 else player0) + } + composeTestRule.waitForIdle() + playerIndex.intValue = 1 + composeTestRule.waitForIdle() + + // Verify every player received both surfaces + assertThat(tracker.getAssignedSurfaceCount(player0)).isEqualTo(2) + assertThat(tracker.getAssignedSurfaceCount(player1)).isEqualTo(2) + // Assert correctness and efficiency + assertThat(tracker.isAnySurfaceUsedByTwoPlayersSimultaneously()).isFalse() + assertThat(tracker.isAnySurfaceRemovedWithoutNeedingItForAnotherPlayer()).isFalse() + } + + @Test + fun twoPlayerSurfaces_passPlayerFromOneToAnotherAndBack_avoidsIntermediateUnnecessaryRemoval() { + val tracker = PlayerSurfaceTracker() + val player = tracker.createPlayer() lateinit var playerIndex: MutableIntState composeTestRule.setContent { playerIndex = remember { mutableIntStateOf(0) } - PlayerSurface(player = if (playerIndex.intValue == 0) spyPlayer0 else spyPlayer1) - PlayerSurface(player = if (playerIndex.intValue == 0) spyPlayer1 else spyPlayer0) + PlayerSurface(player = if (playerIndex.intValue == 0) player else null) + PlayerSurface(player = if (playerIndex.intValue == 0) null else player) } + composeTestRule.waitForIdle() playerIndex.intValue = 1 composeTestRule.waitForIdle() + playerIndex.intValue = 0 + composeTestRule.waitForIdle() - assertThat(player0.videoOutput).isNotNull() - assertThat(player1.videoOutput).isNotNull() + // Verify the player received all surface updates + assertThat(tracker.getAssignedSurfaceCount(player)).isEqualTo(3) + // Check no unnecessary placeholder surfaces were needed + assertThat(tracker.isAnySurfaceRemovedWithoutNeedingItForAnotherPlayer()).isFalse() + } - val inOrder = inOrder(spyPlayer0, spyPlayer1) - // Original setup - inOrder.verify(spyPlayer0).setVideoSurfaceView(argCaptor.capture()) - val surfaceView0 = argCaptor.value - inOrder.verify(spyPlayer1).setVideoSurfaceView(argCaptor.capture()) - val surfaceView1 = argCaptor.value + @Test + fun playerSurface_inReusableContainerWithDifferentPlayers_neverAssignsSurfacesSimultaneouslyAndAvoidsUnnecessaryRemoval() { + val tracker = PlayerSurfaceTracker() + val players = + listOf( + tracker.createPlayer(), + tracker.createPlayer(), + tracker.createPlayer(), + tracker.createPlayer(), + ) + + composeTestRule.setContent { + LazyColumn(modifier = Modifier.testTag("lazyColumn")) { + items(count = 4) { index -> + // Use very large height to ensure only a single item is shown. + PlayerSurface( + modifier = Modifier.defaultMinSize(minHeight = 10000.dp), + player = players[index], + surfaceType = SURFACE_TYPE_SURFACE_VIEW, + ) + } + } + } + // Show every element twice to verify reuse within and across items + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(0) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) + composeTestRule.waitForIdle() - // Stages of exchanging players - inOrder.verify(spyPlayer0).clearVideoSurfaceView(surfaceView0) - inOrder.verify(spyPlayer1).setVideoSurfaceView(surfaceView0) - inOrder.verify(spyPlayer1).clearVideoSurfaceView(surfaceView1) // no-op, not the current surface - inOrder.verify(spyPlayer0).setVideoSurfaceView(surfaceView1) - inOrder.verifyNoMoreInteractions() + // Verify test setup actually re-uses surfaces + assertThat(tracker.getTotalSurfaceCount()).isLessThan(4) + // Verify every player received a surface twice (=each time it became visible) + assertThat(tracker.getAssignedSurfaceCount(players[0])).isEqualTo(2) + assertThat(tracker.getAssignedSurfaceCount(players[1])).isEqualTo(2) + assertThat(tracker.getAssignedSurfaceCount(players[2])).isEqualTo(2) + assertThat(tracker.getAssignedSurfaceCount(players[3])).isEqualTo(2) + // Assert correctness and efficiency + assertThat(tracker.isAnySurfaceUsedByTwoPlayersSimultaneously()).isFalse() + assertThat(tracker.isAnySurfaceRemovedWithoutNeedingItForAnotherPlayer()).isFalse() } @Test - fun twoPlayerSurfaces_passPlayerFromOneToAnotherAndBack_avoidsIntermediateUnnecessaryClearing() { - val player = TestPlayer() - val spyPlayer = spy(ForwardingPlayer(player)) - val argCaptor = ArgumentCaptor.forClass(SurfaceView::class.java) + fun playerSurface_inReusableContainerWithDifferentPlayersOnlyOneAssigned_neverAssignsSurfacesSimultaneouslyAndAvoidsUnnecessaryRemoval() { + val tracker = PlayerSurfaceTracker() + val players = + listOf( + tracker.createPlayer(), + tracker.createPlayer(), + tracker.createPlayer(), + tracker.createPlayer(), + ) - lateinit var playerIndex: MutableIntState composeTestRule.setContent { - playerIndex = remember { mutableIntStateOf(0) } - PlayerSurface(player = if (playerIndex.intValue == 0) spyPlayer else null) - PlayerSurface(player = if (playerIndex.intValue == 0) null else spyPlayer) + val listState = rememberLazyListState() + val currentVisibleIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } } + LazyColumn(state = listState, modifier = Modifier.testTag("lazyColumn")) { + items(count = 4) { index -> + // Use very large height to ensure only a single item is shown. + PlayerSurface( + modifier = Modifier.defaultMinSize(minHeight = 10000.dp), + player = if (index == currentVisibleIndex) players[index] else null, + surfaceType = SURFACE_TYPE_SURFACE_VIEW, + ) + } + } } + // Show every element twice to verify reuse within and across items + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(0) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) composeTestRule.waitForIdle() - val inOrder = inOrder(spyPlayer) - inOrder.verify(spyPlayer).setVideoSurfaceView(argCaptor.capture()) - val originalSurface = argCaptor.value - playerIndex.intValue = 1 + // Verify test setup actually re-uses surfaces + assertThat(tracker.getTotalSurfaceCount()).isLessThan(4) + // Verify every player received a surface twice (=each time it became visible) + assertThat(tracker.getAssignedSurfaceCount(players[0])).isEqualTo(2) + assertThat(tracker.getAssignedSurfaceCount(players[1])).isEqualTo(2) + assertThat(tracker.getAssignedSurfaceCount(players[2])).isEqualTo(2) + assertThat(tracker.getAssignedSurfaceCount(players[3])).isEqualTo(2) + // Assert correctness and efficiency + assertThat(tracker.isAnySurfaceUsedByTwoPlayersSimultaneously()).isFalse() + assertThat(tracker.isAnySurfaceRemovedWithoutNeedingItForAnotherPlayer()).isFalse() + } + + @Test + fun playerSurface_inReusableContainerWithSamePlayer_updatesSurfacesOnPlayerWithoutRemoval() { + val tracker = PlayerSurfaceTracker() + val player = tracker.createPlayer() + + composeTestRule.setContent { + LazyColumn(modifier = Modifier.testTag("lazyColumn")) { + items(count = 4) { index -> + // Use very large height to ensure only a single item is shown. + PlayerSurface( + modifier = Modifier.defaultMinSize(minHeight = 10000.dp), + player = player, + surfaceType = SURFACE_TYPE_SURFACE_VIEW, + ) + } + } + } + // Show every element twice to verify reuse within and across items + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(0) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) + composeTestRule.waitForIdle() + + // Verify test setup actually re-uses surfaces + assertThat(tracker.getTotalSurfaceCount()).isLessThan(4) + // Verify the player the expected total number of surfaces + assertThat(tracker.getAssignedSurfaceCount(player)).isEqualTo(8) + // Assert the surface usage was efficient + assertThat(tracker.isAnySurfaceRemovedWithoutNeedingItForAnotherPlayer()).isFalse() + } + + @Test + fun playerSurface_inReusableContainerWithSamePlayerOnlyOneAssigned_updatesSurfacesOnPlayerWithoutRemoval() { + val tracker = PlayerSurfaceTracker() + val player = tracker.createPlayer() + + composeTestRule.setContent { + val listState = rememberLazyListState() + val currentVisibleIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } } + LazyColumn(state = listState, modifier = Modifier.testTag("lazyColumn")) { + items(count = 4) { index -> + // Use very large height to ensure only a single item is shown. + PlayerSurface( + modifier = Modifier.defaultMinSize(minHeight = 10000.dp), + player = if (index == currentVisibleIndex) player else null, + surfaceType = SURFACE_TYPE_SURFACE_VIEW, + ) + } + } + } + // Show every element twice to verify reuse within and across items + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(0) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) + composeTestRule.waitForIdle() + + // Verify test setup actually re-uses surfaces + assertThat(tracker.getTotalSurfaceCount()).isLessThan(4) + // Verify the player the expected total number of surfaces + assertThat(tracker.getAssignedSurfaceCount(player)).isEqualTo(8) + // Assert the surface usage was efficient + assertThat(tracker.isAnySurfaceRemovedWithoutNeedingItForAnotherPlayer()).isFalse() + } - assertThat(player.videoOutput).isNotNull() - inOrder.verify(spyPlayer).setVideoSurfaceView(argCaptor.capture()) - val newSurface1 = argCaptor.value - assertThat(originalSurface).isNotEqualTo(newSurface1) - inOrder.verify(spyPlayer).clearVideoSurfaceView(originalSurface) // no-op, wrong surface - inOrder.verifyNoMoreInteractions() + @Test + fun playerSurface_inReusableContainerWithTwoPlayers_neverAssignsSurfacesSimultaneouslyAndAvoidsUnnecessaryRemoval() { + // Using two players is meant to force a situation where the same re-used surface is assigned + // to the same player again. + val tracker = PlayerSurfaceTracker() + val players = listOf(tracker.createPlayer(), tracker.createPlayer()) - playerIndex.intValue = 0 + composeTestRule.setContent { + LazyColumn(modifier = Modifier.testTag("lazyColumn")) { + items(count = 4) { index -> + // Use very large height to ensure only a single item is shown. + PlayerSurface( + modifier = Modifier.defaultMinSize(minHeight = 10000.dp), + player = players[index % 2], + surfaceType = SURFACE_TYPE_SURFACE_VIEW, + ) + } + } + } + // Show every element twice to verify reuse within and across items + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(0) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) + composeTestRule.waitForIdle() + + // Verify test setup actually re-uses surfaces and the same surface has been used consecutively + // by the same player at least once + assertThat(tracker.getTotalSurfaceCount()).isLessThan(4) + assertThat(tracker.hasAnyPlayerTheSameSurfaceAssignedConsecutively()).isTrue() + // Verify every player received a surface twice (=each time it became visible) + assertThat(tracker.getAssignedSurfaceCount(players[0])).isEqualTo(4) + assertThat(tracker.getAssignedSurfaceCount(players[1])).isEqualTo(4) + // Assert correctness and efficiency + assertThat(tracker.isAnySurfaceUsedByTwoPlayersSimultaneously()).isFalse() + assertThat(tracker.isAnySurfaceRemovedWithoutNeedingItForAnotherPlayer()).isFalse() + } + + @Test + fun playerSurface_inReusableContainerWithTwoPlayersOnlyOneAssigned_neverAssignsSurfacesSimultaneouslyAndAvoidsUnnecessaryRemoval() { + // Using two players is meant to force a situation where the same re-used surface is assigned + // to the same player again. + val tracker = PlayerSurfaceTracker() + val players = listOf(tracker.createPlayer(), tracker.createPlayer()) + + composeTestRule.setContent { + val listState = rememberLazyListState() + val currentVisibleIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } } + LazyColumn(state = listState, modifier = Modifier.testTag("lazyColumn")) { + items(count = 4) { index -> + // Use very large height to ensure only a single item is shown. + PlayerSurface( + modifier = Modifier.defaultMinSize(minHeight = 10000.dp), + player = if (index == currentVisibleIndex) players[index % 2] else null, + surfaceType = SURFACE_TYPE_SURFACE_VIEW, + ) + } + } + } + // Show every element twice to verify reuse within and across items + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(0) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(1) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(2) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("lazyColumn").performScrollToIndex(3) + composeTestRule.waitForIdle() + + // Verify test setup actually re-uses surfaces and the same surface has been used consecutively + // by the same player at least once + assertThat(tracker.getTotalSurfaceCount()).isLessThan(4) + assertThat(tracker.hasAnyPlayerTheSameSurfaceAssignedConsecutively()).isTrue() + // Verify every player received a surface twice (=each time it became visible) + assertThat(tracker.getAssignedSurfaceCount(players[0])).isEqualTo(4) + assertThat(tracker.getAssignedSurfaceCount(players[1])).isEqualTo(4) + // Assert correctness and efficiency + assertThat(tracker.isAnySurfaceUsedByTwoPlayersSimultaneously()).isFalse() + assertThat(tracker.isAnySurfaceRemovedWithoutNeedingItForAnotherPlayer()).isFalse() + } + + private class PlayerSurfaceTracker { - assertThat(player.videoOutput).isNotNull() - inOrder.verify(spyPlayer).setVideoSurfaceView(argCaptor.capture()) - val newSurface0 = argCaptor.value - assertThat(originalSurface).isEqualTo(newSurface0) - inOrder.verify(spyPlayer).clearVideoSurfaceView(newSurface1) // no-op, wrong surface - inOrder.verifyNoMoreInteractions() + private enum class SurfaceChangeType { + ADD, + REPLACE, + REMOVE, + } + + private data class Interaction( + val player: Player, + val surface: SurfaceView, + val type: SurfaceChangeType, + ) + + private val interactions: MutableList = mutableListOf() + + fun createPlayer(): Player { + return object : SimpleBasePlayer(Looper.myLooper()!!) { + var currentSurface: SurfaceView? = null + + override fun getState(): State { + return State.Builder() + .setAvailableCommands(Player.Commands.Builder().add(COMMAND_SET_VIDEO_SURFACE).build()) + .build() + } + + override fun handleSetVideoOutput(videoOutput: Any): ListenableFuture<*> { + currentSurface?.let { + interactions.add(Interaction(player = this, surface = it, SurfaceChangeType.REPLACE)) + } + currentSurface = videoOutput as SurfaceView + interactions.add(Interaction(player = this, videoOutput, SurfaceChangeType.ADD)) + return Futures.immediateVoidFuture() + } + + override fun handleClearVideoOutput(videoOutput: Any?): ListenableFuture<*> { + currentSurface?.let { current -> + if (videoOutput == null || videoOutput == current) { + interactions.add(Interaction(player = this, current, SurfaceChangeType.REMOVE)) + currentSurface = null + } + } + return Futures.immediateVoidFuture() + } + } + } + + fun getTotalSurfaceCount(): Int { + return interactions.map { interaction -> interaction.surface }.distinct().count() + } + + fun getAssignedSurfaceCount(player: Player): Int { + return interactions.count { interaction -> + interaction.player == player && interaction.type == SurfaceChangeType.ADD + } + } + + fun hasAnyPlayerTheSameSurfaceAssignedConsecutively(): Boolean { + return interactions + .groupBy { interaction -> interaction.player } + .any { (_, surfaceInteractions) -> + surfaceInteractions.zipWithNext().any { (first, second) -> + (first.type == SurfaceChangeType.REPLACE || first.type == SurfaceChangeType.REMOVE) && + second.type == SurfaceChangeType.ADD && + first.surface == second.surface + } + } + } + + fun isAnySurfaceUsedByTwoPlayersSimultaneously(): Boolean { + // Check if any surface has two consecutive ADD interactions without REPLACE/REMOVE indicating + // it's been assigned to two players simultaneously. + return interactions + .groupBy { interaction -> interaction.surface } + .any { (_, surfaceInteractions) -> + surfaceInteractions.zipWithNext().any { (first, second) -> + first.type == SurfaceChangeType.ADD && second.type == SurfaceChangeType.ADD + } + } + } + + fun isAnySurfaceRemovedWithoutNeedingItForAnotherPlayer(): Boolean { + return interactions.withIndex().any { interaction -> + interaction.value.type == SurfaceChangeType.REMOVE && + !isSurfaceRemovalRequiredForAnotherPlayer( + removalInteraction = interaction.value, + remainingInteractions = interactions.drop(interaction.index + 1), + ) + } + } + + private fun isSurfaceRemovalRequiredForAnotherPlayer( + removalInteraction: Interaction, + remainingInteractions: List, + ): Boolean { + // Removing a surface is inefficient as it requires a placeholder surface. Check if this only + // happens if the surface needs to be re-assigned to another player before the previous player + // gets a new surface. + for (interaction in remainingInteractions) { + if ( + interaction.surface == removalInteraction.surface && + interaction.type == SurfaceChangeType.ADD && + interaction.player != removalInteraction.player + ) { + // Same surface assigned to new player, removal was required. + return true + } + if ( + interaction.type == SurfaceChangeType.ADD && + interaction.player == removalInteraction.player + ) { + // Previous player gets a new surface first, removal was unnecessary. + return false + } + } + // Removal wasn't needed in any of the remaining interactions + return false + } } } diff --git a/settings.gradle b/settings.gradle index f4803b7513..97e1f627c1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -64,4 +64,8 @@ project(modulePrefix + 'test-session-current').projectDir = new File(rootDir, 'l include modulePrefix + 'testapp-controller' project(modulePrefix + 'testapp-controller').projectDir = new File(rootDir, 'testapps/controller') +// Documentation samples. +include modulePrefix + 'doc-samples' +project(modulePrefix + 'doc-samples').projectDir = new File(rootDir, 'docsamples') + apply from: 'core_settings.gradle' diff --git a/testapps/controller/src/main/java/androidx/media3/testapp/controller/BrowseMediaItemsAdapter.kt b/testapps/controller/src/main/java/androidx/media3/testapp/controller/BrowseMediaItemsAdapter.kt index 496f8b7dec..ed3c893527 100644 --- a/testapps/controller/src/main/java/androidx/media3/testapp/controller/BrowseMediaItemsAdapter.kt +++ b/testapps/controller/src/main/java/androidx/media3/testapp/controller/BrowseMediaItemsAdapter.kt @@ -38,7 +38,7 @@ import java.util.Stack /** Helper class that enables navigation on tree in MediaBrowser. */ class BrowseMediaItemsAdapter( private val activity: Activity, - private val mediaBrowser: MediaBrowser + private val mediaBrowser: MediaBrowser, ) : RecyclerView.Adapter() { private var items: List = emptyList() // Stack that holds ancestors of current item. @@ -86,7 +86,7 @@ class BrowseMediaItemsAdapter( val result: LibraryResult = libraryResult.get() result.value?.let { setRoot(it.mediaId) } }, - ContextCompat.getMainExecutor(activity) + ContextCompat.getMainExecutor(activity), ) } } @@ -122,7 +122,7 @@ class BrowseMediaItemsAdapter( BitmapFactory.decodeByteArray( mediaMetadata.artworkData, 0, - mediaMetadata.artworkData!!.size + mediaMetadata.artworkData!!.size, ) holder.icon.setImageBitmap(bitmap) } @@ -148,7 +148,7 @@ class BrowseMediaItemsAdapter( override fun getItemCount(): Int { // Leave one item for message if nodes or items are empty. - if (nodes.size == 0 || items.isEmpty()) return 1 + if (nodes.isEmpty() || items.isEmpty()) return 1 return items.size } diff --git a/testapps/controller/src/main/java/androidx/media3/testapp/controller/MediaAppListAdapter.kt b/testapps/controller/src/main/java/androidx/media3/testapp/controller/MediaAppListAdapter.kt index 7a296529de..d7699726a5 100644 --- a/testapps/controller/src/main/java/androidx/media3/testapp/controller/MediaAppListAdapter.kt +++ b/testapps/controller/src/main/java/androidx/media3/testapp/controller/MediaAppListAdapter.kt @@ -207,7 +207,7 @@ class MediaAppListAdapter(val mediaAppSelectedListener: MediaAppSelectedListener } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val type: ViewType = ViewType.values()[viewType] + val type: ViewType = ViewType.entries[viewType] val itemLayout: View = LayoutInflater.from(parent.context).inflate(type.layoutId, parent, false) return type.create(itemLayout) }