diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index ed8a3df6137..c11e3e85cdf 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -19,6 +19,8 @@ body: options: - Media3 main branch - Media3 pre-release (alpha, beta or RC not in this list) + - Media3 1.10.1 + - Media3 1.10.0 - Media3 1.9.2 - Media3 1.9.1 - Media3 1.9.0 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 365b5c4f358..b9959cd4162 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,93 +1,73 @@ # Release notes -### Unreleased changes +## 1.10 + +### 1.10.1 (2026-05-12) + +This release includes the following changes since [1.10.0](#1100-2026-03-25): * Common library: - * Add `Format.primaryTrackGroupId` to identify the primary group of - embedded `Format` instances - ([#294](https://github.com/androidx/media/issues/294)). + * Fix handling of `onAudioSessionIdChanged` in `SimpleBasePlayer` and + `ForwardingSimpleBasePlayer`. * ExoPlayer: - * Fix issue where ExoPlayer would not request delayed audio focus when - playback is requested during phone calls. - * Fix bug where transitions from on-demand to live content may cause - re-buffers at the end of the on-demand content - ([#3052](https://github.com/androidx/media/issues/3052)). - * Add support for Dolby Vision Profile 10 - ([#2830](https://github.com/androidx/media/pull/2830)). -* CompositionPlayer: -* Transformer: + * Fix race condition related to audio session id generation that could + lead to an `IllegalStateException` with tunneling mode. + ([#3099](https://github.com/androidx/media/issues/3099)). + * Fix possible app crashes when recovering from decoder errors with + renderer prewarming. + * Fix issue where video artifacts were caused by supplying initialization + data when using an AV1-based Dolby Vision codec + ([#3153](https://github.com/androidx/media/pull/3153)). * Track selection: - * Change signature of `DefaultTrackSelector.selectAllTracks` to include - the output array as a parameter, pre-populated with track overrides. - * Fix issue where embedded metadata tracks are selected irrespective of - whether the corresponding primary track is selected - ([#294](https://github.com/androidx/media/issues/294)). - * Stabilize `TrackSelectionParameters` options for - `preferredVideoLanguages`, `preferredVideoLabels`, - `preferredAudioLabels`, `preferredTextLabels` and `selectTextByDefault`. + * Adjust track selection logic in `VideoTrackInfo` to resolve fallback + MIME types and move HDR and codec score preferences to quality + preferences ([#3135](https://github.com/androidx/media/issues/3135)). * Extractors: - * MP4: Support files with large `uuid` boxes before other boxes like - `moov` ([#3046](https://github.com/androidx/media/issues/3046)). - * MP4: Add support for Versatile Video Coding (VVC) tracks in MP4 - containers. -* Inspector: + * MP3: Ignore Xing data length if it is longer than the known stream + length ([#3117](https://github.com/androidx/media/issues/3117)). + * Fix `ArrayIndexOutOfBoundsException` in `Mp4Extractor` when + `FLAG_OMIT_TRACK_SAMPLE_TABLE` is set and the track lacks a sync sample + (`stss`) box. * Audio: - * Improve the retry logic of `AudioOutput` initialization in - `DefaultAudioSink` - ([#2905](https://github.com/androidx/media/issues/2905)). + * Fix bug where audio events may be misrouted if multiple audio renderers + are added to the player. * Video: -* Text: -* Metadata: - * Increase default count of metadata renderers to four to cover all - potential metadata tracks published by HLS variants and renditions - ([#3043](https://github.com/androidx/media/issues/3043)). + * Adjust logic for codec reuse at frame rate changes on API below 30 to + avoid codec resets where they are not beneficial + ([#3120](https://github.com/androidx/media/issues/3120)). + * Disable forced synchronization workaround in `queueSecureInputBuffer` + for API 31+. The workaround was an artificial bottleneck that forced + decryption to run serially which prevented garbled video due to a + framework issue existing prior to API 31. * Image: -* DataSource: -* DRM: -* Effect: -* Effect Lottie: -* Muxers: -* IMA extension: + * Fix issue in scrubbing mode where image updates would only take effect + when the user "stops scrubbing" + ([#2815](https://github.com/androidx/media/issues/2815)). * Session: - * Fix issue where system UI button placement workaround negatively affects - other UI surface like Android Auto or manufacturers not needing the - workaround ([#3041](https://github.com/androidx/media/issues/3041)). - * Fix issue where access to subscriptions was not synchronized - ([#3056](https://github.com/androidx/media/issues/3056)). -* UI: - * Add `Player` Composable to `media3-ui-compose-material3` which combines - a `ContentFrame` with customizable controls aligned to top, center, and - bottom. -* Downloads: -* OkHttp extension: -* Cronet extension: -* RTMP extension: + * Fix bug where `ForegroundServiceStartNotAllowedException` wasn't + propagated across thread boundaries in case the application thread is + not the main thread + ([#2499](https://github.com/androidx/media/issues/2499)). + * Fix bug where `COMMAND_SEEK_TO_MEDIA_ITEM` is not available in a + `MediaController` when connecting to a platform-only session that + supports seeking to other items but no playlist modifications. + * Fix issue where `MediaController` can't set media items on platform + media sessions that don't allow `PREPARE_FROM` actions. + * Fix bug where own process wasn't marked with `Controller.isTrusted()` on + API 27 or before when connecting via platform controllers. * HLS extension: - * Expose ID3 (EMSG) metadata track in audio renditions - ([#3043](https://github.com/androidx/media/issues/3043)). - * Allow location fallback upon encountering load errors, if redundant - streams from different locations are available - ([#1988](https://github.com/androidx/media/issues/1988)). + * Fix a bug where an `ArrayIndexOutOfBoundsException` is thrown during + stream fallback if the track selection is a subset of the available + tracks ([#3161](https://github.com/androidx/media/issues/3161)). + * Fix an issue where the initialization segment was not carried over + across media playlist updates if the `#EXT-X-MAP` tag was not repeated + in subsequent updates + ([#3105](https://github.com/androidx/media/issues/3105)). * DASH extension: - * Fix issue where tracks with unaligned segment start times cause delays - or missed samples at the beginning of playback - ([#3057](https://github.com/androidx/media/issues/3057)). -* Smooth Streaming extension: -* RTSP extension: -* Decoder extensions (FFmpeg, VP9, AV1, etc.): - * Add MPEG-H UI manager support to the MPEG-H decoder extension - ([#3066](https://github.com/androidx/media/pull/3066)). -* MIDI extension: -* Leanback extension: -* Cast extension: -* Test utilities: -* Remove deprecated symbols: - * Remove `androidx.media3.extractor.metadata.flac.VorbisComment`. Use - `androidx.media3.extractor.metadata.vorbis.VorbisComment` instead. + * Fix crash in `SampleQueue` when seeking into a chunk that is currently + being canceled. -## 1.10 - -### 1.10.0-alpha01 (2026-02-06) +### 1.10.0 (2026-03-25) This release includes the following changes since [1.9.1 release](#191-2026-01-26): @@ -97,6 +77,9 @@ This release includes the following changes since stable. * Promote `Format.pcmEncoding` and `C.PcmEncoding` from `UnstableApi` to stable. + * Add `Format.primaryTrackGroupId` to identify the primary group of + embedded `Format` instances + ([#294](https://github.com/androidx/media/issues/294)). * ExoPlayer: * Add clipping capabilities to `AdsMediaSource`. * Add support for dynamic scheduling in `MediaCodecVideoRenderer` to @@ -116,12 +99,36 @@ This release includes the following changes since * Fix a bug where `ProgressiveMediaSource` propagates out-of-date timeline info to the player, and the queued periods unexpectedly get removed ([#3016](https://github.com/androidx/media/issues/3016)). + * Fix issue where ExoPlayer would not request delayed audio focus when + playback is requested during phone calls. + * Fix bug where transitions from on-demand to live content may cause + re-buffers at the end of the on-demand content + ([#3052](https://github.com/androidx/media/issues/3052)). + * Add support for Dolby Vision Profile 10 + ([#2830](https://github.com/androidx/media/pull/2830)). + * Fix bug where an uninitialized pre roll ad produced an + `ArrayIndexOutOfBoundsException` + ([#3125](https://github.com/androidx/media/issues/3125)). * CompositionPlayer: * Add support for `EditedMediaItem.removeVideo`. * Transformer: * `EditedMediaItem.Builder.setFrameRate()` can now set a maximum output frame rate for video, which is particularly helpful when increasing media speed with `setSpeed()`. +* Track selection: + * Change signature of `DefaultTrackSelector.selectAllTracks` to include + the output array as a parameter, pre-populated with track overrides. + * Fix issue where embedded metadata tracks are selected irrespective of + whether the corresponding primary track is selected + ([#294](https://github.com/androidx/media/issues/294)). + * Stabilize `TrackSelectionParameters` options for + `preferredVideoLanguages`, `preferredVideoLabels`, + `preferredAudioLabels`, `preferredTextLabels` and `selectTextByDefault`. +* Extractors: + * MP4: Support files with large `uuid` boxes before other boxes like + `moov` ([#3046](https://github.com/androidx/media/issues/3046)). + * MP4: Add support for Versatile Video Coding (VVC) tracks in MP4 + containers. * Inspector: * Breaking change: remove `FrameExtractor`. Users should now depend on the new `:media3-inspector-frame` module and update imports to @@ -139,10 +146,20 @@ This release includes the following changes since * Improve the retry logic of `AudioOutput` initialization in `DefaultAudioSink` ([#2905](https://github.com/androidx/media/issues/2905)). + * Attempt to match the output layout of the speakers (or Spatializer if + applicable) when decoding IAMF, both with the IAMF extension and media + codec decoders, supported by new fields and logic in AudioCapabilities + and AudioCapabilitiesReceiver. + * Fix bug where playback gets stuck when playing a playlist of gapless + audio in compressed offload mode. * Text: * VobSub: Use `START` and `STOP` SPU control commands to derive cue duration instead of always defaulting to 5 seconds ([#2935](https://github.com/androidx/media/issues/2935)). +* Metadata: + * Increase default count of metadata renderers to four to cover all + potential metadata tracks published by HLS variants and renditions + ([#3043](https://github.com/androidx/media/issues/3043)). * Effect: * Move `LottieOverlay` to a new `:media3-effect-lottie` module (which also involved renaming it from `androidx.media3.effect.LottieOverlay` to @@ -191,6 +208,16 @@ This release includes the following changes since when playback transitioned from a given media item index to another with the same media item for discontinuity reason 'AUTO' and 'SEEK' ([#2997](https://github.com/androidx/media/issues/2997)). + * Fix issue where system UI button placement workaround negatively affects + other UI surface like Android Auto or manufacturers not needing the + workaround ([#3041](https://github.com/androidx/media/issues/3041)). + * Fix issue where access to subscriptions was not synchronized + ([#3056](https://github.com/androidx/media/issues/3056)). + * Workaround SysUI bug on API 29 where media session artwork in a + particular size can crash SysUI and force the device to reboot + ([#3118](https://github.com/androidx/media/issues/3118)). + * Fix notification artwork icon scaling on Android Q + ([#3084](https://github.com/androidx/media/issues/3084)). * UI: * Add `PlaybackSpeedControl` Composable to `media3-ui-compose` for displaying and modifying playback speed. @@ -200,6 +227,11 @@ This release includes the following changes since displaying player progress and performing seeks using dragging and tapping ([#2288](https://github.com/androidx/media/issues/2288)). * Avoid large offscreen buffer in animating PlayerControlView. + * Add `Player` Composable to `media3-ui-compose-material3` which combines + a `ContentFrame` with customizable controls aligned to top, center, and + bottom. + * Hide playback position from the accessibility service to prevent + continuous announcement update when `PlayerControlView` is visible. * HLS extension: * Support X-PLAYOUT-LIMIT with HLS interstitials ([#2484](https://github.com/androidx/media/issues/2484)). @@ -207,6 +239,28 @@ This release includes the following changes since ([#1854](https://github.com/androidx/media/issues/1854)). * Optimize HLS playlist parsing by caching regular expression matchers ([#3008](https://github.com/androidx/media/pull/3008)). + * Expose ID3 (EMSG) metadata track in audio renditions + ([#3043](https://github.com/androidx/media/issues/3043)). + * Allow location fallback upon encountering load errors, if redundant + streams from different locations are available + ([#1988](https://github.com/androidx/media/issues/1988)). + * Fix X-SNAP behaviour for HLS interstitials to correctly calculate start + and resumption position + ([#3013](https://github.com/androidx/media/issues/3013)). +* DASH extension: + * Fix issue where tracks with unaligned segment start times cause delays + or missed samples at the beginning of playback + ([#3057](https://github.com/androidx/media/issues/3057)). + * Fix bug where timestamps of inband EMSG v0 metadata are not set + correctly ([#3024](https://github.com/androidx/media/issues/3024)). +* RTSP extension: + * Fixed regression in H.264 and H.265 streaming that would incorrectly + parse samples of the same access unit that spanned multiple RTP packets. + This resulted in visual artifacts and corruption in the video + ([#3121](https://github.com/androidx/media/issues/3121)). +* Decoder extensions (FFmpeg, VP9, AV1, etc.): + * Add MPEG-H UI manager support to the MPEG-H decoder extension + ([#3066](https://github.com/androidx/media/pull/3066)). * Cast extension: * Add `RemoteCastPlayer.Builder#setTrackSelector` method that enables the implementation of track selection for Cast playbacks. @@ -215,6 +269,31 @@ This release includes the following changes since `androidx.media3.inspector.frame.FrameExtractor` instead. * Remove `ChannelMixingMatrix.create()`. Use `ChannelMixingMatrix.createForConstantGain()` instead. + * Remove `androidx.media3.extractor.metadata.flac.VorbisComment`. Use + `androidx.media3.extractor.metadata.vorbis.VorbisComment` instead. + * Remove `MediaSource.prepareSource(MediaSourceCaller, TransferListener)`. + Use `MediaSource.prepareSource(MediaSourceCaller, TransferListener, + PlayerId)` instead. + +### 1.10.0-rc03 (2026-03-25) + +Use the 1.10.0 [stable version](#1100-2026-03-25). + +### 1.10.0-rc02 (2026-03-16) + +Use the 1.10.0 [stable version](#1100-2026-03-25). + +### 1.10.0-rc01 (2026-03-10) + +Use the 1.10.0 [stable version](#1100-2026-03-25). + +### 1.10.0-beta01 (2026-02-23) + +Use the 1.10.0 [stable version](#1100-2026-03-25). + +### 1.10.0-alpha01 (2026-02-06) + +Use the 1.10.0 [stable version](#1100-2026-03-25). ## 1.9 @@ -769,6 +848,12 @@ This release includes the following changes since * Add support for displaying a media route button on a Composable UI. * Add support for displaying a media route button on an action bar menu. * Add support for displaying a media route button as a View UI. + * Change the Cast media channel message type used to populate the Cast + receiver's media queue from "QUEUE_LOAD" to "LOAD" + ([CAF docs](https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.MessageType)). + This may affect receiver applications' relying on a specific message + format to load the media queue + ([#3080](https://github.com/androidx/media/issues/3080)). * Test Utilities: * Add maximum time diff for the auto-advancing behavior of `FakeClock`. It defaults to 1 second, but is configurable via `FakeClock.Builder`. @@ -1462,7 +1547,7 @@ This release includes the following changes since the line endings ([#2167](https://github.com/androidx/media/issues/2167)). * DRM: * Fix `MediaCodec$CryptoException: Operation not supported in this - configuration` error when playing ClearKey content on API < 27 devices + configuration` error when playing ClearKey content on API below 27 ([#1732](https://github.com/androidx/media/issues/1732)). * Effect: * Moved the functionality of `OverlaySettings` into @@ -3976,7 +4061,7 @@ This release corresponds to the * Use LG AC3 audio decoder advertising non-standard MIME type. * Change the return type of `AudioAttributes.getAudioAttributesV21()` from `android.media.AudioAttributes` to a new `AudioAttributesV21` wrapper - class, to prevent slow ART verification on API < 21. + class, to prevent slow ART verification on API below 21. * Query the platform (API 29+) or assume the audio encoding channel count for audio passthrough when the format audio channel count is unset, which occurs with HLS chunkless preparation diff --git a/common_library_config.gradle b/common_config.gradle similarity index 98% rename from common_library_config.gradle rename to common_config.gradle index 611ea7ce8c4..6d74a86f5f3 100644 --- a/common_library_config.gradle +++ b/common_config.gradle @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. apply from: "$gradle.ext.androidxMediaSettingsDir/constants.gradle" -apply plugin: 'com.android.library' group = 'androidx.media3' diff --git a/constants.gradle b/constants.gradle index 40513baf94d..741c063422d 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.10.0-alpha01' - releaseVersionCode = 1_010_000_0_01 + releaseVersion = '1.10.1' + releaseVersionCode = 1_010_001_3_00 minSdkVersion = 23 // 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 73f3c542f30..e92e330335a 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -102,9 +102,6 @@ project(modulePrefix + 'lib-effect').projectDir = new File(rootDir, 'libraries/e include modulePrefix + 'lib-effect-lottie' project(modulePrefix + 'lib-effect-lottie').projectDir = new File(rootDir, 'libraries/effect_lottie') -include modulePrefix + 'lib-effect-ndk' -project(modulePrefix + 'lib-effect-ndk').projectDir = new File(rootDir, 'libraries/effect_ndk') - include modulePrefix + 'lib-inspector' project(modulePrefix + 'lib-inspector').projectDir = new File(rootDir, 'libraries/inspector') include modulePrefix + 'lib-inspector-frame' diff --git a/demos/compose/src/main/java/androidx/media3/demo/compose/layout/MainScreen.kt b/demos/compose/src/main/java/androidx/media3/demo/compose/layout/MainScreen.kt index fd7f3fee7bd..a0d4df9b3ae 100644 --- a/demos/compose/src/main/java/androidx/media3/demo/compose/layout/MainScreen.kt +++ b/demos/compose/src/main/java/androidx/media3/demo/compose/layout/MainScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -51,6 +52,7 @@ import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleStartEffect import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.util.ExperimentalApi import androidx.media3.demo.compose.buttons.LabeledProgressSlider import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.compose.material3.Player @@ -58,6 +60,7 @@ import androidx.media3.ui.compose.material3.buttons.PlaybackSpeedToggleButton import androidx.media3.ui.compose.material3.buttons.RepeatButton import androidx.media3.ui.compose.material3.buttons.ShuffleButton import androidx.media3.ui.compose.material3.indicator.PositionAndDurationText +import kotlinx.coroutines.delay @Composable fun MainScreen(mediaItems: List, modifier: Modifier = Modifier) { @@ -94,22 +97,36 @@ fun MainScreen(mediaItems: List, modifier: Modifier = Modifier) { MainScreen(player, modifier = modifier.fillMaxSize()) } +@OptIn(ExperimentalMaterial3Api::class) +@androidx.annotation.OptIn(ExperimentalApi::class) @Composable internal fun MainScreen(player: Player?, modifier: Modifier = Modifier) { var currentContentScaleIndex by remember { mutableIntStateOf(0) } var keepContentOnReset by remember { mutableStateOf(false) } // Shutter is on by default + var showControls by remember { mutableStateOf(true) } + var anyPointerDown by remember { mutableStateOf(false) } + LaunchedEffect(showControls, anyPointerDown) { + if (showControls && !anyPointerDown) { + delay(CONTROLS_VISIBILITY_TIMEOUT_MS) + showControls = false + } + } + Box(modifier.background(MaterialTheme.colorScheme.background).statusBarsPadding()) { Player( player = player, + showControls = showControls, + modifier = + Modifier.noRippleClickable { showControls = !showControls } + .reportPointerDown { anyPointerDown = it }, contentScale = CONTENT_SCALES[currentContentScaleIndex].second, keepContentOnReset = keepContentOnReset, - controlsTimeoutMs = CONTROLS_VISIBILITY_TIMEOUT_MS, bottomControls = { player, showControls -> BottomControlsWithLabeledProgress( player, showControls, - Modifier.fillMaxWidth().padding(horizontal = 15.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp), ) }, ) diff --git a/demos/compose/src/main/java/androidx/media3/demo/compose/layout/modifiers.kt b/demos/compose/src/main/java/androidx/media3/demo/compose/layout/modifiers.kt new file mode 100644 index 00000000000..35e255da0fc --- /dev/null +++ b/demos/compose/src/main/java/androidx/media3/demo/compose/layout/modifiers.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2026 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.demo.compose.layout + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput + +@Composable +internal fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier = + clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, // to prevent the ripple from the tap + ) { + onClick() + } + +@Composable +internal fun Modifier.reportPointerDown(onPointerDownChange: (Boolean) -> Unit): Modifier = + this.pointerInput(onPointerDownChange) { + awaitPointerEventScope { + var isDown = false + while (true) { + val event = awaitPointerEvent() + val currentlyPressed = event.changes.any { it.pressed } + if (isDown != currentlyPressed) { + isDown = currentlyPressed + onPointerDownChange(isDown) + } + } + } + } diff --git a/demos/composition/build.gradle b/demos/composition/build.gradle index 97608a8cf9a..1deebc05466 100644 --- a/demos/composition/build.gradle +++ b/demos/composition/build.gradle @@ -63,7 +63,6 @@ dependencies { implementation 'com.google.android.material:material:' + androidxMaterialVersion implementation project(modulePrefix + 'lib-effect') implementation project(modulePrefix + 'lib-effect-lottie') - implementation project(modulePrefix + 'lib-effect-ndk') implementation project(modulePrefix + 'lib-exoplayer') implementation project(modulePrefix + 'lib-exoplayer-dash') implementation project(modulePrefix + 'lib-muxer') diff --git a/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.kt b/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.kt index 3c3f4067c7e..7f615cf4d34 100644 --- a/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.kt +++ b/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.kt @@ -336,21 +336,6 @@ class CompositionPreviewActivity : AppCompatActivity() { }, modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = stringResource(R.string.frame_consumer_enabled), - modifier = Modifier.textPadding(), - ) - Switch( - checked = uiState.outputSettingsState.frameConsumerEnabled, - onCheckedChange = { isEnabled -> viewModel.onFrameConsumerEnabledChanged(isEnabled) }, - ) - } - Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, 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 590eb89236e..1a832d31eee 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 @@ -63,11 +63,8 @@ import androidx.media3.effect.LanczosResample import androidx.media3.effect.MultipleInputVideoGraph import androidx.media3.effect.OverlayEffect import androidx.media3.effect.Presentation -import androidx.media3.effect.ProcessAndRenderToSurfaceConsumer import androidx.media3.effect.RgbFilter import androidx.media3.effect.StaticOverlaySettings -import androidx.media3.effect.ndk.NdkCompositionPlayerBuilder -import androidx.media3.effect.ndk.NdkTransformerBuilder import androidx.media3.transformer.Composition import androidx.media3.transformer.CompositionPlayer import androidx.media3.transformer.EditedMediaItem @@ -107,16 +104,7 @@ class CompositionPreviewViewModel(application: Application) : AndroidViewModel(a var compositionPlayer by mutableStateOf(createCompositionPlayer()) val EXPORT_ERROR_MESSAGE = application.resources.getString(R.string.export_error) val EXPORT_STARTED_MESSAGE = application.resources.getString(R.string.export_started) - internal var frameConsumerEnabled: Boolean = false internal var surfaceView: SurfaceView? = null - internal val packetConsumerFactory: ProcessAndRenderToSurfaceConsumer.Factory by lazy { - if (SDK_INT < 34) { - throw IllegalStateException( - "Render with PacketConsumer requires API 34+" - ) - } - ProcessAndRenderToSurfaceConsumer.Factory() - } private val glExecutorService: ExecutorService by lazy { Util.newSingleThreadExecutor("CompositionDemo::GlThread") } @@ -220,12 +208,6 @@ class CompositionPreviewViewModel(application: Application) : AndroidViewModel(a DebugTraceUtil.enableTracing = enable } - fun onFrameConsumerEnabledChanged(isEnabled: Boolean) { - _uiState.update { - it.copy(outputSettingsState = it.outputSettingsState.copy(frameConsumerEnabled = isEnabled)) - } - } - fun onIncludeBackgroundAudioChanged(isEnabled: Boolean) { _uiState.update { it.copy(outputSettingsState = it.outputSettingsState.copy(includeBackgroundAudio = isEnabled)) @@ -494,23 +476,7 @@ class CompositionPreviewViewModel(application: Application) : AndroidViewModel(a } val filePath = outputFile!!.absolutePath - val transformerBuilder = - if (uiState.value.outputSettingsState.frameConsumerEnabled) { - if (SDK_INT < 34) { - _uiState.update { - it.copy(snackbarMessage = "API 34+ required to export with PacketConsumer") - } - return - } - NdkTransformerBuilder.create(getApplication()) - .setHardwareBufferEffectsPipeline( - // TODO: b/449957627 - Implement HardwareBuffer compositing. - DefaultHardwareBufferEffectsPipeline() - ) - } else { - Transformer.Builder(/* context= */ getApplication()) - } - + val transformerBuilder = Transformer.Builder(/* context= */ getApplication()) if (SAME_AS_INPUT_OPTION != settings.audioMimeType) { transformerBuilder.setAudioMimeType(settings.audioMimeType) } @@ -780,18 +746,9 @@ class CompositionPreviewViewModel(application: Application) : AndroidViewModel(a } private fun createCompositionPlayer(): CompositionPlayer { - val playerBuilder: CompositionPlayer.Builder - frameConsumerEnabled = uiState.value.outputSettingsState.frameConsumerEnabled - if (uiState.value.outputSettingsState.frameConsumerEnabled && SDK_INT >= 34) { - playerBuilder = NdkCompositionPlayerBuilder.create(getApplication()) - surfaceView?.let { packetConsumerFactory.setOutput(it.holder, it::post) } - playerBuilder.setPacketConsumerFactory(packetConsumerFactory) - playerBuilder.setGlThreadExecutorService(glExecutorService) - } else { - playerBuilder = CompositionPlayer.Builder(getApplication()) - if (uiState.value.compositionLayout != COMPOSITION_LAYOUT[0]) { - playerBuilder.setVideoGraphFactory(MultipleInputVideoGraph.Factory()) - } + val playerBuilder = CompositionPlayer.Builder(getApplication()) + if (uiState.value.compositionLayout != COMPOSITION_LAYOUT[0]) { + playerBuilder.setVideoGraphFactory(MultipleInputVideoGraph.Factory()) } playerBuilder.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true) val player = playerBuilder.build() diff --git a/demos/composition/src/main/java/androidx/media3/demo/composition/data/CompositionState.kt b/demos/composition/src/main/java/androidx/media3/demo/composition/data/CompositionState.kt index 808de4050a6..9f23c189171 100644 --- a/demos/composition/src/main/java/androidx/media3/demo/composition/data/CompositionState.kt +++ b/demos/composition/src/main/java/androidx/media3/demo/composition/data/CompositionState.kt @@ -154,7 +154,6 @@ sealed interface PlacementState { */ @OptIn(UnstableApi::class) data class OutputSettingsState( - val frameConsumerEnabled: Boolean = false, val includeBackgroundAudio: Boolean = false, val resolutionHeight: String, val hdrMode: Int, diff --git a/demos/composition/src/main/res/values/strings.xml b/demos/composition/src/main/res/values/strings.xml index ff28ee1b734..97cc7bb33f3 100644 --- a/demos/composition/src/main/res/values/strings.xml +++ b/demos/composition/src/main/res/values/strings.xml @@ -27,7 +27,6 @@ Export completed in %.3f seconds.\nOutput: %s Export error Export started - Use FrameConsumer Add background audio Output video resolution HDR mode diff --git a/demos/session_service/build.gradle b/demos/session_service/build.gradle index 79d14a2aa8a..0d9f12c0846 100644 --- a/demos/session_service/build.gradle +++ b/demos/session_service/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" apply plugin: 'kotlin-android' apply plugin: 'com.google.protobuf' diff --git a/demos/transformer/build.gradle b/demos/transformer/build.gradle index 32eaa34f4ff..669336a4f5d 100644 --- a/demos/transformer/build.gradle +++ b/demos/transformer/build.gradle @@ -78,7 +78,6 @@ dependencies { implementation 'androidx.window:window:' + androidxWindowVersion implementation 'com.google.android.material:material:' + androidxMaterialVersion implementation project(modulePrefix + 'lib-effect') - implementation project(modulePrefix + 'lib-effect-ndk') implementation project(modulePrefix + 'lib-exoplayer') implementation project(modulePrefix + 'lib-exoplayer-dash') implementation project(modulePrefix + 'lib-transformer') diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java index 9e5d93358f1..2838931a45d 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java @@ -102,7 +102,6 @@ public final class ConfigurationActivity extends AppCompatActivity { public static final String TEXT_OVERLAY_TEXT = "text_overlay_text"; public static final String TEXT_OVERLAY_TEXT_COLOR = "text_overlay_text_color"; public static final String TEXT_OVERLAY_ALPHA = "text_overlay_alpha"; - public static final String ENABLE_PACKET_PROCESSOR = "enable_packet_processor"; // Video effect selections. public static final int DIZZY_CROP_INDEX = 0; @@ -184,7 +183,6 @@ public final class ConfigurationActivity extends AppCompatActivity { private CheckBox produceFragmentedMp4; private Spinner hdrModeSpinner; private CheckBox enableTrimOptimization; - private CheckBox enablePacketProcessorCheckBox; private CheckBox enableMp4EditListTrimming; private CheckBox enableCodecDbLite; private Button selectAudioEffectsButton; @@ -319,7 +317,6 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { enableMp4EditListTrimming.setChecked(false); } }); - enablePacketProcessorCheckBox = findViewById(R.id.enable_packet_processor); enableMp4EditListTrimming.setOnCheckedChangeListener( (buttonView, isChecked) -> { if (isChecked) { @@ -417,7 +414,6 @@ private void startExport() { bundle.putBoolean(ABORT_SLOW_EXPORT, abortSlowExportCheckBox.isChecked()); bundle.putBoolean(PRODUCE_FRAGMENTED_MP4, produceFragmentedMp4.isChecked()); bundle.putBoolean(ENABLE_TRIM_OPTIMIZATION, enableTrimOptimization.isChecked()); - bundle.putBoolean(ENABLE_PACKET_PROCESSOR, enablePacketProcessorCheckBox.isChecked()); bundle.putBoolean(ENABLE_MP4_EDIT_LIST_TRIMMING, enableMp4EditListTrimming.isChecked()); bundle.putBoolean(ENABLE_CODECDB_LITE, enableCodecDbLite.isChecked()); String selectedHdrMode = String.valueOf(hdrModeSpinner.getSelectedItem()); diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java index 59f7edd38ef..87005116380 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java @@ -365,13 +365,6 @@ private Transformer createTransformer( @Nullable Bundle bundle, Composition composition, Uri inputUri, String filePath) { Transformer.Builder transformerBuilder = new Transformer.Builder(/* context= */ this); - if (bundle != null && bundle.getBoolean(ConfigurationActivity.ENABLE_PACKET_PROCESSOR)) { - if (SDK_INT < 34) { - throw new IllegalStateException("API version 34+ required to export with PacketProcessor"); - } - transformerBuilder.setHardwareBufferEffectsPipeline( - new DefaultHardwareBufferEffectsPipeline()); - } transformerBuilder.addListener( new Transformer.Listener() { @Override diff --git a/demos/transformer/src/main/res/layout/configuration_activity.xml b/demos/transformer/src/main/res/layout/configuration_activity.xml index af10d38a3b2..8cebb54807b 100644 --- a/demos/transformer/src/main/res/layout/configuration_activity.xml +++ b/demos/transformer/src/main/res/layout/configuration_activity.xml @@ -256,15 +256,6 @@ android:id="@+id/enable_trim_optimization" android:layout_gravity="end"/> - - - - Trim HDR mode Enable trim optimization - Enable Packet Processor Enable MP4 edit list trimming Enable CodecDB Lite Add audio effects diff --git a/docsamples/build.gradle b/docsamples/build.gradle index 42d1f28cec6..d8d7eb2c3a2 100644 --- a/docsamples/build.gradle +++ b/docsamples/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" apply plugin: 'kotlin-android' apply plugin: 'org.jetbrains.kotlin.plugin.compose' diff --git a/libraries/cast/build.gradle b/libraries/cast/build.gradle index bef24e4a027..7988ace1eeb 100644 --- a/libraries/cast/build.gradle +++ b/libraries/cast/build.gradle @@ -14,7 +14,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget -apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" +apply plugin: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" apply plugin: 'kotlin-android' apply plugin: 'org.jetbrains.kotlin.plugin.compose' diff --git a/libraries/common/build.gradle b/libraries/common/build.gradle index 5286807f577..02ba0be4fbd 100644 --- a/libraries/common/build.gradle +++ b/libraries/common/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" // Make sure this project is evaluated after all other libraries. This ensures // the Gradle properties of each library are populated and we can automatically diff --git a/libraries/common/src/main/java/androidx/media3/common/ForwardingSimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/ForwardingSimpleBasePlayer.java index 7482218742e..42b2fc137a9 100644 --- a/libraries/common/src/main/java/androidx/media3/common/ForwardingSimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/ForwardingSimpleBasePlayer.java @@ -123,6 +123,7 @@ protected State getState() { if (player.isCommandAvailable(Player.COMMAND_GET_AUDIO_ATTRIBUTES)) { state.setAudioAttributes(player.getAudioAttributes()); } + state.setAudioSessionId(player.getAudioSessionId()); state.setAvailableCommands(player.getAvailableCommands()); if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) { state.setContentBufferedPositionMs(positionSuppliers.contentBufferedPositionSupplier); 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 7918bce8344..dffb4f9f55a 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java @@ -28,11 +28,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.10.0-alpha01"; + public static final String VERSION = "1.10.1"; /** 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.10.0-alpha01"; + public static final String VERSION_SLASHY = "AndroidXMedia3/1.10.1"; /** * The version of the library expressed as an integer, for example 1002003300. @@ -46,7 +46,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_010_000_0_01; + public static final int VERSION_INT = 1_010_001_3_00; /** Whether the library was compiled with {@link TraceUtil} trace enabled. */ public static final boolean TRACE_ENABLED = true; diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index 070c4d908c1..b409bf9f901 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -43,6 +43,7 @@ import androidx.media3.common.util.Util; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -52,6 +53,7 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -2216,7 +2218,7 @@ public long get() { private final ListenerSet listeners; private final Looper applicationLooper; private final HandlerWrapper applicationHandler; - private final HashSet> pendingOperations; + private final Set> pendingOperations; private final Timeline.Period period; private @MonotonicNonNull State state; @@ -2242,7 +2244,7 @@ protected SimpleBasePlayer(Looper applicationLooper) { protected SimpleBasePlayer(Looper applicationLooper, Clock clock) { this.applicationLooper = applicationLooper; applicationHandler = clock.createHandler(applicationLooper, /* callback= */ null); - pendingOperations = new HashSet<>(); + pendingOperations = Sets.newIdentityHashSet(); period = new Timeline.Period(); @SuppressWarnings("nullness:argument.type.incompatible") // Using this in constructor. ListenerSet listenerSet = @@ -3817,6 +3819,11 @@ private void updateStateAndInformListeners( Player.EVENT_AUDIO_ATTRIBUTES_CHANGED, listener -> listener.onAudioAttributesChanged(newState.audioAttributes)); } + if (previousState.audioSessionId != newState.audioSessionId) { + listeners.queueEvent( + Player.EVENT_AUDIO_SESSION_ID, + listener -> listener.onAudioSessionIdChanged(newState.audioSessionId)); + } if (!previousState.videoSize.equals(newState.videoSize)) { listeners.queueEvent( Player.EVENT_VIDEO_SIZE_CHANGED, diff --git a/libraries/common/src/main/java/androidx/media3/common/text/Cue.java b/libraries/common/src/main/java/androidx/media3/common/text/Cue.java index 6f3063a21f0..475eeefe384 100644 --- a/libraries/common/src/main/java/androidx/media3/common/text/Cue.java +++ b/libraries/common/src/main/java/androidx/media3/common/text/Cue.java @@ -274,9 +274,9 @@ public final class Cue { public final float size; /** - * The bitmap height as a fraction of the of the viewport size, or {@link #DIMEN_UNSET} if the - * bitmap should be displayed at its natural height given the bitmap dimensions and the specified - * {@link #size}. + * The bitmap height as a fraction of the viewport size, or {@link #DIMEN_UNSET} if the bitmap + * should be displayed at its natural height given the bitmap dimensions and the specified {@link + * #size}. */ public final float bitmapHeight; 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 7c3a0ae28d3..18f389552a6 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 @@ -19,6 +19,7 @@ import android.annotation.SuppressLint; import android.media.MediaCodecInfo; +import android.media.MediaCodecInfo.CodecProfileLevel; import android.util.Pair; import androidx.annotation.Nullable; import androidx.media3.common.C; @@ -26,11 +27,13 @@ import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import com.google.common.collect.ImmutableList; +import com.google.common.primitives.UnsignedBytes; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -562,9 +565,9 @@ public static String buildApvCodecString(byte[] initializationData) { "Invalid APV CSD version: %s", initializationData[0]); // configurationVersion == 1 - int profile = initializationData[5]; - int level = initializationData[6]; - int band = initializationData[7]; + int profile = UnsignedBytes.toInt(initializationData[5]); + int level = UnsignedBytes.toInt(initializationData[6]); + int band = UnsignedBytes.toInt(initializationData[7]); return Util.formatInvariant("apv1.apvf%d.apvl%d.apvb%d", profile, level, band); } @@ -1001,6 +1004,36 @@ private static Pair getDolbyVisionProfileAndLevel( return new Pair<>(profile, level); } + /** + * Returns a Dolby Vision base layer codec MIME type of the provided {@link Format}. + * + * @param format The media format. + * @return A Dolby Vision base layer MIME type, or {@code null} if a Dolby Vision profile is not + * identified. + */ + @Nullable + public static String getDolbyVisionBaseLayerMimeType(Format format) { + if (!Objects.equals(format.sampleMimeType, MimeTypes.VIDEO_DOLBY_VISION)) { + return null; + } + @Nullable Pair codecProfileAndLevel = getCodecProfileAndLevel(format); + if (codecProfileAndLevel == null) { + return null; + } + switch (codecProfileAndLevel.first) { + case CodecProfileLevel.DolbyVisionProfileDvheDtr: // profile 4 + case CodecProfileLevel.DolbyVisionProfileDvheStn: // profile 5 + case CodecProfileLevel.DolbyVisionProfileDvheSt: // profile 8 + return MimeTypes.VIDEO_H265; + case CodecProfileLevel.DolbyVisionProfileDvavSe: // profile 9 + return MimeTypes.VIDEO_H264; + case CodecProfileLevel.DolbyVisionProfileDvav110: // profile 10 + return MimeTypes.VIDEO_AV1; + default: + return null; + } + } + /** Returns H263 profile and level from codec string. */ private static Pair getH263ProfileAndLevel(String codec, String[] parts) { Pair defaultProfileAndLevel = diff --git a/libraries/common/src/test/java/androidx/media3/common/ForwardingSimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/ForwardingSimpleBasePlayerTest.java index c97d84edafc..062e660ac47 100644 --- a/libraries/common/src/test/java/androidx/media3/common/ForwardingSimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/ForwardingSimpleBasePlayerTest.java @@ -84,7 +84,9 @@ public void forwardingSimpleBasePlayer_overridesAllSimpleBasePlayerMethods() thr public void getterMethods_noOtherMethodCalls_returnCurrentStateFromWrappedPlayer() { Player.Commands commands = new Player.Commands.Builder() - .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE) + .addAllCommands() + // Remove a command to check the commands are propagated through the wrapping player. + .remove(Player.COMMAND_SET_MEDIA_ITEM) .build(); PlaybackException error = new PlaybackException( @@ -169,6 +171,7 @@ public void getterMethods_noOtherMethodCalls_returnCurrentStateFromWrappedPlayer .setPlaybackParameters(playbackParameters) .setTrackSelectionParameters(trackSelectionParameters) .setAudioAttributes(audioAttributes) + .setAudioSessionId(1234) .setVolume(0.5f) .setVideoSize(videoSize) .setCurrentCues(cueGroup) @@ -191,7 +194,7 @@ protected State getState() { } }; - Player forwardingPlayer = new ForwardingPlayer(wrappedPlayer); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); assertThat(forwardingPlayer.getApplicationLooper()).isEqualTo(Looper.myLooper()); assertThat(forwardingPlayer.getAvailableCommands()).isEqualTo(commands); @@ -223,6 +226,7 @@ protected State getState() { assertThat(forwardingPlayer.getContentPosition()).isEqualTo(456); assertThat(forwardingPlayer.getContentBufferedPosition()).isEqualTo(499); assertThat(forwardingPlayer.getAudioAttributes()).isEqualTo(audioAttributes); + assertThat(forwardingPlayer.getAudioSessionId()).isEqualTo(1234); assertThat(forwardingPlayer.getVolume()).isEqualTo(0.5f); assertThat(forwardingPlayer.getVideoSize()).isEqualTo(videoSize); assertThat(forwardingPlayer.getCurrentCues()).isEqualTo(cueGroup); @@ -1982,8 +1986,6 @@ public void setPlayer_afterReplacement_playerCallsForwardedToNewPlayer() { .setAvailableCommands( new Player.Commands.Builder().add(Player.COMMAND_SET_REPEAT_MODE).build()) .build(); - // We need the ForwardingPlayer to allow the mockito spy because setRepeatMode is final in - // SimpleBasePlayer, but not in ForwardingPlayer. Player player1 = mock( Player.class, 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 13b8db7f09b..dff14a4a886 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -1439,6 +1439,7 @@ public void invalidateState_updatesStateAndInformsListeners() throws Exception { .setPlaybackParameters(PlaybackParameters.DEFAULT) .setTrackSelectionParameters(TrackSelectionParameters.DEFAULT) .setAudioAttributes(AudioAttributes.DEFAULT) + .setAudioSessionId(1234) .setVolume(1f) .setVideoSize(VideoSize.UNKNOWN) .setCurrentCues(CueGroup.EMPTY_TIME_ZERO) @@ -1509,6 +1510,7 @@ public void invalidateState_updatesStateAndInformsListeners() throws Exception { .setPlaybackParameters(playbackParameters) .setTrackSelectionParameters(trackSelectionParameters) .setAudioAttributes(audioAttributes) + .setAudioSessionId(5678) .setVolume(0.5f) .setVideoSize(videoSize) .setCurrentCues(cueGroup) @@ -1569,6 +1571,7 @@ protected State getState() { verify(listener).onPlaybackParametersChanged(playbackParameters); verify(listener).onTrackSelectionParametersChanged(trackSelectionParameters); verify(listener).onAudioAttributesChanged(audioAttributes); + verify(listener).onAudioSessionIdChanged(5678); verify(listener).onVolumeChanged(0.5f); verify(listener).onVideoSizeChanged(videoSize); verify(listener).onCues(cueGroup.cues); @@ -1635,6 +1638,7 @@ protected State getState() { Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED, Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, Player.EVENT_AUDIO_ATTRIBUTES_CHANGED, + Player.EVENT_AUDIO_SESSION_ID, Player.EVENT_VOLUME_CHANGED, Player.EVENT_SURFACE_SIZE_CHANGED, Player.EVENT_VIDEO_SIZE_CHANGED, 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 384aa311cf2..9078ae9a61c 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 @@ -25,6 +25,7 @@ import androidx.media3.common.ColorInfo; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; +import androidx.media3.test.utils.TestUtil; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; @@ -192,31 +193,31 @@ public void getCodecProfileAndLevel_rejectsEmptyCodecString() { @Test public void buildApvCodecString_withValidApvSpecificConfig_returnsCorrectCodecString() { byte[] apvSpecificConfig = - new byte[] { - 1, // configurationVersion - 1, // number_of_configuration_entry - 1, // pbu_type - 1, // number_of_frame_info - 0, // reserved_zero_6bits, color_description_present_flag(1 bit), - // capture_time_distance_ignored(1 bit) - 33, // profile_idc - 60, // level_idc - 0, // band_idc - 0, // frame_width (4 bytes) - 0, - 2, - -128, - 0, // frame_height (4 bytes) - 0, - 1, - -32, - 34, // chroma_format_idc (4 bit) + bit_depth_minus8(4 bit) - 0 // capture_time_distance - }; + TestUtil.createByteArray( + 1, // configurationVersion + 1, // number_of_configuration_entry + 1, // pbu_type + 1, // number_of_frame_info + 0, // reserved_zero_6bits, color_description_present_flag(1 bit), + // capture_time_distance_ignored(1 bit) + 33, // profile_idc + 150, // level_idc + 0, // band_idc + 0, // frame_width (4 bytes) + 0, + 2, + 255, + 0, // frame_height (4 bytes) + 0, + 1, + 224, + 34, // chroma_format_idc (4 bit) + bit_depth_minus8(4 bit) + 0 // capture_time_distance + ); String codecString = CodecSpecificDataUtil.buildApvCodecString(apvSpecificConfig); - assertThat(codecString).isEqualTo("apv1.apvf33.apvl60.apvb0"); + assertThat(codecString).isEqualTo("apv1.apvf33.apvl150.apvb0"); } @Test @@ -320,6 +321,40 @@ public void getCodecProfileAndLevel_handlesIamfCodecString_forBaseProfilePcm() { 0); } + @Test + public void + getDolbyVisionBaseLayerMimeType_withNonFallbackCompatibleFormat_returnsBaseEncoding() { + // Profile 10.0 (Full Range PQ) which does NOT allow fallback. + Format formatDav1NoFallbackPossible = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) + .setCodecs("dav1.10.01") + .setColorInfo( + new ColorInfo.Builder() + .setColorSpace(C.COLOR_SPACE_BT2020) + .setColorTransfer(C.COLOR_TRANSFER_ST2084) + .setColorRange(C.COLOR_RANGE_FULL) + .build()) + .build(); + // Profile 10.1 (Limited Range PQ) which allows fallback to AV1. + Format formatDav1FallbackToAv1 = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) + .setCodecs("dav1.10.01") + .setColorInfo( + new ColorInfo.Builder() + .setColorSpace(C.COLOR_SPACE_BT2020) + .setColorTransfer(C.COLOR_TRANSFER_ST2084) + .setColorRange(C.COLOR_RANGE_LIMITED) + .build()) + .build(); + + assertThat(CodecSpecificDataUtil.getDolbyVisionBaseLayerMimeType(formatDav1NoFallbackPossible)) + .isEqualTo(MimeTypes.VIDEO_AV1); + assertThat(CodecSpecificDataUtil.getDolbyVisionBaseLayerMimeType(formatDav1FallbackToAv1)) + .isEqualTo(MimeTypes.VIDEO_AV1); + } + private static void assertCodecProfileAndLevelForCodecsString( String sampleMimeType, String codecs, int profile, int level) { Format format = diff --git a/libraries/common_ktx/build.gradle b/libraries/common_ktx/build.gradle index c9523dfb9aa..9db98f92115 100644 --- a/libraries/common_ktx/build.gradle +++ b/libraries/common_ktx/build.gradle @@ -15,7 +15,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget -apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" +apply plugin: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" apply plugin: 'kotlin-android' android { diff --git a/libraries/container/build.gradle b/libraries/container/build.gradle index a731bb76c64..3b7bd60deaa 100644 --- a/libraries/container/build.gradle +++ b/libraries/container/build.gradle @@ -12,7 +12,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.container' diff --git a/libraries/container/src/main/java/androidx/media3/container/ParsableNalUnitBitArray.java b/libraries/container/src/main/java/androidx/media3/container/ParsableNalUnitBitArray.java index 35838e5e656..d3d37e5579c 100644 --- a/libraries/container/src/main/java/androidx/media3/container/ParsableNalUnitBitArray.java +++ b/libraries/container/src/main/java/androidx/media3/container/ParsableNalUnitBitArray.java @@ -30,6 +30,8 @@ public final class ParsableNalUnitBitArray { private byte[] data; private int byteLimit; + // The start offset which defines the range of data to be considered. + private int startOffset; // The byte offset is never equal to the offset of the 3rd byte in a subsequence [0, 0, 3]. private int byteOffset; @@ -54,6 +56,7 @@ public ParsableNalUnitBitArray(byte[] data, int offset, int limit) { */ public void reset(byte[] data, int offset, int limit) { this.data = data; + startOffset = offset; byteOffset = offset; byteLimit = limit; bitOffset = 0; @@ -116,7 +119,7 @@ public boolean canReadBits(int numBits) { newByteOffset++; newBitOffset -= 8; } - for (int i = oldByteOffset + 1; i <= newByteOffset && newByteOffset < byteLimit; i++) { + for (int i = oldByteOffset + 1; i <= newByteOffset && newByteOffset <= byteLimit; i++) { if (shouldSkipByte(i)) { // Skip the byte and move forward to check three bytes ahead. newByteOffset++; @@ -208,7 +211,8 @@ private int readExpGolombCodeNum() { } private boolean shouldSkipByte(int offset) { - return 2 <= offset + // Do not read outside of [startOffset, byteLimit) bounds. + return startOffset <= offset - 2 && offset < byteLimit && data[offset] == (byte) 0x03 && data[offset - 2] == (byte) 0x00 diff --git a/libraries/container/src/test/java/androidx/media3/container/ParsableNalUnitBitArrayTest.java b/libraries/container/src/test/java/androidx/media3/container/ParsableNalUnitBitArrayTest.java index e9cd4f315bc..b967f003083 100644 --- a/libraries/container/src/test/java/androidx/media3/container/ParsableNalUnitBitArrayTest.java +++ b/libraries/container/src/test/java/androidx/media3/container/ParsableNalUnitBitArrayTest.java @@ -63,6 +63,7 @@ public void readAllEscaping() { new ParsableNalUnitBitArray(ALL_ESCAPING_TEST_DATA, 0, ALL_ESCAPING_TEST_DATA.length); assertThat(array.canReadBits(48)).isTrue(); assertThat(array.canReadBits(49)).isFalse(); + assertThat(array.canReadBits(72)).isFalse(); assertThat(array.readBits(15)).isEqualTo(0); assertThat(array.readBit()).isFalse(); assertThat(array.readBits(17)).isEqualTo(0); @@ -83,6 +84,21 @@ public void readMix() { assertThat(array.canReadBits(1)).isFalse(); } + @Test + public void readMix_respectsStartOffset() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray( + MIX_TEST_DATA, /* offset= */ 2, /* limit= */ MIX_TEST_DATA.length); + assertThat(array.canReadBits(48)).isTrue(); + assertThat(array.canReadBits(49)).isFalse(); + assertThat(array.readBits(7)).isEqualTo(0); + assertThat(array.readBits(2)).isEqualTo(0); + assertThat(array.readBits(17)).isEqualTo(4092); + assertThat(array.readBits(7)).isEqualTo(0); + assertThat(array.readBits(15)).isEqualTo(127); + assertThat(array.canReadBits(1)).isFalse(); + } + @Test public void readExpGolomb() { ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0x9E), 0, 1); diff --git a/libraries/database/build.gradle b/libraries/database/build.gradle index 88e9e1f5674..46e1a1674fa 100644 --- a/libraries/database/build.gradle +++ b/libraries/database/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.database' diff --git a/libraries/datasource/build.gradle b/libraries/datasource/build.gradle index 71aa8bb1ca7..eefa280d9ca 100644 --- a/libraries/datasource/build.gradle +++ b/libraries/datasource/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.datasource' diff --git a/libraries/datasource_cronet/build.gradle b/libraries/datasource_cronet/build.gradle index 54edd86fbcf..b2a63be409f 100644 --- a/libraries/datasource_cronet/build.gradle +++ b/libraries/datasource_cronet/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.datasource.cronet' diff --git a/libraries/datasource_okhttp/build.gradle b/libraries/datasource_okhttp/build.gradle index 7bdc93d6031..9e760789b5d 100644 --- a/libraries/datasource_okhttp/build.gradle +++ b/libraries/datasource_okhttp/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.datasource.okhttp' diff --git a/libraries/datasource_rtmp/build.gradle b/libraries/datasource_rtmp/build.gradle index 1fcf3c0e4cc..a52d8c94627 100644 --- a/libraries/datasource_rtmp/build.gradle +++ b/libraries/datasource_rtmp/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.datasource.rtmp' diff --git a/libraries/decoder/build.gradle b/libraries/decoder/build.gradle index bef3ec4602e..4cd40766152 100644 --- a/libraries/decoder/build.gradle +++ b/libraries/decoder/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.decoder' diff --git a/libraries/decoder_av1/build.gradle b/libraries/decoder_av1/build.gradle index 8684c864915..cb5773a0615 100644 --- a/libraries/decoder_av1/build.gradle +++ b/libraries/decoder_av1/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.decoder.av1' diff --git a/libraries/decoder_av1/src/main/java/androidx/media3/decoder/av1/Dav1dDecoder.java b/libraries/decoder_av1/src/main/java/androidx/media3/decoder/av1/Dav1dDecoder.java index 1b166cad106..4fd58aa0774 100644 --- a/libraries/decoder_av1/src/main/java/androidx/media3/decoder/av1/Dav1dDecoder.java +++ b/libraries/decoder_av1/src/main/java/androidx/media3/decoder/av1/Dav1dDecoder.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState; import android.view.Surface; +import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.util.UnstableApi; @@ -26,7 +27,6 @@ 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; @@ -52,13 +52,15 @@ public final class Dav1dDecoder @GuardedBy("lock") private final ArrayDeque queuedOutputBuffers; + private final DecoderInputBuffer[] allInputBuffers; + @GuardedBy("lock") private final DecoderInputBuffer[] availableInputBuffers; @GuardedBy("lock") private final VideoDecoderOutputBuffer[] availableOutputBuffers; - private long dav1dDecoderContext; + private volatile long dav1dDecoderContext; private volatile @C.VideoOutputMode int outputMode; @@ -120,12 +122,14 @@ public Dav1dDecoder( outputStartTimeUs = C.TIME_UNSET; queuedInputBuffers = new ArrayDeque<>(); queuedOutputBuffers = new ArrayDeque<>(); + allInputBuffers = new DecoderInputBuffer[numInputBuffers]; availableInputBuffers = new DecoderInputBuffer[numInputBuffers]; availableInputBufferCount = numInputBuffers; for (int i = 0; i < availableInputBufferCount; i++) { - availableInputBuffers[i] = + allInputBuffers[i] = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); - availableInputBuffers[i].ensureSpaceForWrite(initialInputBufferSize); + allInputBuffers[i].ensureSpaceForWrite(initialInputBufferSize); + availableInputBuffers[i] = allInputBuffers[i]; } availableOutputBuffers = new VideoDecoderOutputBuffer[numOutputBuffers]; availableOutputBufferCount = numOutputBuffers; @@ -137,7 +141,7 @@ public Dav1dDecoder( @Override public void run() { Dav1dDecoder.this.dav1dDecoderContext = - dav1dInit(threads, maxFrameDelay, useCustomAllocator); + dav1dInit(threads, maxFrameDelay, useCustomAllocator, numInputBuffers); if (dav1dCheckError(Dav1dDecoder.this.dav1dDecoderContext) == DAV1D_ERROR) { synchronized (lock) { Dav1dDecoder.this.exception = @@ -247,12 +251,17 @@ public void setOutputMode(@C.VideoOutputMode int outputMode) { */ 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."); + synchronized (lock) { + if (outputMode != C.VIDEO_OUTPUT_MODE_SURFACE_YUV) { + throw new Dav1dDecoderException("Unsupported Output Mode."); + } + int error = dav1dRenderFrame(dav1dDecoderContext, surface, outputBuffer); + if (error != DAV1D_OK) { + String errorMsg = dav1dGetErrorMessage(dav1dDecoderContext); + int jniCode = dav1dGetLastErrorJniStatusCode(dav1dDecoderContext); + throw new Dav1dDecoderException( + "Failed to render output buffer to surface. " + errorMsg, jniCode); + } } } @@ -330,10 +339,18 @@ private boolean decode() throws InterruptedException { ByteBuffer inputData = Util.castNonNull(inputBuffer.data); int inputOffset = inputData.position(); int inputSize = inputData.remaining(); + int bufferIndex = -1; + for (int i = 0; i < allInputBuffers.length; i++) { + if (allInputBuffers[i] == inputBuffer) { + bufferIndex = i; + break; + } + } int status = dav1dDecode( dav1dDecoderContext, inputBuffer, + bufferIndex, inputOffset, inputSize, decodeOnly, @@ -344,6 +361,13 @@ private boolean decode() throws InterruptedException { throw new Dav1dDecoderException( "dav1dDecode error: " + dav1dGetErrorMessage(dav1dDecoderContext)); } + // If the decoder is overloaded, we will get EAGAIN. In this case, we will put the input + // buffer back into the front of the queue and try again later. + if (status == DAV1D_EAGAIN) { + synchronized (lock) { + queuedInputBuffers.addFirst(inputBuffer); + } + } while ((status = dav1dGetFrame(dav1dDecoderContext, outputBuffer)) == DAV1D_OK || status == DAV1D_DECODE_ONLY) { if (status == DAV1D_DECODE_ONLY) { @@ -418,6 +442,16 @@ private void releaseInputBuffer(DecoderInputBuffer inputBuffer) { } } + private void releaseInputBuffer(int bufferIndex) { + synchronized (lock) { + DecoderInputBuffer inputBuffer = allInputBuffers[bufferIndex]; + // Prevent the EAGAIN double-free race condition: + if (!queuedInputBuffers.contains(inputBuffer)) { + releaseInputBufferInternal(inputBuffer); + } + } + } + @GuardedBy("lock") private void releaseInputBufferInternal(DecoderInputBuffer inputBuffer) { inputBuffer.clear(); @@ -481,9 +515,11 @@ private void run() { * @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. + * @param numInputBuffers Number of input buffers to be allocated for the decoder. * @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); + private native long dav1dInit( + int threads, int maxFrameDelay, boolean useCustomAllocator, int numInputBuffers); /** * Deallocates the decoder context. @@ -500,7 +536,7 @@ private void run() { * @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 flags {@link androidx.media3.common.C.BufferFlags} Information about output buffer. * @param timeUs Time of input data. * @param outputMode Output mode for output buffer. * @return {@link #DAV1D_OK} if successful, {@link #DAV1D_ERROR} if an error occurred, {@link @@ -509,6 +545,7 @@ private void run() { private native int dav1dDecode( long context, DecoderInputBuffer inputBuffer, + int bufferIndex, int inputOffset, int inputSize, boolean decodeOnly, @@ -553,6 +590,14 @@ private native int dav1dRenderFrame( */ private native String dav1dGetErrorMessage(long context); + /** + * Returns the JNI status code of the last error encountered in the given context. + * + * @param context Decoder context. + * @return The JNI status code of the last encountered error. + */ + private native int dav1dGetLastErrorJniStatusCode(long context); + /** * Returns whether an error occurred. * diff --git a/libraries/decoder_av1/src/main/java/androidx/media3/decoder/av1/Dav1dDecoderException.java b/libraries/decoder_av1/src/main/java/androidx/media3/decoder/av1/Dav1dDecoderException.java index 73a1d497306..4ca71ce2aac 100644 --- a/libraries/decoder_av1/src/main/java/androidx/media3/decoder/av1/Dav1dDecoderException.java +++ b/libraries/decoder_av1/src/main/java/androidx/media3/decoder/av1/Dav1dDecoderException.java @@ -22,13 +22,33 @@ @UnstableApi public final class Dav1dDecoderException extends DecoderException { + /** The error code returned when the decoder error is unknown. */ + public static final int ERROR_CODE_UNKNOWN = -1; + + /** + * The libdav1d error code associated with the decoder error, or {@link #ERROR_CODE_UNKNOWN} if + * unknown. + */ + public final int errorCode; + + /** + * Constructs a {@code Dav1dDecoderException} with the specified message and error code. + * + * @param message The error message. + * @param errorCode The libdav1d error code. + */ + public Dav1dDecoderException(String message, int errorCode) { + super(message); + this.errorCode = errorCode; + } + /** * Constructs a {@code Dav1dDecoderException} with the specified message. * * @param message The error message. */ public Dav1dDecoderException(String message) { - super(message); + this(message, ERROR_CODE_UNKNOWN); } /** @@ -39,5 +59,6 @@ public Dav1dDecoderException(String message) { */ public Dav1dDecoderException(String message, Throwable cause) { super(message, cause); + this.errorCode = ERROR_CODE_UNKNOWN; } } diff --git a/libraries/decoder_av1/src/main/jni/dav1d_jni.cc b/libraries/decoder_av1/src/main/jni/dav1d_jni.cc index 9112fcc6f06..62b368209d1 100644 --- a/libraries/decoder_av1/src/main/jni/dav1d_jni.cc +++ b/libraries/decoder_av1/src/main/jni/dav1d_jni.cc @@ -137,12 +137,21 @@ enum JniStatusCode { kJniStatusDecoderInitFailed = -8, kJniStatusBufferInitError = -9, kJniStatusANativeWindowError = -10, + kJniStatusDataWrapError = -11, + kJniStatusUserDataWrapError = -12, + kJniStatusSendDataError = -13, + kJniStatusRenderMaybeAcquireFailedError = -14, + kJniStatusRenderNativeWindowNullError = -15, + kJniStatusRenderPictureNullError = -16, + kJniStatusRenderLockFailedError = -17, }; const int kLibdav1dDecoderStatusOk = 0; const char* GetJniErrorMessage(JniStatusCode error_code) { switch (error_code) { + case kJniStatusOk: + return "No JNI error."; case kJniStatusOutOfMemory: return "Out of memory."; case kJniStatusBufferAlreadyReleased: @@ -162,11 +171,38 @@ const char* GetJniErrorMessage(JniStatusCode error_code) { return "Decoder initialization failed."; case kJniStatusBufferInitError: return "Output buffer initialization failed."; + case kJniStatusANativeWindowError: + return "ANativeWindow error."; + case kJniStatusDataWrapError: + return "Data wrap error."; + case kJniStatusUserDataWrapError: + return "User data wrap error."; + case kJniStatusSendDataError: + return "Send data error."; + case kJniStatusRenderMaybeAcquireFailedError: + return "Acquire Native Window failed."; + case kJniStatusRenderNativeWindowNullError: + return "ANativeWindow null natively."; + case kJniStatusRenderPictureNullError: + return "Picture unallocated null natively."; + case kJniStatusRenderLockFailedError: + return "ANativeWindow raw lock broke natively."; default: return "Unrecognized error code."; } } +// Logs and clears any pending JNI exceptions to prevent an ART abort. +// Returns true if an exception was cleared, false otherwise. +bool ClearPendingException(JNIEnv* env) { + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + return true; + } + return false; +} + const int GetThreadCount(jint threads) { switch (threads) { case kDav1dThreadCountDefault: @@ -180,13 +216,8 @@ const int GetThreadCount(jint threads) { } } -// TODO: b/446178978 - Refactor buffer management to remove global JNI -// references. Remove global_ref_input_buffer and global_ref_dav1d_data from -// Cookie. Instead, manage buffers by passing an index into a Java-side -// DecoderInputBuffer array. struct Cookie { - jobject global_ref_input_buffer; - jobject global_ref_dav1d_data; + jint buffer_index; jlong jni_context; }; @@ -219,21 +250,36 @@ struct JniContext { } bool MaybeAcquireNativeWindow(JNIEnv* env, jobject new_surface) { - if (surface == new_surface) { + if (env->IsSameObject(surface, new_surface)) { return true; } if (native_window) { ANativeWindow_release(native_window); + native_window = nullptr; + } + if (surface) { + env->DeleteGlobalRef(surface); + surface = nullptr; } native_window_width = 0; native_window_height = 0; + if (new_surface == nullptr) { + LOGE("MaybeAcquireNativeWindow: new_surface is null."); + jni_status_code = kJniStatusRenderMaybeAcquireFailedError; + return false; + } native_window = ANativeWindow_fromSurface(env, new_surface); if (native_window == nullptr) { - jni_status_code = kJniStatusANativeWindowError; - surface = nullptr; + jni_status_code = kJniStatusRenderMaybeAcquireFailedError; + return false; + } + surface = env->NewGlobalRef(new_surface); + if (surface == nullptr) { + ANativeWindow_release(native_window); + native_window = nullptr; + jni_status_code = kJniStatusOutOfMemory; return false; } - surface = new_surface; return true; } @@ -262,11 +308,13 @@ struct JniContext { int libdav1d_status_code = kLibdav1dDecoderStatusOk; JniStatusCode jni_status_code = kJniStatusOk; - // TODO: b/446178978 - Replace unused_cookies std::vector with a fixed-size - // array. The size should be bounded by the number of input buffers to avoid - // dynamic allocations. - std::vector> unused_cookies; + int num_input_buffers; + std::unique_ptr cookies_array; + std::unique_ptr unused_cookies_array; + int unused_cookies_count; + std::mutex state_mutex; // NOLINT(build/c++11) std::mutex unused_cookies_mutex; // NOLINT(build/c++11) + std::vector cookies_to_release; std::vector> unused_picture_allocator_data; std::mutex unused_picture_allocator_data_mutex; // NOLINT(build/c++11) @@ -324,21 +372,11 @@ void Dav1dDataFreeCallback(const uint8_t* data, void* cookie) { reinterpret_cast(cookie_ptr->jni_context); std::lock_guard unused_cookies_lock( // NOLINT(build/c++11) context->unused_cookies_mutex); - try { - context->unused_cookies.emplace_back(cookie_ptr); - } catch (const std::bad_alloc&) { - // Allocation failed for unique_ptr, prevent memory leak - LOGE("Failed to emplace Cookie: Out of memory"); - JNIEnv* env = nullptr; - // Ensure the current thread is attached to the JVM - jint attach_result = context->jvm->AttachCurrentThread(&env, nullptr); - if (attach_result == JNI_OK) { - env->DeleteGlobalRef(cookie_ptr->global_ref_dav1d_data); - env->DeleteGlobalRef(cookie_ptr->global_ref_input_buffer); - } else { - LOGE("Failed to attach current thread to JVM in Data callback."); - } - delete cookie_ptr; + if (context->unused_cookies_count < context->num_input_buffers) { + context->unused_cookies_array[context->unused_cookies_count++] = cookie_ptr; + } else { + LOGE("unused_cookies_array is full! buffer_index: %d", + cookie_ptr->buffer_index); } } @@ -380,7 +418,7 @@ static int dav1d_picture_allocator(Dav1dPicture* p, void* cookie) { reinterpret_cast(context->byte_buffer_class), context->create_direct_byte_buffer_method, total_size); - if (direct_byte_buffer == nullptr) { + if (ClearPendingException(env) || direct_byte_buffer == nullptr) { LOGE("Failed to create direct byte buffer."); return DAV1D_ERR(ENOMEM); } @@ -438,6 +476,8 @@ void CleanUpAllocatorData(jlong jContext, JNIEnv* env) { return; } JniContext* const context = reinterpret_cast(jContext); + std::lock_guard state_lock( // NOLINT(build/c++11) + context->state_mutex); std::lock_guard // NOLINT(build/c++11) unused_picture_allocator_data_lock( context->unused_picture_allocator_data_mutex); @@ -451,12 +491,33 @@ void CleanUpAllocatorData(jlong jContext, JNIEnv* env) { } // namespace DECODER_FUNC(jlong, dav1dInit, jint threads, jint max_frame_delay, - jboolean use_custom_allocator) { + jboolean use_custom_allocator, jint num_input_buffers) { JniContext* context = new (std::nothrow) JniContext(); if (context == nullptr) { return kStatusError; } + if (num_input_buffers <= 0) { + delete context; + return kStatusError; + } + context->num_input_buffers = num_input_buffers; + context->cookies_array.reset(new (std::nothrow) Cookie[num_input_buffers]); + context->unused_cookies_array.reset(new (std::nothrow) + Cookie*[num_input_buffers]); + context->unused_cookies_count = 0; + if (!context->cookies_array || !context->unused_cookies_array) { + delete context; + return kStatusError; + } + + try { + context->cookies_to_release.reserve(num_input_buffers); + } catch (const std::bad_alloc&) { + delete context; + return kStatusError; + } + // Always get and store the JVM if (env->GetJavaVM(&context->jvm) != JNI_OK) { LOGE("Failed to get JavaVM"); @@ -504,13 +565,27 @@ DECODER_FUNC(jlong, dav1dInit, jint threads, jint max_frame_delay, context->jni_status_code = kJniStatusDecoderInitFailed; } + // Helper macro to execute a JNI lookup and abort initialization if it fails. + // This fully complies with JNI specs so we never call JNI with a pending + // exception. +#define CHECK_JNI_EXCEPTION(assignment) \ + assignment; \ + if (ClearPendingException(env)) { \ + LOGE("dav1dInit: JNI lookup failed: %s", #assignment); \ + context->jni_status_code = kJniStatusDecoderInitFailed; \ + return reinterpret_cast(context); \ + } + // 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/av1/Dav1dDecoder"); + jclass inputBufferClass; + CHECK_JNI_EXCEPTION(inputBufferClass = env->FindClass( + "androidx/media3/decoder/DecoderInputBuffer")); + jclass outputBufferClass; + CHECK_JNI_EXCEPTION(outputBufferClass = env->FindClass( + "androidx/media3/decoder/VideoDecoderOutputBuffer")); + jclass decoderClass; + CHECK_JNI_EXCEPTION(decoderClass = env->FindClass( + "androidx/media3/decoder/av1/Dav1dDecoder")); const jclass byteBufferClass = env->FindClass("java/nio/ByteBuffer"); context->input_buffer_class = env->NewGlobalRef(inputBufferClass); @@ -518,36 +593,42 @@ DECODER_FUNC(jlong, dav1dInit, jint threads, jint max_frame_delay, 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->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"); + CHECK_JNI_EXCEPTION(context->input_data_field = env->GetFieldID( + inputBufferClass, "data", "Ljava/nio/ByteBuffer;")); + CHECK_JNI_EXCEPTION(context->display_width_field = + env->GetFieldID(outputBufferClass, "width", "I")); + CHECK_JNI_EXCEPTION(context->display_height_field = + env->GetFieldID(outputBufferClass, "height", "I")); + CHECK_JNI_EXCEPTION( + context->output_buffer_stride_array_field = + env->GetFieldID(outputBufferClass, "yuvStrides", "[I")); + CHECK_JNI_EXCEPTION(context->ystride_field = + env->GetFieldID(outputBufferClass, "yStride", "I")); + CHECK_JNI_EXCEPTION(context->uvstride_field = + env->GetFieldID(outputBufferClass, "uvStride", "I")); + CHECK_JNI_EXCEPTION(context->init_output_buffer_method = + env->GetMethodID(outputBufferClass, "init", + "(JILjava/nio/ByteBuffer;)V")); + CHECK_JNI_EXCEPTION(context->data_field = env->GetFieldID( + outputBufferClass, "data", "Ljava/nio/ByteBuffer;")); + CHECK_JNI_EXCEPTION(context->decoder_private_field = env->GetFieldID( + outputBufferClass, "decoderPrivate", "J")); + CHECK_JNI_EXCEPTION(context->init_for_yuv_frame_method = env->GetMethodID( + outputBufferClass, "initForYuvFrame", "(IIIII)Z")); + CHECK_JNI_EXCEPTION(context->init_for_private_frame_method = env->GetMethodID( + outputBufferClass, "initForPrivateFrame", "(II)V")); + CHECK_JNI_EXCEPTION(context->set_flags_method = env->GetMethodID( + outputBufferClass, "setFlags", "(I)V")); + CHECK_JNI_EXCEPTION(context->release_input_buffer_method = env->GetMethodID( + decoderClass, "releaseInputBuffer", "(I)V")); + CHECK_JNI_EXCEPTION( + 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;"); +#undef CHECK_JNI_EXCEPTION + // Assert JNI References are valid. assert(inputBufferClass); assert(outputBufferClass); @@ -588,6 +669,10 @@ DECODER_FUNC(void, dav1dClose, jlong jContext) { } // Clean up JNI global references + if (context->surface) { + env->DeleteGlobalRef(context->surface); + context->surface = nullptr; + } if (context->input_buffer_class) env->DeleteGlobalRef(context->input_buffer_class); if (context->output_buffer_class) @@ -600,12 +685,7 @@ DECODER_FUNC(void, dav1dClose, jlong jContext) { { std::lock_guard unused_cookies_lock( // NOLINT(build/c++11) 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(); - } + context->unused_cookies_count = 0; } { if (context->use_custom_allocator) { @@ -616,14 +696,18 @@ DECODER_FUNC(void, dav1dClose, jlong jContext) { } DECODER_FUNC(jint, dav1dDecode, jlong jContext, jobject jInputBuffer, - jint offset, jint length, jboolean decodeOnly, jint flags, - jlong timeUs, jint outputMode) { + jint bufferIndex, jint offset, jint length, jboolean decodeOnly, + jint flags, jlong timeUs, jint outputMode) { if (jContext == kStatusError) { return kStatusError; } JniContext* const context = reinterpret_cast(jContext); const jobject encoded_data = env->GetObjectField(jInputBuffer, context->input_data_field); + if (ClearPendingException(env)) { + LOGE("Failed to get input data field."); + return kStatusError; + } void* const buffer_ptr = env->GetDirectBufferAddress(encoded_data); if (buffer_ptr == nullptr) { LOGE("Failed to get direct buffer address."); @@ -634,18 +718,23 @@ DECODER_FUNC(jint, dav1dDecode, jlong jContext, jobject jInputBuffer, Dav1dData data = {}; - Cookie* cookie = new Cookie(); - cookie->global_ref_input_buffer = env->NewGlobalRef(jInputBuffer); - cookie->global_ref_dav1d_data = env->NewGlobalRef(encoded_data); + if (bufferIndex < 0 || bufferIndex >= context->num_input_buffers) { + LOGE("Invalid buffer index: %d", bufferIndex); + return kStatusError; + } + Cookie* cookie = &context->cookies_array[bufferIndex]; + cookie->buffer_index = bufferIndex; 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; + { + std::lock_guard lock( // NOLINT(build/c++11) + context->state_mutex); + context->jni_status_code = kJniStatusDataWrapError; + } return kStatusError; } @@ -663,13 +752,30 @@ DECODER_FUNC(jint, dav1dDecode, jlong jContext, jobject jInputBuffer, LOGE("Failed to wrap user data."); delete user_data; dav1d_data_unref(&data); + { + std::lock_guard lock( // NOLINT(build/c++11) + context->state_mutex); + context->jni_status_code = kJniStatusUserDataWrapError; + } 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)) { + int libdav1d_status_code = dav1d_send_data(context->decoder, &data); + { + std::lock_guard lock( // NOLINT(build/c++11) + context->state_mutex); + context->libdav1d_status_code = libdav1d_status_code; + } + if (libdav1d_status_code != 0) { LOGE("Failed to send data."); dav1d_data_unref(&data); + if (libdav1d_status_code == DAV1D_ERR(EAGAIN)) { + return kStatusEagain; + } + { + std::lock_guard lock( // NOLINT(build/c++11) + context->state_mutex); + context->jni_status_code = kJniStatusSendDataError; + } return kStatusError; } return kStatusOk; @@ -687,21 +793,25 @@ DECODER_FUNC(jint, dav1dGetFrame, jlong jContext, jobject jOutputBuffer) { }; std::unique_ptr dav1d_picture( new Dav1dPicture(), cleanup_dav1d_picture); - context->libdav1d_status_code = + int 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); + { + std::lock_guard lock( // NOLINT(build/c++11) + context->state_mutex); + context->libdav1d_status_code = libdav1d_status_code; + } + if (libdav1d_status_code != 0 && libdav1d_status_code != DAV1D_ERR(EAGAIN)) { + LOGE("Failed to get picture. %d", libdav1d_status_code); return kStatusError; } - if (context->libdav1d_status_code == DAV1D_ERR(EAGAIN)) { + if (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()) { + if (ClearPendingException(env)) { context->jni_status_code = kJniStatusBufferInitError; return kStatusError; } @@ -714,11 +824,13 @@ DECODER_FUNC(jint, dav1dGetFrame, jlong jContext, jobject jOutputBuffer) { env->CallVoidMethod(jOutputBuffer, context->init_output_buffer_method, returned_user_data->time_us, returned_user_data->output_mode, nullptr); - if (env->ExceptionCheck()) { + if (ClearPendingException(env)) { context->jni_status_code = kJniStatusBufferInitError; return kStatusError; } if (dav1d_picture->p.bpc != 8) { + std::lock_guard lock( // NOLINT(build/c++11) + context->state_mutex); context->jni_status_code = kJniStatusHighBitDepthNotSupportedWithYuv; return kStatusError; } @@ -745,19 +857,30 @@ DECODER_FUNC(jint, dav1dGetFrame, jlong jContext, jobject jOutputBuffer) { GetColorSpace(dav1d_picture->seq_hdr->pri)); } if (!init_result) { + std::lock_guard lock( // NOLINT(build/c++11) + context->state_mutex); context->jni_status_code = kJniStatusBufferResizeError; return kStatusError; } - if (env->ExceptionCheck()) { - // Exception is thrown in Java when returning from the native call. + if (ClearPendingException(env)) { + std::lock_guard lock( // NOLINT(build/c++11) + context->state_mutex); context->jni_status_code = kJniStatusBufferResizeError; return kStatusError; } if (!context->use_custom_allocator) { const jobject data_object = env->GetObjectField(jOutputBuffer, context->data_field); + if (ClearPendingException(env)) { + LOGE("Failed to get data object from output buffer."); + return kStatusError; + } jbyte* const data = reinterpret_cast(env->GetDirectBufferAddress(data_object)); + if (data == nullptr) { + LOGE("Failed to get direct buffer address for output data."); + return kStatusError; + } CopyFrameToDataBuffer(dav1d_picture.get(), data); } } else if (returned_user_data->output_mode == kOutputModeSurfaceYuv) { @@ -765,8 +888,20 @@ DECODER_FUNC(jint, dav1dGetFrame, jlong jContext, jobject jOutputBuffer) { Dav1dPicture* dav1d_picture_raw_ptr = dav1d_picture.release(); env->SetLongField(jOutputBuffer, context->decoder_private_field, (uint64_t)dav1d_picture_raw_ptr); + if (ClearPendingException(env)) { + // If an exception occurred, the long field might not have been set. + // To avoid leaking dav1d_picture_raw_ptr, we unref it here. + dav1d_picture_unref(dav1d_picture_raw_ptr); + delete dav1d_picture_raw_ptr; + context->jni_status_code = kJniStatusBufferInitError; + return kStatusError; + } env->CallVoidMethod(jOutputBuffer, context->init_for_private_frame_method, dav1d_picture_raw_ptr->p.w, dav1d_picture_raw_ptr->p.h); + if (ClearPendingException(env)) { + context->jni_status_code = kJniStatusBufferInitError; + return kStatusError; + } } return kStatusOk; } @@ -778,14 +913,43 @@ DECODER_FUNC(jint, dav1dRenderFrame, jlong jContext, jobject jSurface, return kStatusError; } JniContext* const context = reinterpret_cast(jContext); + std::lock_guard lock( // NOLINT(build/c++11) + context->state_mutex); 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 = + if (context->native_window == nullptr) { + LOGE("Failed to render frame. native_window is null."); + context->jni_status_code = kJniStatusRenderNativeWindowNullError; + return kStatusError; + } + + Dav1dPicture* dav1d_picture = reinterpret_cast( + env->GetLongField(jOutputBuffer, context->decoder_private_field)); + if (ClearPendingException(env)) { + context->jni_status_code = kJniStatusBufferInitError; + return kStatusError; + } + if (dav1d_picture == nullptr) { + LOGE("Failed to get dav1d picture."); + context->jni_status_code = kJniStatusRenderPictureNullError; + return kStatusError; + } + + // Get the display width and height metadata from the output buffer. + int32_t width = env->GetIntField(jOutputBuffer, context->display_width_field); + if (ClearPendingException(env)) { + context->jni_status_code = kJniStatusANativeWindowError; + return kStatusError; + } + int32_t height = env->GetIntField(jOutputBuffer, context->display_height_field); + if (ClearPendingException(env)) { + context->jni_status_code = kJniStatusANativeWindowError; + return kStatusError; + } if (context->native_window_width != width || context->native_window_height != height) { if (ANativeWindow_setBuffersGeometry(context->native_window, width, height, @@ -802,22 +966,25 @@ DECODER_FUNC(jint, dav1dRenderFrame, jlong jContext, jobject jSurface, if (ANativeWindow_lock(context->native_window, &native_window_buffer, /*inOutDirtyBounds=*/nullptr) || native_window_buffer.bits == nullptr) { - context->jni_status_code = kJniStatusANativeWindowError; + context->jni_status_code = kJniStatusRenderLockFailedError; 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; - } + // Calculate the clamped copy dimensions to avoid out-of-bounds access. + int32_t y_copy_height = std::min((int32_t)dav1d_picture->p.h, + (int32_t)native_window_buffer.height); + y_copy_height = std::min(y_copy_height, (int32_t)height); + + int32_t y_copy_width = std::min((int32_t)dav1d_picture->p.w, + (int32_t)native_window_buffer.stride); + y_copy_width = std::min(y_copy_width, (int32_t)width); + // 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); + native_window_buffer.stride, y_copy_width, y_copy_height); const int y_plane_size = native_window_buffer.stride * native_window_buffer.height; @@ -826,7 +993,12 @@ DECODER_FUNC(jint, dav1dRenderFrame, jlong jContext, jobject jSurface, const int native_window_buffer_uv_stride = AlignTo16(native_window_buffer.stride / 2); - const int uv_plane_height = native_window_buffer_uv_height; + // UV planes + const int uv_copy_height = (y_copy_height + 1) / 2; + const int v_plane_size = + native_window_buffer_uv_height * native_window_buffer_uv_stride; + + const int uv_copy_width = (y_copy_width + 1) / 2; // TODO(b/140606738): Handle monochrome videos. // V plane @@ -836,16 +1008,14 @@ DECODER_FUNC(jint, dav1dRenderFrame, jlong jContext, jobject jSurface, 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; + native_window_buffer_uv_stride, uv_copy_width, uv_copy_height); // 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); + native_window_buffer_uv_stride, uv_copy_width, uv_copy_height); if (ANativeWindow_unlockAndPost(context->native_window)) { context->jni_status_code = kJniStatusANativeWindowError; @@ -860,13 +1030,20 @@ DECODER_FUNC(void, dav1dReleaseFrame, jlong jContext, jobject jOutputBuffer) { return; } JniContext* const context = reinterpret_cast(jContext); - Dav1dPicture* dav1d_picture = (Dav1dPicture*)env->GetLongField( - jOutputBuffer, context->decoder_private_field); + Dav1dPicture* dav1d_picture = reinterpret_cast( + env->GetLongField(jOutputBuffer, context->decoder_private_field)); + if (ClearPendingException(env)) { + return; + } env->SetLongField(jOutputBuffer, context->decoder_private_field, 0); + ClearPendingException(env); if (dav1d_picture != nullptr) { dav1d_picture_unref(dav1d_picture); delete dav1d_picture; } + if (context->use_custom_allocator) { + CleanUpAllocatorData(jContext, env); + } } DECODER_FUNC(jstring, dav1dGetErrorMessage, jlong jContext) { @@ -875,10 +1052,19 @@ DECODER_FUNC(jstring, dav1dGetErrorMessage, jlong jContext) { } JniContext* const context = reinterpret_cast(jContext); + std::lock_guard lock( // NOLINT(build/c++11) + context->state_mutex); + char error_message[256]; 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); + if (context->libdav1d_status_code == DAV1D_ERR(EINVAL)) { + snprintf(error_message, sizeof(error_message), + "There is a decoder error. %d. %s", + context->libdav1d_status_code, + GetJniErrorMessage(context->jni_status_code)); + } else { + 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) { @@ -887,11 +1073,21 @@ DECODER_FUNC(jstring, dav1dGetErrorMessage, jlong jContext) { return env->NewStringUTF("None."); } +DECODER_FUNC(jint, dav1dGetLastErrorJniStatusCode, jlong jContext) { + if (jContext == kStatusError) { + return kJniStatusOk; + } + JniContext* const context = reinterpret_cast(jContext); + return context->jni_status_code; +} + DECODER_FUNC(jint, dav1dCheckError, jlong jContext) { if (jContext == kStatusError) { return kStatusError; } JniContext* const context = reinterpret_cast(jContext); + std::lock_guard lock( // NOLINT(build/c++11) + context->state_mutex); return (context->libdav1d_status_code != kLibdav1dDecoderStatusOk || context->jni_status_code != kJniStatusOk) ? kStatusError @@ -904,6 +1100,9 @@ DECODER_FUNC(void, dav1dFlush, jlong jContext) { } JniContext* const context = reinterpret_cast(jContext); dav1d_flush(context->decoder); + if (context->use_custom_allocator) { + CleanUpAllocatorData(jContext, env); + } } DECODER_FUNC(void, releaseUnusedInputBuffers, jlong jContext, jobject decoder) { @@ -914,18 +1113,23 @@ DECODER_FUNC(void, releaseUnusedInputBuffers, jlong jContext, jobject decoder) { { std::lock_guard unused_cookies_lock( // NOLINT(build/c++11) 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(); + if (context->unused_cookies_count > 0) { + context->cookies_to_release.assign( + context->unused_cookies_array.get(), + context->unused_cookies_array.get() + context->unused_cookies_count); + context->unused_cookies_count = 0; + } + } + for (Cookie* cookie : context->cookies_to_release) { + env->CallVoidMethod(decoder, context->release_input_buffer_method, + cookie->buffer_index); + if (ClearPendingException(env)) { + LOGE("Failed to release input buffer."); } } + context->cookies_to_release.clear(); + + if (context->use_custom_allocator) { + CleanUpAllocatorData(jContext, env); + } } diff --git a/libraries/decoder_ffmpeg/README.md b/libraries/decoder_ffmpeg/README.md index 4af789415f9..96f0cb2d4e3 100644 --- a/libraries/decoder_ffmpeg/README.md +++ b/libraries/decoder_ffmpeg/README.md @@ -49,35 +49,25 @@ HOST_PLATFORM="linux-x86_64" ANDROID_ABI=21 ``` -* Fetch FFmpeg and checkout an appropriate branch. We cannot guarantee - compatibility with all versions of FFmpeg. We currently recommend version - 6.0: - -``` -cd "" && \ -git clone git://source.ffmpeg.org/ffmpeg && \ -cd ffmpeg && \ -git checkout release/6.0 && \ -FFMPEG_PATH="$(pwd)" -``` - -* Configure the decoders to include. See the [Supported formats][] page for - details of the available decoders, and which formats they support. +* Configure the decoders to include. See the [Supported formats][] page for + details of the available decoders, and which formats they support. ``` ENABLED_DECODERS=(vorbis opus flac) ``` -* Add a link to the FFmpeg source code in the FFmpeg module `jni` directory. +* Fetch FFmpeg and checkout an appropriate branch. We cannot guarantee + compatibility with all versions of FFmpeg. We currently recommend version + 6.0: -``` +```shell cd "${FFMPEG_MODULE_PATH}/jni" && \ -ln -s "$FFMPEG_PATH" ffmpeg +git clone git://source.ffmpeg.org/ffmpeg --branch=release/6.0 --depth=1 ``` -* Execute `build_ffmpeg.sh` to build FFmpeg for `armeabi-v7a`, `arm64-v8a`, - `x86` and `x86_64`. The script can be edited if you need to build for - different architectures: +* Execute `build_ffmpeg.sh` to build FFmpeg for `armeabi-v7a`, `arm64-v8a`, + `x86` and `x86_64`. The script can be edited if you need to build for + different architectures: ``` cd "${FFMPEG_MODULE_PATH}/jni" && \ diff --git a/libraries/decoder_ffmpeg/build.gradle b/libraries/decoder_ffmpeg/build.gradle index ddba0a7abab..accf03ca6a5 100644 --- a/libraries/decoder_ffmpeg/build.gradle +++ b/libraries/decoder_ffmpeg/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.decoder.ffmpeg' diff --git a/libraries/decoder_flac/README.md b/libraries/decoder_flac/README.md index 5bb24c8b11a..d09c4ffd4f2 100644 --- a/libraries/decoder_flac/README.md +++ b/libraries/decoder_flac/README.md @@ -17,20 +17,12 @@ 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 libflac as follows: +In addition, it's necessary to fetch libflac as follows (starting at the root of +your media3 check-out): -* Set the following environment variables: - -``` -cd "" -FLAC_MODULE_PATH="$(pwd)/libraries/decoder_flac/src/main" -``` - -* Fetch libflac: - -``` -cd "${FLAC_MODULE_PATH}/jni" && \ -git clone https://github.com/xiph/flac.git libflac +```shell +cd libraries/decoder_flac/src/main/jni && \ +git clone https://github.com/xiph/flac.git --depth=1 libflac ``` * [Install CMake][] diff --git a/libraries/decoder_flac/build.gradle b/libraries/decoder_flac/build.gradle index 2867957a909..45bc16f14f6 100644 --- a/libraries/decoder_flac/build.gradle +++ b/libraries/decoder_flac/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.decoder.flac' diff --git a/libraries/decoder_flac/src/androidTest/java/androidx/media3/decoder/flac/FlacExtractorTest.java b/libraries/decoder_flac/src/androidTest/java/androidx/media3/decoder/flac/FlacExtractorTest.java index c04365fc212..6b33007ecb9 100644 --- a/libraries/decoder_flac/src/androidTest/java/androidx/media3/decoder/flac/FlacExtractorTest.java +++ b/libraries/decoder_flac/src/androidTest/java/androidx/media3/decoder/flac/FlacExtractorTest.java @@ -110,6 +110,17 @@ public void sampleWithVorbisComments() throws Exception { simulationConfig); } + @Test + public void sampleWithVorbisCommentsInvalidMutf8() throws Exception { + ExtractorAsserts.assertBehavior( + FlacExtractor::new, + "media/flac/bear_with_vorbis_comments_invalid_mutf8.flac", + new AssertionConfig.Builder() + .setDumpFilesPrefix("extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw") + .build(), + simulationConfig); + } + @Test public void sampleWithPicture() throws Exception { ExtractorAsserts.assertBehavior( diff --git a/libraries/decoder_flac/src/main/jni/flac_jni.cc b/libraries/decoder_flac/src/main/jni/flac_jni.cc index 688b0e6d19f..b7a19388f78 100644 --- a/libraries/decoder_flac/src/main/jni/flac_jni.cc +++ b/libraries/decoder_flac/src/main/jni/flac_jni.cc @@ -84,6 +84,29 @@ struct Context { } }; +static jstring createSafeString(JNIEnv* env, const std::string& str, + jclass stringClass, jmethodID stringConstructor, + jstring charsetName) { + if (env->PushLocalFrame(10) < 0) { + return nullptr; + } + jbyteArray bytes = env->NewByteArray(str.size()); + if (!bytes || env->ExceptionCheck()) { + return static_cast(env->PopLocalFrame(nullptr)); + } + env->SetByteArrayRegion(bytes, 0, str.size(), + reinterpret_cast(str.data())); + if (env->ExceptionCheck()) { + return static_cast(env->PopLocalFrame(nullptr)); + } + jobject result = + env->NewObject(stringClass, stringConstructor, bytes, charsetName); + if (env->ExceptionCheck()) { + return static_cast(env->PopLocalFrame(nullptr)); + } + return static_cast(env->PopLocalFrame(result)); +} + DECODER_FUNC(jlong, flacInit) { Context* context = new Context; if (!context->parser->init()) { @@ -97,7 +120,21 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { Context* context = reinterpret_cast(jContext); context->source->setFlacDecoderJni(env, thiz); if (!context->parser->decodeMetadata()) { - return NULL; + return nullptr; + } + + jclass stringClass = env->FindClass("java/lang/String"); + if (!stringClass || env->ExceptionCheck()) { + return nullptr; + } + jmethodID stringConstructor = + env->GetMethodID(stringClass, "", "([BLjava/lang/String;)V"); + if (!stringConstructor || env->ExceptionCheck()) { + return nullptr; + } + jstring charsetName = env->NewStringUTF("UTF-8"); + if (!charsetName || env->ExceptionCheck()) { + return nullptr; } jclass arrayListClass = env->FindClass("java/util/ArrayList"); @@ -113,9 +150,12 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { for (std::vector::const_iterator vorbisComment = vorbisComments.begin(); vorbisComment != vorbisComments.end(); ++vorbisComment) { - jstring commentString = env->NewStringUTF((*vorbisComment).c_str()); - env->CallBooleanMethod(commentList, arrayListAddMethod, commentString); - env->DeleteLocalRef(commentString); + jstring commentString = createSafeString(env, *vorbisComment, stringClass, + stringConstructor, charsetName); + if (commentString != nullptr) { + env->CallBooleanMethod(commentList, arrayListAddMethod, commentString); + env->DeleteLocalRef(commentString); + } } } @@ -130,9 +170,29 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { "(ILjava/lang/String;Ljava/lang/String;IIII[B)V"); for (std::vector::const_iterator picture = pictures.begin(); picture != pictures.end(); ++picture) { - jstring mimeType = env->NewStringUTF(picture->mimeType.c_str()); - jstring description = env->NewStringUTF(picture->description.c_str()); + jstring mimeType = createSafeString(env, picture->mimeType, stringClass, + stringConstructor, charsetName); + if (mimeType == nullptr) { + if (env->ExceptionCheck()) return nullptr; + continue; + } + + jstring description = + createSafeString(env, picture->description, stringClass, + stringConstructor, charsetName); + if (description == nullptr) { + env->DeleteLocalRef(mimeType); + if (env->ExceptionCheck()) return nullptr; + continue; + } + jbyteArray pictureData = env->NewByteArray(picture->data.size()); + if (pictureData == nullptr) { + env->DeleteLocalRef(mimeType); + env->DeleteLocalRef(description); + if (env->ExceptionCheck()) return nullptr; + continue; + } env->SetByteArrayRegion(pictureData, 0, picture->data.size(), (signed char*)&picture->data[0]); jobject pictureFrame = env->NewObject( diff --git a/libraries/decoder_iamf/README.md b/libraries/decoder_iamf/README.md index 382ce9e8547..698e796d27d 100644 --- a/libraries/decoder_iamf/README.md +++ b/libraries/decoder_iamf/README.md @@ -40,11 +40,25 @@ In addition, it's necessary to fetch iamf_tools with dependencies as follows: ```shell cd "${IAMF_MODULE_PATH}/jni" && \ - git clone https://github.com/AOMediaCodec/iamf-tools.git iamf_tools && \ - cd iamf_tools && \ - git reset --hard de364b983 + git clone https://github.com/AOMediaCodec/iamf-tools.git \ + --depth=1 \ + --revision=de364b983447a45d8be81f9172eea422c139dcf0 \ + iamf_tools ``` + * If you are using an old version of `git` without the `--revision` flag + (added in 2.49), run this instead: + + ```shell + cd "${IAMF_MODULE_PATH}/jni" && \ + mkdir iamf_tools && \ + cd iamf_tools && \ + git init && \ + git remote add origin https://github.com/AOMediaCodec/iamf-tools.git \ + git fetch --depth=1 origin de364b983447a45d8be81f9172eea422c139dcf0 && \ + git checkout FETCH_HEAD + ``` + * Build the library. ```shell diff --git a/libraries/decoder_iamf/build.gradle b/libraries/decoder_iamf/build.gradle index f072a867d3e..b4ee4693ca0 100644 --- a/libraries/decoder_iamf/build.gradle +++ b/libraries/decoder_iamf/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.decoder.iamf' diff --git a/libraries/decoder_iamf/src/androidTest/java/androidx/media3/decoder/iamf/IamfAudioRendererTest.java b/libraries/decoder_iamf/src/androidTest/java/androidx/media3/decoder/iamf/IamfAudioRendererTest.java new file mode 100644 index 00000000000..70f1252cc18 --- /dev/null +++ b/libraries/decoder_iamf/src/androidTest/java/androidx/media3/decoder/iamf/IamfAudioRendererTest.java @@ -0,0 +1,167 @@ +/* + * Copyright 2026 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.iamf; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.media.AudioFormat; +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.exoplayer.audio.AudioCapabilities; +import androidx.media3.exoplayer.audio.DefaultAudioSink; +import androidx.media3.exoplayer.audio.ForwardingAudioSink; +import androidx.media3.exoplayer.audio.IamfUtil; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests different configurations of {@link IamfAudioRenderer}. */ +@RunWith(AndroidJUnit4.class) +public final class IamfAudioRendererTest { + + // Sample configOBUs data from sample_iamf.mp4 file. + private static final byte[] iacbObus = { + -8, 6, 105, 97, 109, 102, 0, 0, 0, 15, -56, 1, 105, 112, 99, 109, 64, 0, 0, 1, 16, 0, 0, 62, + -128, 8, 12, -84, 2, 0, -56, 1, 1, 0, 0, 32, 16, 1, 1, 16, 78, 42, 1, 101, 110, 45, 117, 115, 0, + 116, 101, 115, 116, 95, 109, 105, 120, 95, 112, 114, 101, 115, 0, 1, 1, -84, 2, 116, 101, 115, + 116, 95, 115, 117, 98, 95, 109, 105, 120, 95, 48, 95, 97, 117, 100, 105, 111, 95, 101, 108, 101, + 109, 101, 110, 116, 95, 48, 0, 0, 0, 100, -128, 125, -128, 0, 0, 100, -128, 125, -128, 0, 0, 1, + -128, 0, -54, 81, -51, -79 + }; + + @Test + @SuppressWarnings("ForOverride") + public void createDecoder_basicCase() throws Exception { + AudioCapabilitiesAudioSink audioSink = + new AudioCapabilitiesAudioSink( + ApplicationProvider.getApplicationContext(), + AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES); // Stereo speakers. + IamfAudioRenderer renderer = new IamfAudioRenderer.Builder(audioSink).build(); + + IamfDecoder decoder = + renderer.createDecoder( + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_IAMF) + .setInitializationData(ImmutableList.of(iacbObus)) + .build(), + /* cryptoConfig= */ null); + + assertThat(decoder.getSelectedOutputLayout()) + .isEqualTo(IamfUtil.OUTPUT_LAYOUT_ITU2051_SOUND_SYSTEM_A_0_2_0); + Format outputFormat = renderer.getOutputFormat(decoder); + assertThat(outputFormat.channelCount).isEqualTo(2); + } + + @Test + @SuppressWarnings("ForOverride") + public void createDecoder_acceptsRequestedOutputLayout() throws Exception { + AudioCapabilitiesAudioSink audioSink = + new AudioCapabilitiesAudioSink( + ApplicationProvider.getApplicationContext(), + AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES); + IamfAudioRenderer renderer = + new IamfAudioRenderer.Builder(audioSink) + .setRequestedOutputLayout(IamfUtil.OUTPUT_LAYOUT_IAMF_SOUND_SYSTEM_EXTENSION_2_3_0) + .build(); + + IamfDecoder decoder = + renderer.createDecoder( + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_IAMF) + .setInitializationData(ImmutableList.of(iacbObus)) + .build(), + /* cryptoConfig= */ null); + + assertThat(decoder.getSelectedOutputLayout()) + .isEqualTo(IamfUtil.OUTPUT_LAYOUT_IAMF_SOUND_SYSTEM_EXTENSION_2_3_0); + } + + // IamfAudioRenderer uses built-in binaural when the Spatializer channel masks are non-empty and + // enable_integrated_binaural is true (the default value). + @Test + @SuppressWarnings("ForOverride") + public void createDecoder_usesBuiltInBinauralWhenAppropriate() throws Exception { + AudioCapabilities audioCapabilities = + getAudioCapabilitiesWithSpatializerMasks(ImmutableList.of(AudioFormat.CHANNEL_OUT_5POINT1)); + AudioCapabilitiesAudioSink audioSink = + new AudioCapabilitiesAudioSink( + ApplicationProvider.getApplicationContext(), audioCapabilities); + IamfAudioRenderer renderer = new IamfAudioRenderer.Builder(audioSink).build(); + + IamfDecoder decoder = + renderer.createDecoder( + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_IAMF) + .setInitializationData(ImmutableList.of(iacbObus)) + .build(), + /* cryptoConfig= */ null); + + assertThat(decoder.getSelectedOutputLayout()).isEqualTo(IamfUtil.OUTPUT_LAYOUT_BINAURAL); + } + + @Test + @SuppressWarnings("ForOverride") + public void createDecoder_usesSpatializerWhenIntegratedBinauralDisabled() throws Exception { + AudioCapabilities audioCapabilities = + getAudioCapabilitiesWithSpatializerMasks(ImmutableList.of(AudioFormat.CHANNEL_OUT_5POINT1)); + AudioCapabilitiesAudioSink audioSink = + new AudioCapabilitiesAudioSink( + ApplicationProvider.getApplicationContext(), audioCapabilities); + IamfAudioRenderer renderer = + new IamfAudioRenderer.Builder(audioSink).setEnableIntegratedBinaural(false).build(); + + IamfDecoder decoder = + renderer.createDecoder( + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_IAMF) + .setInitializationData(ImmutableList.of(iacbObus)) + .build(), + /* cryptoConfig= */ null); + + assertThat(decoder.getSelectedOutputLayout()) + .isEqualTo(IamfUtil.OUTPUT_LAYOUT_ITU2051_SOUND_SYSTEM_B_0_5_0); + } + + /** Helper just to get AudioCapabilities with non-empty spatializer channel masks. */ + private static AudioCapabilities getAudioCapabilitiesWithSpatializerMasks( + ImmutableList spatializerChannelMasks) { + return AudioCapabilities.getCapabilities( + ApplicationProvider.getApplicationContext(), + AudioAttributes.DEFAULT, + /* routedDevice= */ null, + spatializerChannelMasks); + } + + /** Wraps a {@link DefaultAudioSink} that allows us to set the {@link AudioCapabilities}. */ + private static class AudioCapabilitiesAudioSink extends ForwardingAudioSink { + + private final AudioCapabilities audioCapabilities; + + private AudioCapabilitiesAudioSink(Context context, AudioCapabilities audioCapabilities) { + super(new DefaultAudioSink.Builder(context).build()); + this.audioCapabilities = audioCapabilities; + } + + @Override + public AudioCapabilities getAudioCapabilities() { + return audioCapabilities; + } + } +} diff --git a/libraries/decoder_iamf/src/androidTest/java/androidx/media3/decoder/iamf/IamfPlaybackTest.java b/libraries/decoder_iamf/src/androidTest/java/androidx/media3/decoder/iamf/IamfPlaybackTest.java index 6c09dc95fa6..2ace35b1bae 100644 --- a/libraries/decoder_iamf/src/androidTest/java/androidx/media3/decoder/iamf/IamfPlaybackTest.java +++ b/libraries/decoder_iamf/src/androidTest/java/androidx/media3/decoder/iamf/IamfPlaybackTest.java @@ -127,7 +127,7 @@ public void run() { textRendererOutput, metadataRendererOutput) -> new Renderer[] { - new IamfAudioRenderer.Builder(context, audioSink) + new IamfAudioRenderer.Builder(audioSink) .setEventHandlerAndListener(eventHandler, audioRendererEventListener) .setRequestedOutputLayout(IamfUtil.OUTPUT_LAYOUT_ITU2051_SOUND_SYSTEM_A_0_2_0) .build() diff --git a/libraries/decoder_iamf/src/main/java/androidx/media3/decoder/iamf/IamfAudioRenderer.java b/libraries/decoder_iamf/src/main/java/androidx/media3/decoder/iamf/IamfAudioRenderer.java index ccb245662f1..3b837f1e014 100644 --- a/libraries/decoder_iamf/src/main/java/androidx/media3/decoder/iamf/IamfAudioRenderer.java +++ b/libraries/decoder_iamf/src/main/java/androidx/media3/decoder/iamf/IamfAudioRenderer.java @@ -28,6 +28,7 @@ import androidx.media3.common.util.Util; import androidx.media3.decoder.CryptoConfig; import androidx.media3.decoder.DecoderException; +import androidx.media3.exoplayer.audio.AudioCapabilities; import androidx.media3.exoplayer.audio.AudioRendererEventListener; import androidx.media3.exoplayer.audio.AudioSink; import androidx.media3.exoplayer.audio.DecoderAudioRenderer; @@ -41,7 +42,6 @@ public class IamfAudioRenderer extends DecoderAudioRenderer { /** Builds {@link IamfAudioRenderer} instances. */ public static final class Builder { - private final Context context; private final AudioSink audioSink; @Nullable private Handler eventHandler; @Nullable private AudioRendererEventListener eventListener; @@ -51,14 +51,20 @@ public static final class Builder { private @IamfDecoder.ChannelOrdering int channelOrdering; private boolean enableIntegratedBinaural; + /** + * @deprecated Use {@link #Builder(AudioSink)} instead. + */ + @Deprecated + public Builder(Context context, AudioSink audioSink) { + this(audioSink); + } + /** * Creates a builder. * - * @param context The {@link Context}. * @param audioSink The sink to which audio will be output. */ - public Builder(Context context, AudioSink audioSink) { - this.context = context; + public Builder(AudioSink audioSink) { this.audioSink = audioSink; this.requestedOutputLayout = IamfUtil.OUTPUT_LAYOUT_UNSET; this.requestedMixPresentationId = IamfUtil.REQUESTED_MIX_PRESENTATION_ID_UNSET; @@ -128,10 +134,11 @@ public Builder setRequestedMixPresentationId(long requestedMixPresentationId) { /** * Enables or disables binaural rendering within the IAMF decoder. Default {@code true}. * - *

This setting controls the behaviour when we believe the user is using headphones. If - * {@code true}, the IAMF decoder can output pre-rendered binaural audio. If this is {@code - * false}, the {@link IamfAudioRenderer} will instead produce output appropriate for the Android - * {@link android.media.Spatializer}. + *

This setting controls the behaviour only when we believe the user is using headphones. If + * the user is using headphones and this setting is {@code true}, the decoder will produce + * two-channel binaural output directly. If {@code false}, the decoder will produce output to + * match a channel mask from the {@link AudioCapabilities#getSpatializerChannelMasks()} to be + * rendered by for headphones by the {@link android.media.Spatializer}. * *

The IAMF binaural renderer may be higher fidelity because the audio content of the IAMF * stream can be rendered directly to binaural audio, without first producing an intermediate @@ -154,22 +161,21 @@ public IamfAudioRenderer build() { } private static final String TAG = "IamfAudioRenderer"; - private final Context context; + private final AudioSink audioSink; private final @IamfDecoder.OutputSampleType int outputSampleType; private final @IamfDecoder.ChannelOrdering int channelOrdering; private final @IamfUtil.OutputLayout int requestedOutputLayout; - private final @IamfUtil.OutputLayout int currentOutputLayout; private final long requestedMixPresentationId; + private final boolean enableIntegratedBinaural; private IamfAudioRenderer(Builder builder) { super(builder.eventHandler, builder.eventListener, builder.audioSink); - this.context = builder.context; + this.audioSink = builder.audioSink; this.outputSampleType = builder.outputSampleType; this.channelOrdering = builder.channelOrdering; this.requestedOutputLayout = builder.requestedOutputLayout; this.requestedMixPresentationId = builder.requestedMixPresentationId; - this.currentOutputLayout = - determineOutputLayout(context, requestedOutputLayout, builder.enableIntegratedBinaural); + this.enableIntegratedBinaural = builder.enableIntegratedBinaural; } @Override @@ -184,6 +190,7 @@ protected int supportsFormatInternal(Format format) { protected IamfDecoder createDecoder(Format format, @Nullable CryptoConfig cryptoConfig) throws DecoderException { TraceUtil.beginSection("createIamfDecoder"); + @IamfUtil.OutputLayout int currentOutputLayout = determineOutputLayout(); IamfDecoder decoder = new IamfDecoder( format.initializationData, @@ -214,6 +221,22 @@ public String getName() { return "IamfAudioRenderer"; } + private @IamfUtil.OutputLayout int determineOutputLayout() { + // If the user has requested a specific output layout, use that. + if (requestedOutputLayout != IamfUtil.OUTPUT_LAYOUT_UNSET) { + return requestedOutputLayout; + } + AudioCapabilities audioCapabilities = audioSink.getAudioCapabilities(); + if (audioCapabilities == null) { + Log.w( + TAG, + "AudioCapabilities from the AudioSink are null, using default stereo output layout."); + return IamfUtil.OUTPUT_LAYOUT_ITU2051_SOUND_SYSTEM_A_0_2_0; // Default to stereo. + } + return IamfUtil.getOutputLayoutForCurrentConfiguration( + audioCapabilities, enableIntegratedBinaural); + } + private static int getEncodingForSampleType(@IamfDecoder.OutputSampleType int sampleType) { switch (sampleType) { case IamfDecoder.OUTPUT_SAMPLE_TYPE_INT16_LITTLE_ENDIAN: @@ -224,15 +247,4 @@ private static int getEncodingForSampleType(@IamfDecoder.OutputSampleType int sa throw new IllegalArgumentException("Unsupported sample type: " + sampleType); } } - - private static @IamfUtil.OutputLayout int determineOutputLayout( - Context context, - @IamfUtil.OutputLayout int requestedOutputLayout, - boolean enableIntegratedBinaural) { - // If the user has requested a specific output layout, use that. - if (requestedOutputLayout != IamfUtil.OUTPUT_LAYOUT_UNSET) { - return requestedOutputLayout; - } - return IamfUtil.getOutputLayoutForCurrentConfiguration(context, enableIntegratedBinaural); - } } diff --git a/libraries/decoder_iamf/src/main/jni/build_iamf_tools.sh b/libraries/decoder_iamf/src/main/jni/build_iamf_tools.sh index d8de1a5f17b..53d40a00138 100755 --- a/libraries/decoder_iamf/src/main/jni/build_iamf_tools.sh +++ b/libraries/decoder_iamf/src/main/jni/build_iamf_tools.sh @@ -31,7 +31,7 @@ echo "Cleaned old pre-built libraries." # settings in files such as iamf_tools/.bazelrc. cd ${IAMF_MODULE_PATH}/jni/iamf_tools -if ! command -v bazelisk > /dev/null 2 >&1 +if ! command -v bazelisk > /dev/null 2>&1 then echo "bazelisk required but not found. Install and try again." exit 1 @@ -52,7 +52,6 @@ for config in android_armv7 android_arm64 android_x86_32 android_x86_64; do --copt=-fvisibility=hidden \ --copt=-fno-exceptions \ --copt=-fno-rtti \ - --features=thin_lto \ //iamf/include/iamf_tools:iamf_decoder_static ABI="${CONFIG_TO_ABI_MAP[$config]}" OUTPUT_DIR="${IAMF_MODULE_PATH}/jni/nativelib/${ABI}" diff --git a/libraries/decoder_midi/build.gradle b/libraries/decoder_midi/build.gradle index 394087aa01c..105c0e7adee 100644 --- a/libraries/decoder_midi/build.gradle +++ b/libraries/decoder_midi/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.decoder.midi' diff --git a/libraries/decoder_mpegh/README.md b/libraries/decoder_mpegh/README.md index 5d4e1cbe01a..c63a5e15409 100644 --- a/libraries/decoder_mpegh/README.md +++ b/libraries/decoder_mpegh/README.md @@ -19,20 +19,15 @@ 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 libmpegh library as follows: - -* Set the following environment variables: - -``` -cd "" -MPEGH_MODULE_PATH="$(pwd)/libraries/decoder_mpegh/src/main" -``` - -* Fetch libmpegh library: - -``` -cd "${MPEGH_MODULE_PATH}/jni" && \ -git clone https://github.com/Fraunhofer-IIS/mpeghdec.git --branch r3.0.2 libmpegh +In addition, it's necessary to fetch libmpegh library as follows (starting at +the root of your media3 check-out): + +```shell +cd libraries/decoder_mpegh/src/main/jni && \ +git clone https://github.com/Fraunhofer-IIS/mpeghdec.git \ + --branch r3.0.2 \ + --depth=1 \ + libmpegh ``` * [Install CMake][]. diff --git a/libraries/decoder_mpegh/build.gradle b/libraries/decoder_mpegh/build.gradle index d73c7fee35c..17069adffe6 100644 --- a/libraries/decoder_mpegh/build.gradle +++ b/libraries/decoder_mpegh/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.decoder.mpegh' diff --git a/libraries/decoder_opus/README.md b/libraries/decoder_opus/README.md index 5b01e655a9f..8fa5cc08d2b 100644 --- a/libraries/decoder_opus/README.md +++ b/libraries/decoder_opus/README.md @@ -17,20 +17,12 @@ 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 libopus as follows: +In addition, it's necessary to fetch libopus as follows (starting at the root of +your media3 check-out): -* Set the following environment variables: - -``` -cd "" -OPUS_MODULE_PATH="$(pwd)/libraries/decoder_opus/src/main" -``` - -* Fetch libopus: - -``` -cd "${OPUS_MODULE_PATH}/jni" && \ -git clone https://gitlab.xiph.org/xiph/opus.git libopus +```shell +cd libraries/decoder_opus/src/main/jni && \ +git clone https://github.com/xiph/opus.git --depth=1 libopus ``` * [Install CMake][] diff --git a/libraries/decoder_opus/build.gradle b/libraries/decoder_opus/build.gradle index dadb69d888b..e3c7cfc8e6f 100644 --- a/libraries/decoder_opus/build.gradle +++ b/libraries/decoder_opus/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.decoder.opus' diff --git a/libraries/decoder_opus/src/androidTest/java/androidx/media3/decoder/opus/OpusPlaybackTest.java b/libraries/decoder_opus/src/androidTest/java/androidx/media3/decoder/opus/OpusPlaybackTest.java index 897118c7932..bb4cbbcad28 100644 --- a/libraries/decoder_opus/src/androidTest/java/androidx/media3/decoder/opus/OpusPlaybackTest.java +++ b/libraries/decoder_opus/src/androidTest/java/androidx/media3/decoder/opus/OpusPlaybackTest.java @@ -38,10 +38,12 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; /** Playback tests using {@link LibopusAudioRenderer}. */ +@Ignore("Fails on gradle: b/396659095") @RunWith(AndroidJUnit4.class) public class OpusPlaybackTest { diff --git a/libraries/decoder_vp9/README.md b/libraries/decoder_vp9/README.md index 42966b24683..cc872e32973 100644 --- a/libraries/decoder_vp9/README.md +++ b/libraries/decoder_vp9/README.md @@ -21,7 +21,7 @@ In addition, it's necessary to build the module's native components as follows: * Set the following environment variables: -``` +```shell cd "" VP9_MODULE_PATH="$(pwd)/libraries/decoder_vp9/src/main" ``` @@ -33,23 +33,15 @@ VP9_MODULE_PATH="$(pwd)/libraries/decoder_vp9/src/main" NDK_PATH="" ``` -* Fetch an appropriate branch of libvpx. We cannot guarantee compatibility - with all versions of libvpx. We currently recommend version 1.15.2: - -``` -cd "" && \ -git clone https://chromium.googlesource.com/webm/libvpx && \ -cd libvpx && \ -git checkout tags/v1.15.2 -b v1.15.2 && \ -LIBVPX_PATH="$(pwd)" -``` - -* Add a link to the libvpx source code in the vp9 module `jni` directory and - run a script that generates necessary configuration files for libvpx: +* Fetch an appropriate branch of libvpx and run a script that generates + necessary configuration files for libvpx. We cannot guarantee compatibility + with all versions of libvpx. We currently recommend version 1.15.2: -``` +```shell cd ${VP9_MODULE_PATH}/jni && \ -ln -s "$LIBVPX_PATH" libvpx && \ +git clone https://chromium.googlesource.com/webm/libvpx \ + --branch v1.15.2 \ + --depth=1 && \ ./generate_libvpx_android_configs.sh ``` @@ -72,14 +64,14 @@ be possible to follow the Linux instructions in [Windows PowerShell][]. ## Notes -* Every time there is a change to the libvpx checkout: - * Android config scripts should be re-generated by running - `generate_libvpx_android_configs.sh` - * Clean and re-build the project. -* If you want to use your own version of libvpx, point to it with the - `${VP9_MODULE_PATH}/jni/libvpx` symlink. Please note that - `generate_libvpx_android_configs.sh` and the makefiles may need to be modified - to work with arbitrary versions of libvpx. +* Every time there is a change to the libvpx checkout: + * Android config scripts should be re-generated by running + `generate_libvpx_android_configs.sh` + * Clean and re-build the project. +* If you want to use your own version of libvpx located outside your media3 + repository, add `${VP9_MODULE_PATH}/jni/libvpx` as a symlink to it instead + of a simple directory. `generate_libvpx_android_configs.sh` and the + makefiles may need to be modified to work with arbitrary versions of libvpx. ## Using the module with ExoPlayer diff --git a/libraries/decoder_vp9/build.gradle b/libraries/decoder_vp9/build.gradle index b4e5b24d3ce..9c03863a869 100644 --- a/libraries/decoder_vp9/build.gradle +++ b/libraries/decoder_vp9/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.decoder.vp9' diff --git a/libraries/effect/build.gradle b/libraries/effect/build.gradle index 46099662cb8..8bff57855ee 100644 --- a/libraries/effect/build.gradle +++ b/libraries/effect/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" apply plugin: 'kotlin-android' android { namespace 'androidx.media3.effect' diff --git a/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTaskExecutor.java b/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTaskExecutor.java index f7589c97aae..7e06d48fb26 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTaskExecutor.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/VideoFrameProcessingTaskExecutor.java @@ -15,6 +15,7 @@ */ package androidx.media3.effect; +import static androidx.media3.common.util.Util.isRunningOnEmulator; import static com.google.common.base.Preconditions.checkState; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -62,7 +63,7 @@ interface ErrorListener { void onError(VideoFrameProcessingException exception); } - private static final long EXECUTOR_SERVICE_TIMEOUT_MS = 500; + private static final long EXECUTOR_SERVICE_TIMEOUT_MS = isRunningOnEmulator() ? 5000 : 500; private final boolean shouldShutdownExecutorService; private final ExecutorService singleThreadExecutorService; diff --git a/libraries/effect_lottie/build.gradle b/libraries/effect_lottie/build.gradle index e1ac83ebf00..d634ec78736 100644 --- a/libraries/effect_lottie/build.gradle +++ b/libraries/effect_lottie/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.effect.lottie' diff --git a/libraries/effect_ndk/README.md b/libraries/effect_ndk/README.md deleted file mode 100644 index df28043f5bd..00000000000 --- a/libraries/effect_ndk/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Effect NDK module - -Provides functionality for `lib-effect`, that depends on native code. - -## Getting the module - -The easiest way to get the module is to add it as a gradle dependency: - -```groovy -implementation 'androidx.media3:media3-effect-ndk:1.X.X' -``` - -where `1.X.X` is the version, which must match the version of the other media -modules being used. - -Alternatively, you can clone this GitHub project and depend on the module -locally. Instructions for doing this can be found in the [top level README][]. - -[top level README]: ../../README.md - -## Links - -* [Javadoc][] - -[Javadoc]: https://developer.android.com/reference/androidx/media3/effect/ndk/package-summary diff --git a/libraries/effect_ndk/build.gradle b/libraries/effect_ndk/build.gradle deleted file mode 100644 index 5ca946c90a2..00000000000 --- a/libraries/effect_ndk/build.gradle +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2026 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.effect.ndk' - - buildTypes { - debug { - testCoverageEnabled = true - } - } - - kotlinOptions { - jvmTarget = '1.8' - } - - defaultConfig { - minSdkVersion project.ext.minSdkVersion - externalNativeBuild { - cmake { - arguments.add("-DANDROID_WEAK_API_DEFS=ON") - arguments.add("-Werror=unguarded-availability") - targets "hardwareBufferJNI" - } - } - } - externalNativeBuild { - cmake { - path 'src/main/jni/CMakeLists.txt' - } - } - - sourceSets { - androidTest.assets.srcDir '../test_data/src/test/assets/' - test.assets.srcDir '../test_data/src/test/assets/' - } - - publishing { - singleVariant('release') { - withSourcesJar() - } - } -} - -dependencies { - implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion - api project(modulePrefix + 'lib-common') - api project(modulePrefix + 'lib-effect') - api project(modulePrefix + 'lib-transformer') - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:" + kotlinxCoroutinesVersion - compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion - androidTestImplementation 'junit:junit:' + junitVersion - androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion - androidTestImplementation 'com.google.truth:truth:' + truthVersion - androidTestImplementation project(modulePrefix + 'test-utils') - androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:" + kotlinxCoroutinesVersion -} - -ext { - releaseArtifactId = 'media3-effect-ndk' - releaseName = 'Media3 Effect NDK module' -} -apply from: '../../publish.gradle' diff --git a/libraries/effect_ndk/src/androidTest/AndroidManifest.xml b/libraries/effect_ndk/src/androidTest/AndroidManifest.xml deleted file mode 100644 index 90c1e69335d..00000000000 --- a/libraries/effect_ndk/src/androidTest/AndroidManifest.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - diff --git a/libraries/effect_ndk/src/androidTest/assets/first_frame.png b/libraries/effect_ndk/src/androidTest/assets/first_frame.png deleted file mode 100644 index 623bb5ac937..00000000000 Binary files a/libraries/effect_ndk/src/androidTest/assets/first_frame.png and /dev/null differ diff --git a/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/BitmapToHardwareBufferProcessorTest.java b/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/BitmapToHardwareBufferProcessorTest.java deleted file mode 100644 index ba180ee5149..00000000000 --- a/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/BitmapToHardwareBufferProcessorTest.java +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Copyright 2026 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.effect.ndk; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.junit.Assert.assertThrows; - -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.ColorSpace; -import android.hardware.HardwareBuffer; -import android.opengl.EGLContext; -import android.opengl.EGLDisplay; -import android.opengl.EGLSurface; -import android.opengl.GLES20; -import android.os.Build; -import androidx.annotation.RequiresApi; -import androidx.media3.common.util.GlUtil; -import androidx.media3.effect.HardwareBufferFrame; -import androidx.media3.test.utils.BitmapPixelTestUtil; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SdkSuppress; -import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestName; -import org.junit.runner.RunWith; - -/** Instrumentation tests for {@link BitmapToHardwareBufferProcessor}. */ -@RunWith(AndroidJUnit4.class) -public final class BitmapToHardwareBufferProcessorTest { - private static final float MAX_AVG_PIXEL_DIFFERENCE = 1.0f; - - private @MonotonicNonNull ExecutorService executorService; - private @MonotonicNonNull BitmapToHardwareBufferProcessor processor; - - @Rule public final TestName testName = new TestName(); - - @Before - public void setUp() { - executorService = Executors.newSingleThreadExecutor(); - processor = new BitmapToHardwareBufferProcessor(directExecutor()); - } - - @After - public void tearDown() throws Exception { - if (processor != null) { - processor.close(); - } - if (executorService != null) { - executorService.shutdown(); - } - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void process_withBitmap_copiesPixelsCorrectly() throws Exception { - Bitmap inputBitmap = BitmapPixelTestUtil.readBitmap("first_frame.png"); - HardwareBufferFrame inputFrame = - new HardwareBufferFrame.Builder( - /* hardwareBuffer= */ null, directExecutor(), /* releaseCallback= */ (fence) -> {}) - .setInternalFrame(inputBitmap) - .build(); - - HardwareBufferFrame outputFrame = processor.process(inputFrame); - - assertThat(outputFrame.hardwareBuffer).isNotNull(); - HardwareBuffer hardwareBuffer = outputFrame.hardwareBuffer; - - assertThat(hardwareBuffer.getWidth()).isEqualTo(inputBitmap.getWidth()); - assertThat(hardwareBuffer.getHeight()).isEqualTo(inputBitmap.getHeight()); - - Bitmap outputBitmap = readBitmapFromHardwareBuffer(hardwareBuffer); - - assertThat( - BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888( - inputBitmap, outputBitmap, testName.getMethodName())) - .isLessThan(MAX_AVG_PIXEL_DIFFERENCE); - - outputFrame.release(null); - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void process_invalidBitmap_throwsIllegalStateException() throws Exception { - Bitmap inputBitmap = BitmapPixelTestUtil.readBitmap("first_frame.png"); - HardwareBufferFrame inputFrame = - new HardwareBufferFrame.Builder( - /* hardwareBuffer= */ null, directExecutor(), /* releaseCallback= */ (fence) -> {}) - .setInternalFrame(inputBitmap) - .build(); - - inputBitmap.recycle(); - - assertThrows(IllegalStateException.class, () -> processor.process(inputFrame)); - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void process_alreadyHasHardwareBuffer_returnsOriginalFrame() { - try (HardwareBuffer hardwareBuffer = createHardwareBuffer()) { - HardwareBufferFrame inputFrame = - new HardwareBufferFrame.Builder( - hardwareBuffer, directExecutor(), /* releaseCallback= */ (fence) -> {}) - .build(); - - HardwareBufferFrame outputFrame = processor.process(inputFrame); - - assertThat(outputFrame).isSameInstanceAs(inputFrame); - - outputFrame.release(null); - } - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void process_notABitmap_returnsOriginalFrame() { - HardwareBufferFrame inputFrame = - new HardwareBufferFrame.Builder( - /* hardwareBuffer= */ null, directExecutor(), /* releaseCallback= */ (fence) -> {}) - .setInternalFrame(new Object()) - .build(); - - HardwareBufferFrame outputFrame = processor.process(inputFrame); - - assertThat(outputFrame).isSameInstanceAs(inputFrame); - - outputFrame.release(null); - } - - @Test - @SdkSuppress(maxSdkVersion = 25) - public void process_sdkBelow26_returnsOriginalFrame() { - Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); - HardwareBufferFrame inputFrame = - new HardwareBufferFrame.Builder( - /* hardwareBuffer= */ null, directExecutor(), /* releaseCallback= */ (fence) -> {}) - .setInternalFrame(bitmap) - .build(); - - HardwareBufferFrame outputFrame = processor.process(inputFrame); - - assertThat(outputFrame).isSameInstanceAs(inputFrame); - - outputFrame.release(null); - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void process_repeatedBitmap_reusesSameBuffer() throws IOException { - Bitmap inputBitmap = BitmapPixelTestUtil.readBitmap("first_frame.png"); - HardwareBufferFrame inputFrame1 = createBitmapFrame(inputBitmap); - HardwareBufferFrame inputFrame2 = createBitmapFrame(inputBitmap); - - HardwareBufferFrame outputFrame1 = processor.process(inputFrame1); - HardwareBufferFrame outputFrame2 = processor.process(inputFrame2); - - assertThat(outputFrame1.hardwareBuffer).isSameInstanceAs(outputFrame2.hardwareBuffer); - assertThat(outputFrame1.hardwareBuffer.isClosed()).isFalse(); - - outputFrame1.release(null); - outputFrame2.release(null); - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void process_repeatedBitmapAfterRelease_reusesSameBuffer() throws IOException { - Bitmap inputBitmap = BitmapPixelTestUtil.readBitmap("first_frame.png"); - HardwareBufferFrame inputFrame1 = createBitmapFrame(inputBitmap); - HardwareBufferFrame inputFrame2 = createBitmapFrame(inputBitmap); - - HardwareBufferFrame outputFrame1 = processor.process(inputFrame1); - outputFrame1.release(null); - HardwareBufferFrame outputFrame2 = processor.process(inputFrame2); - - assertThat(outputFrame1.hardwareBuffer).isSameInstanceAs(outputFrame2.hardwareBuffer); - assertThat(outputFrame1.hardwareBuffer.isClosed()).isFalse(); - - outputFrame2.release(null); - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void process_differentBitmap_createsNewBuffer() { - Bitmap bitmap1 = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); - Bitmap bitmap2 = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); - - HardwareBufferFrame outputFrame1 = processor.process(createBitmapFrame(bitmap1)); - HardwareBufferFrame outputFrame2 = processor.process(createBitmapFrame(bitmap2)); - - assertThat(outputFrame1.hardwareBuffer).isNotSameInstanceAs(outputFrame2.hardwareBuffer); - - outputFrame1.release(null); - outputFrame2.release(null); - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void process_differentGenerationId_createsNewBuffer() { - Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); - - HardwareBufferFrame outputFrame1 = processor.process(createBitmapFrame(bitmap)); - bitmap.eraseColor(Color.RED); - HardwareBufferFrame outputFrame2 = processor.process(createBitmapFrame(bitmap)); - - assertThat(outputFrame1.hardwareBuffer).isNotSameInstanceAs(outputFrame2.hardwareBuffer); - - outputFrame1.release(null); - outputFrame2.release(null); - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void process_differentBitmap_releasesOldBuffer() { - Bitmap bitmap1 = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); - Bitmap bitmap2 = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); - - HardwareBufferFrame outputFrame1 = processor.process(createBitmapFrame(bitmap1)); - HardwareBufferFrame outputFrame2 = processor.process(createBitmapFrame(bitmap2)); - HardwareBuffer buffer1 = outputFrame1.hardwareBuffer; - - // Processor released its hold on buffer1 when bitmap2 was converted. - // buffer1 is still held by outputFrame1. - assertThat(buffer1.isClosed()).isFalse(); - - outputFrame1.release(null); - assertThat(buffer1.isClosed()).isTrue(); - - outputFrame2.release(null); - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void process_releasesInputFrame() throws Exception { - CountDownLatch releasedLatch = new CountDownLatch(1); - HardwareBufferFrame inputFrame = - new HardwareBufferFrame.Builder( - /* hardwareBuffer= */ null, - directExecutor(), - /* releaseCallback= */ (fence) -> releasedLatch.countDown()) - .setInternalFrame(Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)) - .build(); - - HardwareBufferFrame outputFrame = processor.process(inputFrame); - - assertThat(releasedLatch.await(1000, SECONDS)).isTrue(); - - outputFrame.release(null); - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void releaseOutputFrame_sharedBuffer_doesNotCloseSharedBuffer() throws Exception { - Bitmap inputBitmap = BitmapPixelTestUtil.readBitmap("first_frame.png"); - HardwareBufferFrame inputFrame1 = createBitmapFrame(inputBitmap); - HardwareBufferFrame inputFrame2 = createBitmapFrame(inputBitmap); - - HardwareBufferFrame outputFrame1 = processor.process(inputFrame1); - HardwareBufferFrame outputFrame2 = processor.process(inputFrame2); - - outputFrame1.release(null); - assertThat(outputFrame1.hardwareBuffer.isClosed()).isFalse(); - assertThat(outputFrame2.hardwareBuffer.isClosed()).isFalse(); - - outputFrame2.release(null); - assertThat(outputFrame1.hardwareBuffer.isClosed()).isFalse(); - assertThat(outputFrame2.hardwareBuffer.isClosed()).isFalse(); - } - - @Test - @SdkSuppress(minSdkVersion = 26) - public void close_releasesInternalBuffer() throws Exception { - Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); - HardwareBufferFrame outputFrame = processor.process(createBitmapFrame(bitmap)); - HardwareBuffer hardwareBuffer = outputFrame.hardwareBuffer; - - processor.close(); - // Processor released its reference, but the frame still holds one. - assertThat(hardwareBuffer.isClosed()).isFalse(); - - outputFrame.release(null); - assertThat(hardwareBuffer.isClosed()).isTrue(); - } - - @RequiresApi(26) - private Bitmap readBitmapFromHardwareBuffer(HardwareBuffer hardwareBuffer) throws Exception { - if (Build.VERSION.SDK_INT >= 29) { - Bitmap hardwareBitmap = - Bitmap.wrapHardwareBuffer(hardwareBuffer, ColorSpace.get(ColorSpace.Named.SRGB)); - assertThat(hardwareBitmap).isNotNull(); - return hardwareBitmap.copy(Bitmap.Config.ARGB_8888, false); - } - - int width = hardwareBuffer.getWidth(); - int height = hardwareBuffer.getHeight(); - EGLDisplay eglDisplay = GlUtil.getDefaultEglDisplay(); - EGLContext eglContext = GlUtil.createEglContext(eglDisplay); - EGLSurface eglSurface = GlUtil.createFocusedPlaceholderEglSurface(eglContext, eglDisplay); - - int texId = GlUtil.createTexture(width, height, false); - long eglImage = - HardwareBufferJni.INSTANCE.nativeCreateEglImageFromHardwareBuffer( - eglDisplay.getNativeHandle(), hardwareBuffer); - assertThat(eglImage).isNotEqualTo(0L); - - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId); - assertThat(HardwareBufferJni.INSTANCE.nativeBindEGLImage(GLES20.GL_TEXTURE_2D, eglImage)) - .isTrue(); - - int fboId = GlUtil.createFboForTexture(texId); - GlUtil.focusFramebufferUsingCurrentContext(fboId, width, height); - Bitmap bitmap = BitmapPixelTestUtil.createArgb8888BitmapFromFocusedGlFramebuffer(width, height); - - GLES20.glDeleteTextures(1, new int[] {texId}, 0); - assertThat( - HardwareBufferJni.INSTANCE.nativeDestroyEGLImage( - eglDisplay.getNativeHandle(), eglImage)) - .isTrue(); - GlUtil.destroyEglSurface(eglDisplay, eglSurface); - GlUtil.destroyEglContext(eglDisplay, eglContext); - - return bitmap; - } - - private static HardwareBufferFrame createBitmapFrame(Bitmap bitmap) { - return new HardwareBufferFrame.Builder( - /* hardwareBuffer= */ null, directExecutor(), /* releaseCallback= */ (fence) -> {}) - .setInternalFrame(bitmap) - .build(); - } - - @RequiresApi(26) - private static HardwareBuffer createHardwareBuffer() { - return HardwareBuffer.create( - 10, - 10, - /* format= */ HardwareBuffer.RGBA_8888, - /* layers= */ 1, - /* usage= */ HardwareBuffer.USAGE_CPU_WRITE_OFTEN | HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE); - } -} diff --git a/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/HardwareBufferJniTest.kt b/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/HardwareBufferJniTest.kt deleted file mode 100644 index f540ff6c1c9..00000000000 --- a/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/HardwareBufferJniTest.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2026 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.effect.ndk - -import android.graphics.Bitmap -import android.graphics.ColorSpace -import android.hardware.HardwareBuffer -import android.os.Build.VERSION.SDK_INT -import androidx.media3.test.utils.BitmapPixelTestUtil -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SdkSuppress -import com.google.common.truth.Truth.assertThat -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TestName -import org.junit.runner.RunWith - -/** Tests for [HardwareBufferJni]. */ -@SdkSuppress(minSdkVersion = 26) -@RunWith(AndroidJUnit4::class) -class HardwareBufferJniTest { - - @get:Rule val testName = TestName() - - @Test - fun nativeCopyBitmapToHardwareBuffer_isCorrect() { - val inputBitmap = BitmapPixelTestUtil.readBitmap("first_frame.png") - val hardwareBuffer = - HardwareBuffer.create( - inputBitmap.width, - inputBitmap.height, - /* format= */ HardwareBuffer.RGBA_8888, - /* layers= */ 1, - /* usage= */ HardwareBuffer.USAGE_CPU_WRITE_OFTEN or HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE, - ) - - try { - assertThat(HardwareBufferJni.nativeCopyBitmapToHardwareBuffer(inputBitmap, hardwareBuffer)) - .isTrue() - if (SDK_INT >= 29) { - val hardwareBitmap = - Bitmap.wrapHardwareBuffer(hardwareBuffer, ColorSpace.get(ColorSpace.Named.SRGB))!! - val outputBitmap = hardwareBitmap.copy(Bitmap.Config.ARGB_8888, false) - assertThat( - BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888( - inputBitmap, - outputBitmap, - testName.methodName, - ) - ) - .isZero() - } - } finally { - hardwareBuffer.close() - } - } - - @Test - fun nativeCopyBitmapToHardwareBuffer_mismatchedDimensions_returnsFalse() { - val inputBitmap = BitmapPixelTestUtil.readBitmap("first_frame.png") - val hardwareBuffer = - HardwareBuffer.create( - inputBitmap.width * 2, - inputBitmap.height / 2, - /* format= */ HardwareBuffer.RGBA_8888, - /* layers= */ 1, - /* usage= */ HardwareBuffer.USAGE_CPU_WRITE_OFTEN or HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE, - ) - - try { - assertThat(HardwareBufferJni.nativeCopyBitmapToHardwareBuffer(inputBitmap, hardwareBuffer)) - .isFalse() - } finally { - hardwareBuffer.close() - } - } - - @Test - fun nativeCopyBitmapToHardwareBuffer_incorrectUsage_returnsFalse() { - val inputBitmap = BitmapPixelTestUtil.readBitmap("first_frame.png") - val hardwareBuffer = - HardwareBuffer.create( - inputBitmap.width, - inputBitmap.height, - /* format= */ HardwareBuffer.RGBA_8888, - /* layers= */ 1, - /* usage= */ HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE, - ) - - try { - assertThat(HardwareBufferJni.nativeCopyBitmapToHardwareBuffer(inputBitmap, hardwareBuffer)) - .isFalse() - } finally { - hardwareBuffer.close() - } - } - - @Test - fun nativeCopyBitmapToHardwareBuffer_incorrectFormat_returnsFalse() { - val inputBitmap = BitmapPixelTestUtil.readBitmap("first_frame.png") - val hardwareBuffer = - HardwareBuffer.create( - inputBitmap.width, - inputBitmap.height, - /* format= */ HardwareBuffer.RGBA_1010102, - /* layers= */ 1, - /* usage= */ HardwareBuffer.USAGE_CPU_WRITE_OFTEN or HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE, - ) - - try { - assertThat(HardwareBufferJni.nativeCopyBitmapToHardwareBuffer(inputBitmap, hardwareBuffer)) - .isFalse() - } finally { - hardwareBuffer.close() - } - } -} diff --git a/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/HardwareBufferToGlTextureFrameProcessorTest.kt b/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/HardwareBufferToGlTextureFrameProcessorTest.kt deleted file mode 100644 index ec7c174fc38..00000000000 --- a/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/HardwareBufferToGlTextureFrameProcessorTest.kt +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright 2026 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.effect.ndk - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.PixelFormat -import android.media.Image -import android.media.ImageReader -import androidx.media3.common.Format -import androidx.media3.common.GlObjectsProvider -import androidx.media3.common.MediaItem -import androidx.media3.common.MimeTypes -import androidx.media3.common.PlaybackException -import androidx.media3.common.Player -import androidx.media3.common.SurfaceInfo -import androidx.media3.common.util.Consumer -import androidx.media3.common.util.GlUtil -import androidx.media3.common.util.Util -import androidx.media3.common.util.Util.isRunningOnEmulator -import androidx.media3.effect.GlTextureFrame -import androidx.media3.effect.GlTextureFrameRenderer -import androidx.media3.effect.HardwareBufferFrame -import androidx.media3.effect.PacketConsumer -import androidx.media3.effect.PacketConsumer.Packet -import androidx.media3.effect.SingleContextGlObjectsProvider -import androidx.media3.test.utils.AssetInfo.MP4_ASSET_WITH_INCREASING_TIMESTAMPS -import androidx.media3.test.utils.BitmapPixelTestUtil -import androidx.media3.transformer.Composition -import androidx.media3.transformer.CompositionPlayer -import androidx.media3.transformer.EditedMediaItem -import androidx.media3.transformer.EditedMediaItemSequence -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SdkSuppress -import com.google.common.collect.ImmutableList -import com.google.common.truth.Truth.assertThat -import com.google.common.util.concurrent.MoreExecutors.listeningDecorator -import java.util.concurrent.Executors -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TestName -import org.junit.runner.RunWith - -/** Tests for [HardwareBufferToGlTextureFrameProcessor]. */ -@SdkSuppress(minSdkVersion = 26) -@RunWith(AndroidJUnit4::class) -class HardwareBufferToGlTextureFrameProcessorTest { - private val context: Context = ApplicationProvider.getApplicationContext() - private val glExecutorService = Executors.newSingleThreadExecutor() - private val glDispatcher = glExecutorService.asCoroutineDispatcher() - private val glObjectsProvider: GlObjectsProvider = SingleContextGlObjectsProvider() - private val errorConsumer = Consumer { t -> throw t } - private lateinit var hardwareBufferToGlTextureFrameProcessor: - HardwareBufferToGlTextureFrameProcessor - private var renderedBitmapDeferred: CompletableDeferred? = null - private var toBitmapConsumer: PacketConsumer? = null - private var glTextureFrameRenderer: GlTextureFrameRenderer? = null - private var outputImageReader: ImageReader? = null - - @get:Rule val testName = TestName() - - @Before - fun setUp() { - hardwareBufferToGlTextureFrameProcessor = - HardwareBufferToGlTextureFrameProcessor( - context, - glExecutorService, - glObjectsProvider, - errorConsumer, - ) - } - - @After - fun tearDown() { - runBlocking { - withContext(glDispatcher) { - toBitmapConsumer?.release() - hardwareBufferToGlTextureFrameProcessor?.release() - glTextureFrameRenderer?.release() - glObjectsProvider.release(GlUtil.getDefaultEglDisplay()) - } - } - outputImageReader?.close() - glExecutorService.shutdown() - } - - @SdkSuppress(minSdkVersion = 31) - @Test - fun queuePacket_withARGB8888HardwareBuffer_outputsCorrectGlTexture() = runBlocking { - renderedBitmapDeferred = CompletableDeferred() - toBitmapConsumer = ConvertToBitmapConsumer(glDispatcher, renderedBitmapDeferred!!) - hardwareBufferToGlTextureFrameProcessor.setOutput(toBitmapConsumer!!) - - val argb8888Bitmap = BitmapPixelTestUtil.readBitmap("first_frame.png") - val hardwareBitmap = argb8888Bitmap.copy(Bitmap.Config.HARDWARE, false) - val argb8888HardwareBuffer = hardwareBitmap.hardwareBuffer - - val argb8888HardwareBufferFrame = - HardwareBufferFrame.Builder( - argb8888HardwareBuffer, - glExecutorService, - { argb8888HardwareBuffer.close() }, - ) - .setFormat( - Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_RAW) - .setWidth(argb8888Bitmap.width) - .setHeight(argb8888Bitmap.height) - .build() - ) - .build() - - hardwareBufferToGlTextureFrameProcessor.queuePacket(Packet.of(argb8888HardwareBufferFrame)) - // The output bitmap is also ARGB8888 - - val renderedBitmap = - withTimeout(timeMillis = TEST_TIMEOUT_MS) { renderedBitmapDeferred!!.await() } - - assertThat( - BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888( - argb8888Bitmap, - renderedBitmap, - testName.methodName, - ) - ) - .isLessThan(MAX_AVG_PIXEL_DIFFERENCE) - - argb8888HardwareBufferFrame.release(/* releaseFence= */ null) - } - - @SdkSuppress(minSdkVersion = 28) - @Test - fun queuePacket_withFirstFrameAsYuv420HardwareBuffer_outputsCorrectGlTexture() = runBlocking { - glTextureFrameRenderer = - GlTextureFrameRenderer.create( - context, - listeningDecorator(glExecutorService), - glObjectsProvider, - { e -> errorConsumer.accept(e) }, - GlTextureFrameRenderer.Listener.NO_OP, - ) - outputImageReader = - ImageReader.newInstance(TEST_VIDEO_WIDTH, TEST_VIDEO_HEIGHT, PixelFormat.RGBA_8888, 1) - glTextureFrameRenderer?.setRenderOutput( - SurfaceInfo( - outputImageReader!!.surface, - outputImageReader!!.width, - outputImageReader!!.height, - ) - ) - val composition = - Composition.Builder( - EditedMediaItemSequence.withVideoFrom( - listOf( - EditedMediaItem.Builder( - MediaItem.fromUri(MP4_ASSET_WITH_INCREASING_TIMESTAMPS.uri) - .buildUpon() - .setClippingConfiguration( - // One frame - MediaItem.ClippingConfiguration.Builder().setEndPositionMs(500).build() - ) - .build() - ) - .setDurationUs(MP4_ASSET_WITH_INCREASING_TIMESTAMPS.videoDurationUs) - .build() - ) - ) - ) - .build() - val imageAcquiredDeferred = CompletableDeferred() - outputImageReader?.setOnImageAvailableListener( - { reader -> - if (!imageAcquiredDeferred.isCompleted) { - imageAcquiredDeferred.complete(checkNotNull(reader).acquireNextImage()) - } - }, - Util.createHandlerForCurrentOrMainLooper(), - ) - hardwareBufferToGlTextureFrameProcessor.setOutput(checkNotNull(glTextureFrameRenderer)) - val compositionPlayer = - CompositionPlayer.Builder(context) - .setPacketConsumerFactory { - PassthroughPacketConsumer(hardwareBufferToGlTextureFrameProcessor) - } - .setGlObjectsProvider(glObjectsProvider) - .build() - withContext(Dispatchers.Main) { - compositionPlayer.setComposition(composition) - compositionPlayer.addListener( - object : Player.Listener { - override fun onRenderedFirstFrame() { - compositionPlayer.stop() - } - - override fun onPlayerError(exception: PlaybackException) { - throw exception - } - } - ) - compositionPlayer.prepare() - compositionPlayer.play() - } - - val renderedImage = withTimeout(timeMillis = TEST_TIMEOUT_MS) { imageAcquiredDeferred.await() } - val renderedBitmap = BitmapPixelTestUtil.createArgb8888BitmapFromRgba8888Image(renderedImage) - renderedImage.close() - - val expectedBitmap = BitmapPixelTestUtil.readBitmap("first_frame.png") - assertThat( - BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888( - expectedBitmap, - renderedBitmap, - testName.methodName, - ) - ) - .isLessThan(MAX_AVG_PIXEL_DIFFERENCE) - - withContext(Dispatchers.Main) { compositionPlayer.release() } - } - - private class ConvertToBitmapConsumer( - private val glDispatcher: CoroutineDispatcher, - private val renderedBitmapDeferred: CompletableDeferred, - ) : PacketConsumer { - override suspend fun queuePacket(packet: Packet) = - withContext(glDispatcher) { - when (packet) { - Packet.EndOfStream -> return@withContext - is Packet.Payload -> { - val inputTextureInfo = packet.payload.glTextureInfo - val textureWidth = inputTextureInfo.width - val textureHeight = inputTextureInfo.height - val fboId = GlUtil.createFboForTexture(inputTextureInfo.texId) - GlUtil.focusFramebufferUsingCurrentContext(fboId, textureWidth, textureHeight) - val outputBitmap = - BitmapPixelTestUtil.createArgb8888BitmapFromFocusedGlFramebuffer( - textureWidth, - textureHeight, - ) - renderedBitmapDeferred.complete(outputBitmap) - } - } - } - - override suspend fun release() {} - } - - private class PassthroughPacketConsumer( - private val hardwareBufferToGlTextureFrameProcessor: HardwareBufferToGlTextureFrameProcessor - ) : PacketConsumer> { - override suspend fun queuePacket(packet: Packet>) { - when (packet) { - is Packet.EndOfStream -> hardwareBufferToGlTextureFrameProcessor.queuePacket(packet) - is Packet.Payload> -> - hardwareBufferToGlTextureFrameProcessor.queuePacket(Packet.of(packet.payload[0])) - } - } - - override suspend fun release() {} - } - - companion object { - val TEST_TIMEOUT_MS = if (isRunningOnEmulator()) 20_000L else 10_000L - val TEST_VIDEO_WIDTH = MP4_ASSET_WITH_INCREASING_TIMESTAMPS.videoFormat!!.width - val TEST_VIDEO_HEIGHT = MP4_ASSET_WITH_INCREASING_TIMESTAMPS.videoFormat!!.height - // TODO: b/474075198 - Calculate the transformation matrix from MediaFormat. The current - // transformation matrix is hardcoded, and causing the output bitmap to be a bit misaligned. - const val MAX_AVG_PIXEL_DIFFERENCE = 10F - } -} diff --git a/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/package-info.java b/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/package-info.java deleted file mode 100644 index cd498c0eaf2..00000000000 --- a/libraries/effect_ndk/src/androidTest/java/androidx/media3/effect/ndk/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2026 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. - */ - -@NonNullApi -package androidx.media3.effect.ndk; - -import androidx.media3.common.util.NonNullApi; diff --git a/libraries/effect_ndk/src/main/AndroidManifest.xml b/libraries/effect_ndk/src/main/AndroidManifest.xml deleted file mode 100644 index 82124cc52f1..00000000000 --- a/libraries/effect_ndk/src/main/AndroidManifest.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/BitmapToHardwareBufferProcessor.java b/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/BitmapToHardwareBufferProcessor.java deleted file mode 100644 index 3997bc47073..00000000000 --- a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/BitmapToHardwareBufferProcessor.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright 2026 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.effect.ndk; - -import static android.os.Build.VERSION.SDK_INT; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import android.graphics.Bitmap; -import android.hardware.HardwareBuffer; -import android.system.ErrnoException; -import androidx.annotation.GuardedBy; -import androidx.annotation.Nullable; -import androidx.media3.common.util.ExperimentalApi; -import androidx.media3.common.util.Log; -import androidx.media3.effect.HardwareBufferFrame; -import androidx.media3.effect.SyncFenceCompat; -import androidx.media3.transformer.HardwareBufferFrameProcessor; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * A {@link HardwareBufferFrameProcessor} that converts {@link Bitmap}-backed {@link - * HardwareBufferFrame} instances into {@link android.hardware.HardwareBuffer}-backed ones. - * - *

This processor caches the underlying {@link android.hardware.HardwareBuffer} as long as the - * input {@link Bitmap} remains the same (verified via {@link Bitmap#getGenerationId()}). It uses - * JNI to copy pixels from the bitmap to the hardware buffer. - * - *

The processor manages the lifecycle of the hardware buffer using reference counting, ensuring - * it is only closed once all consumer frames and the processor itself have released their - * references. - */ -@ExperimentalApi // TODO: b/479415385 - remove when packet consumer is production-ready. -public class BitmapToHardwareBufferProcessor implements HardwareBufferFrameProcessor { - - private static final String TAG = "BitmapToHbProcessor"; - private static final int RELEASE_TIMEOUT_MS = 500; - - private final Executor releaseBufferExecutor; - - @GuardedBy("this") - @Nullable - private ReferenceCountedBuffer currentBuffer; - - @GuardedBy("this") - @Nullable - private Bitmap currentBitmap; - - @GuardedBy("this") - private int currentBitmapGenerationId; - - /** - * Creates an instance. - * - * @param releaseBufferExecutor The {@link Executor} on which the hardware buffer release and - * fence waiting logic will be executed. - */ - public BitmapToHardwareBufferProcessor(Executor releaseBufferExecutor) { - this.releaseBufferExecutor = releaseBufferExecutor; - } - - @Override - public HardwareBufferFrame process(HardwareBufferFrame inputFrame) { - if (SDK_INT < 26 - || inputFrame.hardwareBuffer != null - || !(inputFrame.internalFrame instanceof Bitmap)) { - return inputFrame; - } - Bitmap nextBitmap = checkNotNull((Bitmap) inputFrame.internalFrame); - - synchronized (this) { - // Check whether the current bitmap should be updated. - if (currentBuffer != null - && (currentBitmap != nextBitmap - || currentBitmapGenerationId != nextBitmap.getGenerationId())) { - ReferenceCountedBuffer currentBuffer = checkNotNull(this.currentBuffer); - releaseBufferExecutor.execute(() -> currentBuffer.release(/* fence= */ null)); - this.currentBuffer = null; - } - - if (currentBuffer == null) { - HardwareBuffer buffer = - HardwareBuffer.create( - nextBitmap.getWidth(), - nextBitmap.getHeight(), - /* pixelFormat= */ HardwareBuffer.RGBA_8888, - /* layers= */ 1, - /* usageFlags= */ HardwareBuffer.USAGE_GPU_COLOR_OUTPUT - | HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE - | HardwareBuffer.USAGE_CPU_READ_RARELY - | HardwareBuffer.USAGE_CPU_WRITE_OFTEN); - - checkState(HardwareBufferJni.INSTANCE.nativeCopyBitmapToHardwareBuffer(nextBitmap, buffer)); - - currentBuffer = new ReferenceCountedBuffer(buffer); - currentBitmap = nextBitmap; - // Save the generationId from after the native copy, as AndroidBitmap_unlockPixels can cause - // the generationId to increment. - currentBitmapGenerationId = nextBitmap.getGenerationId(); - } - - inputFrame.release(/* releaseFence= */ null); - ReferenceCountedBuffer.BufferReference frameReference = - checkNotNull(currentBuffer).acquireReference(); - - return new HardwareBufferFrame.Builder( - checkNotNull(currentBuffer).hardwareBuffer, - releaseBufferExecutor, - /* releaseCallback= */ frameReference::release) - .setFormat(inputFrame.format) - .setInternalFrame(nextBitmap) - .setReleaseTimeNs(inputFrame.releaseTimeNs) - .setPresentationTimeUs(inputFrame.presentationTimeUs) - .setMetadata(inputFrame.getMetadata()) - .build(); - } - } - - @Override - public void close() { - synchronized (this) { - if (currentBuffer != null) { - currentBuffer.release(/* fence= */ null); - currentBuffer = null; - } - } - } - - /** - * Manages the shared HardwareBuffer. It collects sync fences from all consumers and waits for - * them before closing the underlying buffer. - */ - private static final class ReferenceCountedBuffer { - private final HardwareBuffer hardwareBuffer; - private final AtomicInteger refCount; - - @GuardedBy("this") - private final List pendingFences; - - private ReferenceCountedBuffer(HardwareBuffer hardwareBuffer) { - this.hardwareBuffer = hardwareBuffer; - this.refCount = new AtomicInteger(1); // Starts at 1 (owned by Processor) - this.pendingFences = new ArrayList<>(); - } - - private BufferReference acquireReference() { - refCount.incrementAndGet(); - return new BufferReference(this); - } - - /** - * Decrements the shared counter. If zero, waits for all fences and closes the buffer. - * - *

Called on {@link #releaseBufferExecutor}. - */ - private void release(@Nullable SyncFenceCompat fence) { - if (fence != null) { - synchronized (this) { - pendingFences.add(fence); - } - } - int newRefCount = refCount.decrementAndGet(); - // The frame should only be released once. - checkState(newRefCount >= 0); - if (newRefCount == 0) { - cleanup(); - } - } - - /** - * Waits for all collected fences to signal, closes them, and then closes the HardwareBuffer. - */ - private void cleanup() { - List fencesToWait; - synchronized (this) { - fencesToWait = new ArrayList<>(pendingFences); - pendingFences.clear(); - } - - for (SyncFenceCompat fence : fencesToWait) { - waitAndClose(fence); - } - if (SDK_INT >= 26) { - hardwareBuffer.close(); - } - } - - private static void waitAndClose(@Nullable SyncFenceCompat fence) { - if (fence == null) { - return; - } - try { - boolean signaled = fence.await(RELEASE_TIMEOUT_MS); - if (!signaled) { - Log.w(TAG, "Timed out waiting for release fence."); - } - fence.close(); - } catch (ErrnoException e) { - Log.w(TAG, "Error waiting for release fence.", e); - } catch (IOException e) { - Log.w(TAG, "Error closing release fence.", e); - } - } - - /** - * A unique token representing a single frame's usage. Guaranteed to decrement the shared count - * exactly once. - */ - private static final class BufferReference { - private final ReferenceCountedBuffer parent; - private final AtomicBoolean hasReleased = new AtomicBoolean(false); - - private BufferReference(ReferenceCountedBuffer parent) { - this.parent = parent; - } - - /** Called on {@link #releaseBufferExecutor}. */ - private void release(@Nullable SyncFenceCompat fence) { - // Ensure the parent count is only decremented once per reference instance. - if (hasReleased.compareAndSet(false, true)) { - parent.release(fence); - } else { - // If release is called multiple times, still close the extra fences to prevent leaks. - waitAndClose(fence); - } - } - } - } -} diff --git a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/HardwareBufferJni.kt b/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/HardwareBufferJni.kt deleted file mode 100644 index 8a172a573ac..00000000000 --- a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/HardwareBufferJni.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2026 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.effect.ndk - -import android.graphics.Bitmap -import android.hardware.HardwareBuffer -import androidx.annotation.RequiresApi -import androidx.media3.common.util.ExperimentalApi - -/** JNI methods for HardwareBuffer interaction. */ -@RequiresApi(26) -@ExperimentalApi // TODO: b/449956776 - Remove once FrameConsumer API is finalized. -object HardwareBufferJni { - init { - System.loadLibrary("hardwareBufferJNI") - } - - /** Creates an EGLImage from a [HardwareBuffer]. */ - external fun nativeCreateEglImageFromHardwareBuffer( - displayHandle: Long, - hardwareBuffer: HardwareBuffer, - ): Long - - /** - * Binds an EGLImage to the specified texture target. Returns whether the binding is successful. - */ - external fun nativeBindEGLImage(target: Int, eglImageHandle: Long): Boolean - - /** Destroys an EGLImage. Retutns whether the deletion is successful. */ - external fun nativeDestroyEGLImage(displayHandle: Long, imageHandle: Long): Boolean - - /** Copies a bitmap to a HardwareBuffer. Returns whether the copy is successful. */ - external fun nativeCopyBitmapToHardwareBuffer(bitmap: Bitmap, hb: HardwareBuffer): Boolean -} diff --git a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/HardwareBufferSurfaceRenderer.kt b/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/HardwareBufferSurfaceRenderer.kt deleted file mode 100644 index 603732a8ea4..00000000000 --- a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/HardwareBufferSurfaceRenderer.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2026 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.effect.ndk - -import android.content.Context -import androidx.annotation.RequiresApi -import androidx.media3.common.GlObjectsProvider -import androidx.media3.common.SurfaceInfo -import androidx.media3.common.util.Consumer -import androidx.media3.common.util.ExperimentalApi -import androidx.media3.common.util.Util -import androidx.media3.effect.GlTextureFrameRenderer -import androidx.media3.effect.HardwareBufferFrame -import androidx.media3.effect.PacketConsumer.Packet -import androidx.media3.effect.RenderingPacketConsumer -import androidx.media3.effect.SingleContextGlObjectsProvider -import com.google.common.util.concurrent.ListeningExecutorService -import com.google.common.util.concurrent.MoreExecutors - -/** - * A [androidx.media3.effect.PacketConsumer] that renders a [Packet] of [HardwareBufferFrame]s to an - * output [android.view.Surface] by first converting to a GL texture, using a - * [HardwareBufferToGlTextureFrameProcessor] and [GlTextureFrameRenderer]. - */ -@RequiresApi(26) -@ExperimentalApi // TODO: b/449956776 - Remove once FrameConsumer API is finalized. -class HardwareBufferSurfaceRenderer -private constructor( - private val converter: HardwareBufferToGlTextureFrameProcessor, - private val renderer: GlTextureFrameRenderer, - private var errorConsumer: Consumer, -) : RenderingPacketConsumer { - - override fun setRenderOutput(output: SurfaceInfo?) { - renderer.setRenderOutput(output) - } - - override fun setErrorConsumer(errorConsumer: Consumer) { - converter.setErrorConsumer(errorConsumer) - renderer.setErrorConsumer(errorConsumer) - this.errorConsumer = errorConsumer - } - - override suspend fun queuePacket(packet: Packet) { - converter.queuePacket(packet) - } - - override suspend fun release() { - converter.release() - renderer.release() - } - - companion object { - /** - * Creates a [HardwareBufferSurfaceRenderer] instance, that uses OpenGL to render - * [HardwareBufferFrame] to an output [android.view.Surface]. - * - * Internally creates a single threaded [java.util.concurrent.ExecutorService] to run GL - * commands, and a [SingleContextGlObjectsProvider]. - */ - @JvmStatic - fun create( - context: Context, - listener: GlTextureFrameRenderer.Listener, - errorConsumer: Consumer, - ): HardwareBufferSurfaceRenderer { - val glExecutorService = - MoreExecutors.listeningDecorator( - Util.newSingleThreadExecutor("DefaultHardwareBufferSurfaceRenderer::GlThread") - ) - val glObjectsProvider = SingleContextGlObjectsProvider() - return create(context, glExecutorService, glObjectsProvider, listener, errorConsumer) - } - - /** - * Creates a [HardwareBufferSurfaceRenderer] instance, that uses OpenGL to render - * [HardwareBufferFrame] to an output [android.view.Surface], on the given - * [glThread][glExecutorService]. - */ - @JvmStatic - fun create( - context: Context, - glExecutorService: ListeningExecutorService, - glObjectsProvider: GlObjectsProvider, - listener: GlTextureFrameRenderer.Listener, - errorConsumer: Consumer, - ): HardwareBufferSurfaceRenderer { - val converter = - HardwareBufferToGlTextureFrameProcessor( - context, - glExecutorService, - glObjectsProvider, - errorConsumer, - ) - val renderer = - GlTextureFrameRenderer.create( - context, - glExecutorService, - glObjectsProvider, - errorConsumer::accept, - listener, - ) - converter.setOutput(renderer) - return HardwareBufferSurfaceRenderer(converter, renderer, errorConsumer) - } - } -} diff --git a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/HardwareBufferToGlTextureFrameProcessor.kt b/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/HardwareBufferToGlTextureFrameProcessor.kt deleted file mode 100644 index e1cc863ac0d..00000000000 --- a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/HardwareBufferToGlTextureFrameProcessor.kt +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright 2026 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.effect.ndk - -import android.content.Context -import android.graphics.Matrix -import android.graphics.PixelFormat.RGBA_8888 -import android.hardware.HardwareBuffer -import android.opengl.EGLContext -import android.opengl.EGLSurface -import android.opengl.GLES11Ext -import android.opengl.GLES20 -import androidx.annotation.RequiresApi -import androidx.media3.common.C -import androidx.media3.common.ColorInfo -import androidx.media3.common.GlObjectsProvider -import androidx.media3.common.GlTextureInfo -import androidx.media3.common.VideoFrameProcessingException -import androidx.media3.common.VideoFrameProcessor -import androidx.media3.common.util.Consumer -import androidx.media3.common.util.ExperimentalApi -import androidx.media3.common.util.GlUtil -import androidx.media3.common.util.Log -import androidx.media3.effect.DefaultShaderProgram -import androidx.media3.effect.DefaultVideoFrameProcessor -import androidx.media3.effect.GlShaderProgramPacketProcessor -import androidx.media3.effect.GlTextureFrame -import androidx.media3.effect.HardwareBufferFrame -import androidx.media3.effect.MatrixUtils -import androidx.media3.effect.PacketConsumer -import androidx.media3.effect.PacketConsumer.Packet -import androidx.media3.effect.PacketConsumer.Packet.Payload -import androidx.media3.effect.PacketProcessor -import androidx.media3.effect.ndk.HardwareBufferJni.nativeBindEGLImage -import androidx.media3.effect.ndk.HardwareBufferJni.nativeCreateEglImageFromHardwareBuffer -import androidx.media3.effect.ndk.HardwareBufferJni.nativeDestroyEGLImage -import com.google.common.util.concurrent.MoreExecutors.directExecutor -import java.util.concurrent.ExecutorService -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.withContext - -/** A [PacketProcessor] implementation that converts a [HardwareBufferFrame] to [GlTextureFrame]. */ -@ExperimentalApi // TODO: b/474075198: Remove once FrameConsumer API is stable. -@RequiresApi(26) -class HardwareBufferToGlTextureFrameProcessor( - private val context: Context, - private val glExecutorService: ExecutorService, - private val glObjectsProvider: GlObjectsProvider, - private var errorConsumer: Consumer, -) : PacketProcessor { - - private val glDispatcher = glExecutorService.asCoroutineDispatcher() - - private var outputConsumer: PacketConsumer? = null - private var glShaderProgramPacketProcessor: GlShaderProgramPacketProcessor? = null - private var eglContext: EGLContext? = null - private var eglSurface: EGLSurface? = null - private var isRgba8888Shader = false - - override fun setOutput(output: PacketConsumer) { - this.outputConsumer = output - } - - override suspend fun queuePacket(packet: Packet) { - when (packet) { - is Payload -> { - withContext(glDispatcher) { - try { - maybeSetupGlResources() - val hardwareBufferFrame = packet.payload - val hardwareBuffer = checkNotNull(hardwareBufferFrame.hardwareBuffer) - val isRgba8888 = hardwareBuffer.format == RGBA_8888 - - val (eglImage, texture) = - sampleToGlTexture( - hardwareBuffer, - if (isRgba8888) GLES20.GL_TEXTURE_2D else GLES11Ext.GL_TEXTURE_EXTERNAL_OES, - ) - - if (glShaderProgramPacketProcessor != null && isRgba8888Shader != isRgba8888) { - glShaderProgramPacketProcessor?.release() - glShaderProgramPacketProcessor = null - } - - val glShaderProgramPacketProcessor = - glShaderProgramPacketProcessor - ?: createGlShaderProgramPacketProcessor(hardwareBufferFrame, !isRgba8888) - glShaderProgramPacketProcessor.queuePacket( - Packet.of(createGlTextureFrame(texture, hardwareBufferFrame, eglImage)) - ) - } catch (e: GlUtil.GlException) { - errorConsumer.accept(e) - } - } - } - is Packet.EndOfStream -> { - outputConsumer?.queuePacket(packet) - } - } - } - - fun setErrorConsumer(errorConsumer: Consumer) { - this.errorConsumer = errorConsumer - } - - /** - * Samples the input [HardwareBufferFrame.hardwareBuffer] to a texture of the specific [target]. - * Returns the sampled EGLImageKHR handle and the texture ID. - */ - private fun sampleToGlTexture(hardwareBuffer: HardwareBuffer, target: Int): Pair { - val eglImageHandle = - nativeCreateEglImageFromHardwareBuffer( - GlUtil.getDefaultEglDisplay().nativeHandle, - hardwareBuffer, - ) - if (eglImageHandle == 0L) { - throw GlUtil.GlException( - "Unable to create EGLImageKHR via JNI, format:${hardwareBuffer.format}, usage:${hardwareBuffer.usage}." - ) - } - val texture = GlUtil.generateTexture() - GLES20.glBindTexture(target, texture) - GlUtil.checkGlError() - check(nativeBindEGLImage(target, eglImageHandle)) - return eglImageHandle to texture - } - - private suspend fun createGlShaderProgramPacketProcessor( - hardwareBufferFrame: HardwareBufferFrame, - useExternalSampler: Boolean, - ): GlShaderProgramPacketProcessor { - // TODO: b/474075198 - Add HDR support. - val glShaderProgram = - if (useExternalSampler) { - DefaultShaderProgram.createWithExternalSampler( - context, - ColorInfo.SDR_BT709_LIMITED, - ColorInfo.SDR_BT709_LIMITED, - DefaultVideoFrameProcessor.WORKING_COLOR_SPACE_DEFAULT, - /* sampleWithNearest= */ true, - ) - } else { - DefaultShaderProgram.createWithInternalSampler( - context, - ColorInfo.SDR_BT709_LIMITED, - ColorInfo.SDR_BT709_LIMITED, - DefaultVideoFrameProcessor.WORKING_COLOR_SPACE_DEFAULT, - VideoFrameProcessor.INPUT_TYPE_TEXTURE_ID, - ) - } - glShaderProgram.setTextureTransformMatrix(constructTransformationMatrix(hardwareBufferFrame)) - val glShaderProgramPacketProcessor = - GlShaderProgramPacketProcessor.create(glShaderProgram, glDispatcher, glObjectsProvider) - glShaderProgramPacketProcessor.setOutput(checkNotNull(outputConsumer)) - this.glShaderProgramPacketProcessor = glShaderProgramPacketProcessor - this.isRgba8888Shader = !useExternalSampler - return glShaderProgramPacketProcessor - } - - private fun createGlTextureFrame( - texture: Int, - hardwareBufferFrame: HardwareBufferFrame, - eglImage: Long, - ): GlTextureFrame { - val format = hardwareBufferFrame.format - val frameWidth = - if (format.rotationDegrees != 90 && format.rotationDegrees != 270) format.width - else format.height - val frameHeight = - if (format.rotationDegrees != 90 && format.rotationDegrees != 270) format.height - else format.width - - // TODO: b/479415385 - Convert SyncFence to EGLSync and wait on GL thread. - hardwareBufferFrame.acquireFence?.let { fence -> - val signaled = fence.await(500) - if (!signaled) { - Log.w(TAG, "Timed out waiting for acquire fence.") - } - fence.close() - } - return GlTextureFrame.Builder( - GlTextureInfo(texture, C.INDEX_UNSET, C.INDEX_UNSET, frameWidth, frameHeight), - directExecutor(), - { glTextureInfo -> - // TODO: b/474075198 - Use a more efficient sync method. - GLES20.glFinish() - GlUtil.deleteTexture(glTextureInfo.texId) - if (!nativeDestroyEGLImage(GlUtil.getDefaultEglDisplay().nativeHandle, eglImage)) { - errorConsumer.accept( - VideoFrameProcessingException( - "eglDestroyImageKHR", - hardwareBufferFrame.presentationTimeUs, - ) - ) - } - hardwareBufferFrame.release(/* releaseFence= */ null) - }, - ) - .setPresentationTimeUs(hardwareBufferFrame.presentationTimeUs) - .setReleaseTimeNs(hardwareBufferFrame.releaseTimeNs) - .setFormat(hardwareBufferFrame.format) - .build() - } - - private fun maybeSetupGlResources() { - if (eglContext != null) { - return - } - val eglDisplay = GlUtil.getDefaultEglDisplay() - eglContext = - glObjectsProvider.createEglContext( - eglDisplay, - /* openGlVersion= */ 3, - // TODO: b/474075198 - Add HDR support. - /* configAttributes= */ GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888, - ) - eglSurface = glObjectsProvider.createFocusedPlaceholderEglSurface(eglContext!!, eglDisplay) - } - - override suspend fun release() { - if (eglContext != null) { - try { - glShaderProgramPacketProcessor?.release() - glShaderProgramPacketProcessor = null - GlUtil.destroyEglSurface(GlUtil.getDefaultEglDisplay(), eglSurface) - // eglContext is released by GlObjectsProvider - } catch (e: GlUtil.GlException) { - errorConsumer.accept(e) - } - } - } - - companion object { - init { - System.loadLibrary("hardwareBufferJNI") - } - - private const val TAG = "HBToGlTexture" - - fun constructTransformationMatrix(hardwareBufferFrame: HardwareBufferFrame): FloatArray { - // TODO: b/327467890 - This should work on most devices, but it's better to get the matrix - // directly from MediaCodec. - val hardwareBuffer = checkNotNull(hardwareBufferFrame.hardwareBuffer) - val format = hardwareBufferFrame.format - - // y' = 1 - y - val flipMatrix = Matrix() - flipMatrix.setScale(1f, -1f) - flipMatrix.postTranslate(0f, 1f) - - // Rotate back around the center. - val rotateMatrix = Matrix() - rotateMatrix.setRotate(-format.rotationDegrees.toFloat(), 0.5f, 0.5f) - - val cropMatrix = Matrix() - val croppedWidth = format.width.toFloat() - val croppedHeight = format.height.toFloat() - val bufferWidth = hardwareBuffer.width.toFloat() - val bufferHeight = hardwareBuffer.height.toFloat() - cropMatrix.setScale(croppedWidth / bufferWidth, croppedHeight / bufferHeight) - - // Applies flipping, rotation and cropping, in order - val transformMatrix = Matrix() - transformMatrix.setConcat(rotateMatrix, flipMatrix) - transformMatrix.setConcat(cropMatrix, transformMatrix) - return MatrixUtils.getGlMatrixArray(transformMatrix) - } - } -} diff --git a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/NdkCompositionPlayerBuilder.java b/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/NdkCompositionPlayerBuilder.java deleted file mode 100644 index 487d68cfaed..00000000000 --- a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/NdkCompositionPlayerBuilder.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2026 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.effect.ndk; - -import android.content.Context; -import androidx.annotation.RequiresApi; -import androidx.media3.common.util.ExperimentalApi; -import androidx.media3.common.util.Util; -import androidx.media3.transformer.CompositionPlayer; - -/** - * Helper class to create a {@link CompositionPlayer.Builder} that has {@link - * androidx.media3.effect.ndk} dependencies injected. - */ -@RequiresApi(26) -@ExperimentalApi // TODO: b/474075198: Remove once FrameConsumer API is stable. -public class NdkCompositionPlayerBuilder { - - /** - * Utility method for creating a {@link CompositionPlayer.Builder} that has {@link - * androidx.media3.effect.ndk} dependencies injected. - * - *

Callers must set {@link - * androidx.media3.transformer.CompositionPlayer.Builder#setPacketConsumerFactory} on the returned - * builder before using it, in order to inject the effects processing pipeline. - * - * @param context The {@link Context}. - * @return A {@link androidx.media3.transformer.CompositionPlayer.Builder} that can be built upon. - */ - public static CompositionPlayer.Builder create(Context context) { - return new CompositionPlayer.Builder(context) - .setHardwareBufferPostProcessor( - new BitmapToHardwareBufferProcessor( - Util.newSingleThreadExecutor("BitmapToHardwareBufferProcessor::Thread"))); - } - - private NdkCompositionPlayerBuilder() {} -} diff --git a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/NdkTransformerBuilder.java b/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/NdkTransformerBuilder.java deleted file mode 100644 index 43292dd300d..00000000000 --- a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/NdkTransformerBuilder.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2026 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.effect.ndk; - -import android.content.Context; -import androidx.annotation.RequiresApi; -import androidx.media3.common.util.ExperimentalApi; -import androidx.media3.effect.GlTextureFrameRenderer.Listener.NO_OP; -import androidx.media3.effect.RenderingPacketConsumer; -import androidx.media3.transformer.Transformer; - -/** - * Helper class to create a {@link Transformer.Builder} that has {@link androidx.media3.effect.ndk} - * dependencies injected. - */ -@RequiresApi(26) -@ExperimentalApi // TODO: b/474075198: Remove once FrameConsumer API is stable. -public class NdkTransformerBuilder { - - /** - * Utility method for creating a {@link Transformer.Builder} that has {@link - * androidx.media3.effect.ndk} dependencies injected. - * - *

Callers must set {@link - * androidx.media3.transformer.Transformer.Builder#setHardwareBufferEffectsPipeline(RenderingPacketConsumer)} - * on the returned builder before using it, in order to inject the effects processing pipeline. - * - * @param context The {@link Context}. - * @return A {@link androidx.media3.transformer.Transformer.Builder} that can be built upon. - */ - public static Transformer.Builder create(Context context) { - HardwareBufferSurfaceRenderer hardwareBufferRenderer = - HardwareBufferSurfaceRenderer.create(context, NO_OP.INSTANCE, /* errorConsumer= */ e -> {}); - return new Transformer.Builder(context).setHardwareBufferRenderer(hardwareBufferRenderer); - } - - private NdkTransformerBuilder() {} -} diff --git a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/package-info.java b/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/package-info.java deleted file mode 100644 index 5a508075de2..00000000000 --- a/libraries/effect_ndk/src/main/java/androidx/media3/effect/ndk/package-info.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2026 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. - */ -@NonNullApi -package androidx.media3.effect.ndk; - -import androidx.media3.common.util.NonNullApi; diff --git a/libraries/effect_ndk/src/main/jni/CMakeLists.txt b/libraries/effect_ndk/src/main/jni/CMakeLists.txt deleted file mode 100644 index 19317e54f6c..00000000000 --- a/libraries/effect_ndk/src/main/jni/CMakeLists.txt +++ /dev/null @@ -1,41 +0,0 @@ -# -# Copyright 2026 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. -# - -cmake_minimum_required(VERSION 3.21.0 FATAL_ERROR) - -# Enable C++11 features. -set(CMAKE_CXX_STANDARD 11) - -project(hardwareBufferJNI C CXX) - -# Add the NDK libraries -find_library(log-lib log) -find_library(android-lib android) -find_library(egl-lib EGL) -find_library(glesv2-lib GLESv2) -find_library(jnigraphics-lib jnigraphics) - -add_library(${PROJECT_NAME} SHARED hardware_buffer_jni.cc) - -target_link_options(${PROJECT_NAME} PRIVATE "-Wl,-z,max-page-size=16384") - -target_link_libraries(${PROJECT_NAME} - ${log-lib} - ${android-lib} - ${egl-lib} - ${glesv2-lib} - ${jnigraphics-lib} -) diff --git a/libraries/effect_ndk/src/main/jni/hardware_buffer_jni.cc b/libraries/effect_ndk/src/main/jni/hardware_buffer_jni.cc deleted file mode 100644 index 0bce0f107e0..00000000000 --- a/libraries/effect_ndk/src/main/jni/hardware_buffer_jni.cc +++ /dev/null @@ -1,233 +0,0 @@ -#include -#ifdef __ANDROID__ -#include -#include -#include -#include -#include -#endif // __ANDROID__ -#include -#include -#include -#include - -#ifdef __ANDROID__ -#define API_AT_LEAST(x) __builtin_available(android x, *) -#define LOG_TAG "hardwarebufferJNI" -#define LOGE(...) \ - ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)) -#else // __ANDROID__ -#define LOGE(...) \ - do { \ - } while (0) -#endif // __ANDROID__ - -static PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR = nullptr; -static PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC eglGetNativeClientBufferANDROID = - nullptr; -static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES = - nullptr; -static PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR = nullptr; - -static AHardwareBuffer* (*s_AHardwareBuffer_fromHardwareBuffer)( - JNIEnv*, jobject) = nullptr; -static void (*s_AHardwareBuffer_describe)(const AHardwareBuffer*, - AHardwareBuffer_Desc*) = nullptr; -static int (*s_AHardwareBuffer_lock)(AHardwareBuffer*, uint64_t, int32_t, - const ARect*, void**) = nullptr; -static int (*s_AHardwareBuffer_unlock)(AHardwareBuffer*, int32_t*) = nullptr; - -static void initializeEGLFunctions() { - if (!eglCreateImageKHR) { - eglCreateImageKHR = - (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR"); - } - if (!eglGetNativeClientBufferANDROID) { - eglGetNativeClientBufferANDROID = - (PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC)eglGetProcAddress( - "eglGetNativeClientBufferANDROID"); - } - if (!glEGLImageTargetTexture2DOES) { - glEGLImageTargetTexture2DOES = - (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress( - "glEGLImageTargetTexture2DOES"); - } - if (!eglDestroyImageKHR) { - eglDestroyImageKHR = - (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR"); - } -} - -jint JNI_OnLoad(JavaVM* vm, void* reserved) { - JNIEnv* env; - if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { - return -1; - } - - if (API_AT_LEAST(26)) { - s_AHardwareBuffer_fromHardwareBuffer = AHardwareBuffer_fromHardwareBuffer; - s_AHardwareBuffer_describe = AHardwareBuffer_describe; - s_AHardwareBuffer_lock = AHardwareBuffer_lock; - s_AHardwareBuffer_unlock = AHardwareBuffer_unlock; - } else { - LOGE( - "AHardwareBuffer APIs are not available on this Android version (API < " - "26)"); - return -1; - } - - initializeEGLFunctions(); - if (eglCreateImageKHR == nullptr || - eglGetNativeClientBufferANDROID == nullptr || - glEGLImageTargetTexture2DOES == nullptr || - eglDestroyImageKHR == nullptr) { - LOGE("Failed to get addresses of GL/EGL functions."); - return -1; - } - return JNI_VERSION_1_6; -} - -extern "C" JNIEXPORT jlong JNICALL -Java_androidx_media3_effect_ndk_HardwareBufferJni_nativeCreateEglImageFromHardwareBuffer( - JNIEnv* env, jobject /* this */, jlong displayHandle, - jobject hardwareBufferJava) { - EGLDisplay display = reinterpret_cast(displayHandle); - if (display == EGL_NO_DISPLAY) { - LOGE("Invalid EGL display"); - return 0; // EGL_NO_IMAGE_KHR - } - - AHardwareBuffer* hardwareBuffer = - s_AHardwareBuffer_fromHardwareBuffer(env, hardwareBufferJava); - if (!hardwareBuffer) { - LOGE("Null hardware buffer"); - return 0; // EGL_NO_IMAGE_KHR - } - - EGLClientBuffer clientBuffer = - eglGetNativeClientBufferANDROID(hardwareBuffer); - const EGLint attrs[] = {EGL_NONE}; - EGLImageKHR eglImage = eglCreateImageKHR( - display, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, clientBuffer, attrs); - - if (eglImage == EGL_NO_IMAGE_KHR) { - LOGE("eglCreateImageKHR failed with error 0x%x", eglGetError()); - return 0; // EGL_NO_IMAGE_KHR - } - - return (jlong)eglImage; -} - -extern "C" JNIEXPORT jboolean JNICALL -Java_androidx_media3_effect_ndk_HardwareBufferJni_nativeBindEGLImage( - JNIEnv* env, jobject clazz, jint target, jlong eglImageHandle) { - if (eglImageHandle == 0) { - LOGE("Invalid eglImageHandle (0)"); - return JNI_FALSE; - } - - EGLImageKHR image = reinterpret_cast(eglImageHandle); - - glEGLImageTargetTexture2DOES(static_cast(target), - (GLeglImageOES)image); - - GLenum error = glGetError(); - if (error != GL_NO_ERROR) { - LOGE("glEGLImageTargetTexture2DOES failed: 0x%x", error); - return JNI_FALSE; - } - return JNI_TRUE; -} - -extern "C" JNIEXPORT jboolean JNICALL -Java_androidx_media3_effect_ndk_HardwareBufferJni_nativeDestroyEGLImage( - JNIEnv* env, jobject clazz, jlong displayHandle, jlong imageHandle) { - if (imageHandle == 0) { - LOGE("Invalid eglImageHandle (0)"); - return JNI_FALSE; - } - - EGLDisplay display = reinterpret_cast(displayHandle); - if (display == EGL_NO_DISPLAY) { - return JNI_FALSE; - } - - EGLImageKHR image = reinterpret_cast(imageHandle); - EGLBoolean result = eglDestroyImageKHR(display, image); - - if (result == EGL_TRUE) { - return JNI_TRUE; - } else { - LOGE("eglDestroyImageKHR failed: EGL error 0x%x", eglGetError()); - return JNI_FALSE; - } -} - -extern "C" JNIEXPORT jboolean JNICALL -Java_androidx_media3_effect_ndk_HardwareBufferJni_nativeCopyBitmapToHardwareBuffer( - JNIEnv* env, jobject clazz, jobject bitmap, jobject hardwareBuffer) { - AHardwareBuffer* hb = - s_AHardwareBuffer_fromHardwareBuffer(env, hardwareBuffer); - if (!hb) { - LOGE("Failed to get AHardwareBuffer from jobject"); - return JNI_FALSE; - } - - AndroidBitmapInfo bitmapInfo; - void* bitmapPixels; - if (AndroidBitmap_getInfo(env, bitmap, &bitmapInfo) < 0) { - LOGE("AndroidBitmap_getInfo failed"); - return JNI_FALSE; - } - if (AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels) < 0) { - LOGE("AndroidBitmap_lockPixels failed"); - return JNI_FALSE; - } - if (bitmapInfo.format != ANDROID_BITMAP_FORMAT_RGBA_8888) { - LOGE("Unsupported bitmap format: %d. Only RGBA_8888 is supported.", - bitmapInfo.format); - return JNI_FALSE; - } - - AHardwareBuffer_Desc hbDesc; - s_AHardwareBuffer_describe(hb, &hbDesc); - if (hbDesc.width != bitmapInfo.width || hbDesc.height != bitmapInfo.height) { - LOGE("HardwareBuffer dimensions do not match bitmap dimensions."); - return JNI_FALSE; - } - if (hbDesc.format != AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM) { - LOGE("Unsupported hardware buffer format. Only RGBA_8888 is supported."); - return JNI_FALSE; - } - if (!(hbDesc.usage & AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN)) { - LOGE( - "Unsupported hardware buffer usage. CPU_WRITE_OFTEN must be " - "supported."); - return JNI_FALSE; - } - - void* hbPixels; - jboolean success = JNI_FALSE; - if (s_AHardwareBuffer_lock(hb, AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, -1, - nullptr, &hbPixels) == 0) { - uint8_t* src = static_cast(bitmapPixels); - uint8_t* dst = static_cast(hbPixels); - - // As format is RGBA_8888. - const int bpp = 4; - const size_t rowSize = bitmapInfo.width * bpp; - - for (uint32_t y = 0; y < bitmapInfo.height; ++y) { - memcpy(dst + (y * hbDesc.stride * bpp), src + (y * bitmapInfo.stride), - rowSize); - } - - s_AHardwareBuffer_unlock(hb, nullptr); - success = JNI_TRUE; - } else { - LOGE("AHardwareBuffer_lock failed"); - } - - AndroidBitmap_unlockPixels(env, bitmap); - return success; -} diff --git a/libraries/exoplayer/build.gradle b/libraries/exoplayer/build.gradle index 02605e61874..dd0549199b1 100644 --- a/libraries/exoplayer/build.gradle +++ b/libraries/exoplayer/build.gradle @@ -14,7 +14,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget -apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" +apply plugin: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" apply plugin: 'kotlin-android' android { diff --git a/libraries/exoplayer/proguard-rules.txt b/libraries/exoplayer/proguard-rules.txt index 878634a6b61..8f7c9ad9ea6 100644 --- a/libraries/exoplayer/proguard-rules.txt +++ b/libraries/exoplayer/proguard-rules.txt @@ -23,7 +23,7 @@ } -dontnote androidx.media3.decoder.iamf.IamfAudioRenderer$Builder -keepclasseswithmembers class androidx.media3.decoder.iamf.IamfAudioRenderer$Builder { - (android.content.Context, androidx.media3.exoplayer.audio.AudioSink); + (androidx.media3.exoplayer.audio.AudioSink); androidx.media3.decoder.iamf.IamfAudioRenderer$Builder setEventHandlerAndListener(android.os.Handler, androidx.media3.exoplayer.audio.AudioRendererEventListener); androidx.media3.decoder.iamf.IamfAudioRenderer$Builder setOutputSampleType(int); androidx.media3.decoder.iamf.IamfAudioRenderer$Builder setChannelOrdering(int); diff --git a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/audio/IamfUtilTest.java b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/audio/IamfUtilTest.java index d755f3faf42..030e5e28584 100644 --- a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/audio/IamfUtilTest.java +++ b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/audio/IamfUtilTest.java @@ -18,11 +18,11 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; -import android.content.Context; import android.media.AudioFormat; -import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SdkSuppress; +import com.google.common.collect.ImmutableList; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -99,28 +99,128 @@ public void getChannelMaskForOutputLayout_invalidThrows() { } @Test - public void getOutputLayoutForCurrentConfiguration_returnsDefaultWithoutSpatializer() { - Context context = ApplicationProvider.getApplicationContext(); + public void iamfSupportedLayouts_allAreConvertableToLayout() { + for (int channelMask : IamfUtil.IAMF_SUPPORTED_CHANNEL_MASKS) { + assertThat(IamfUtil.getOutputLayoutForChannelMask(channelMask)) + .isNotEqualTo(IamfUtil.OUTPUT_LAYOUT_UNSET); + } + } + + @Test + public void getOutputLayoutForCurrentConfiguration_defaultWhenNoInformation() { + AudioCapabilities audioCapabilities = + createAudioCapabilities( + /* speakerLayoutChannelMasks= */ ImmutableList.of(), + /* spatializerChannelMasks= */ ImmutableList.of()); boolean useIntegratedBinauralRenderer = true; // Does not matter, no Spatializer. assertThat( - IamfUtil.getOutputLayoutForCurrentConfiguration(context, useIntegratedBinauralRenderer)) + IamfUtil.getOutputLayoutForCurrentConfiguration( + audioCapabilities, useIntegratedBinauralRenderer)) .isEqualTo(IamfUtil.OUTPUT_LAYOUT_ITU2051_SOUND_SYSTEM_A_0_2_0); } @Test - public void getOutputChannelMaskForCurrentConfiguration_returnsDefaultWithoutSpatializer() { - Context context = ApplicationProvider.getApplicationContext(); + public void getOutputLayoutForCurrentConfiguration_picksFirstCompatibleSpeakerLayout() { + AudioCapabilities audioCapabilities = + createAudioCapabilities( + /* speakerLayoutChannelMasks= */ ImmutableList.of( + AudioFormat.CHANNEL_OUT_QUAD, // Not an IAMF layout. + AudioFormat.CHANNEL_OUT_5POINT1, // Chosen. + AudioFormat.CHANNEL_OUT_7POINT1_SURROUND // Never reached. + ), + /* spatializerChannelMasks= */ ImmutableList.of()); + boolean useIntegratedBinauralRenderer = true; // Does not matter, no Spatializer. + + assertThat( + IamfUtil.getOutputLayoutForCurrentConfiguration( + audioCapabilities, useIntegratedBinauralRenderer)) + .isEqualTo(IamfUtil.OUTPUT_LAYOUT_ITU2051_SOUND_SYSTEM_B_0_5_0); + } + + @Test + public void getOutputLayoutForCurrentConfiguration_prefersSpatializerOverSpeakerLayout() { + AudioCapabilities audioCapabilities = + createAudioCapabilities( + /* speakerLayoutChannelMasks= */ ImmutableList.of( + AudioFormat.CHANNEL_OUT_5POINT1 // Not chosen. + ), + /* spatializerChannelMasks= */ ImmutableList.of( + AudioFormat.CHANNEL_OUT_7POINT1_SURROUND // Chosen. + )); + // We won't use decoder built-in binaural, and we'll expect the Spatializer layout. + boolean useIntegratedBinauralRenderer = false; + + assertThat( + IamfUtil.getOutputLayoutForCurrentConfiguration( + audioCapabilities, useIntegratedBinauralRenderer)) + .isEqualTo(IamfUtil.OUTPUT_LAYOUT_ITU2051_SOUND_SYSTEM_I_0_7_0); + } + + @Test + public void getOutputLayoutForCurrentConfiguration_usesIntegratedBinauralRenderer() { + AudioCapabilities audioCapabilities = + createAudioCapabilities( + /* speakerLayoutChannelMasks= */ ImmutableList.of(AudioFormat.CHANNEL_OUT_5POINT1), + /* spatializerChannelMasks= */ ImmutableList.of( + AudioFormat.CHANNEL_OUT_7POINT1_SURROUND)); + // Spatializer is enabled and available, so we use decoder built-in binaural. + boolean useIntegratedBinauralRenderer = true; + + assertThat( + IamfUtil.getOutputLayoutForCurrentConfiguration( + audioCapabilities, useIntegratedBinauralRenderer)) + .isEqualTo(IamfUtil.OUTPUT_LAYOUT_BINAURAL); + } - assertThat(IamfUtil.getOutputChannelMaskForCurrentConfiguration(context)) + @Test + public void getOutputChannelMaskForCurrentConfiguration_defaultWhenNoInformation() { + AudioCapabilities audioCapabilities = + createAudioCapabilities( + /* speakerLayoutChannelMasks= */ ImmutableList.of(), + /* spatializerChannelMasks= */ ImmutableList.of()); + + assertThat(IamfUtil.getOutputChannelMaskForCurrentConfiguration(audioCapabilities)) .isEqualTo(AudioFormat.CHANNEL_OUT_STEREO); } @Test - public void iamfSupportedLayouts_allAreConvertableToLayout() { - for (int channelMask : IamfUtil.IAMF_SUPPORTED_CHANNEL_MASKS) { - assertThat(IamfUtil.getOutputLayoutForChannelMask(channelMask)) - .isNotEqualTo(IamfUtil.OUTPUT_LAYOUT_UNSET); - } + public void getOutputChannelMaskForCurrentConfiguration_picksFirstCompatibleSpeakerLayout() { + AudioCapabilities audioCapabilities = + createAudioCapabilities( + /* speakerLayoutChannelMasks= */ ImmutableList.of( + AudioFormat.CHANNEL_OUT_QUAD, // Not an IAMF layout. + AudioFormat.CHANNEL_OUT_5POINT1, // Chosen. + AudioFormat.CHANNEL_OUT_7POINT1_SURROUND // Never reached. + ), + /* spatializerChannelMasks= */ ImmutableList.of()); + + assertThat(IamfUtil.getOutputChannelMaskForCurrentConfiguration(audioCapabilities)) + .isEqualTo(AudioFormat.CHANNEL_OUT_5POINT1); + } + + @Test + public void getOutputChannelMaskForCurrentConfiguration_prefersSpatializerOverSpeakerLayout() { + AudioCapabilities audioCapabilities = + createAudioCapabilities( + /* speakerLayoutChannelMasks= */ ImmutableList.of( + AudioFormat.CHANNEL_OUT_5POINT1 // Not chosen. + ), + /* spatializerChannelMasks= */ ImmutableList.of( + AudioFormat.CHANNEL_OUT_7POINT1_SURROUND // Chosen. + )); + + assertThat(IamfUtil.getOutputChannelMaskForCurrentConfiguration(audioCapabilities)) + .isEqualTo(AudioFormat.CHANNEL_OUT_7POINT1_SURROUND); + } + + /** Helper method to create an {@link AudioCapabilities} instance with the given channel masks. */ + private AudioCapabilities createAudioCapabilities( + List speakerLayoutChannelMasks, List spatializerChannelMasks) { + return new AudioCapabilities( + /* supportedEncodings= */ null, // Doesn't matter for these tests. + /* maxChannelCount= */ 99, // Doesn't matter for these tests. + speakerLayoutChannelMasks, + spatializerChannelMasks); } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DecoderReuseEvaluation.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DecoderReuseEvaluation.java index c9ee6c8fe2e..c3edf04adb2 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DecoderReuseEvaluation.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DecoderReuseEvaluation.java @@ -155,7 +155,7 @@ public final class DecoderReuseEvaluation { /** * {@link DecoderDiscardReasons Reasons} why the decoder cannot be reused. Always {@code 0} if - * reuse is possible. May also be {code 0} if reuse is not possible for an unspecified reason. + * reuse is possible. May also be {@code 0} if reuse is not possible for an unspecified reason. */ public final @DecoderDiscardReasons int discardReasons; 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 8a4bf9de536..acf3b198de7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java @@ -748,9 +748,8 @@ protected void buildAudioRenderers( // Full class names used for media3 constructor args so the LINT rule triggers if any move. @SuppressWarnings("UnnecessarilyFullyQualified") Constructor builderConstructor = - builderClass.getConstructor( - Context.class, androidx.media3.exoplayer.audio.AudioSink.class); - Object builder = builderConstructor.newInstance(context, audioSink); + builderClass.getConstructor(androidx.media3.exoplayer.audio.AudioSink.class); + Object builder = builderConstructor.newInstance(audioSink); // Full class names used for media3 constructor args so the LINT rule triggers if any move. @SuppressWarnings("UnnecessarilyFullyQualified") Class audioRenderEventListenerClass = 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 bf28187d4ca..a8fa7126449 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -422,15 +422,22 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer) audioSessionIdState = new BackgroundThreadStateHandler<>( - AUDIO_SESSION_ID_UNSET, - playbackLooper, - applicationLooper, + /* initialState= */ AUDIO_SESSION_ID_UNSET, + /* backgroundLooper= */ playbackLooper, + /* foregroundLooper= */ applicationLooper, clock, /* onStateChanged= */ this::onAudioSessionIdChanged); audioSessionIdState.runInBackground( - () -> - audioSessionIdState.setStateInBackground( - Util.generateAudioSessionIdV21(applicationContext))); + () -> { + int newAudioSessionId = Util.generateAudioSessionIdV21(applicationContext); + if (audioSessionIdState.get() != newAudioSessionId) { + audioSessionIdState.setStateInBackground(newAudioSessionId); + // Provide the audio session ID to the renderers on playback thread to prevent race + // condition with player preparation. + sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, newAudioSessionId); + sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, newAudioSessionId); + } + }); audioBecomingNoisyManager = new AudioBecomingNoisyManager( builder.context, playbackLooper, builder.looper, componentListener, clock); 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 8a7b3c50250..88f91140f11 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -69,6 +69,7 @@ import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.drm.DrmSession; +import androidx.media3.exoplayer.image.ImageMetadataListener; import androidx.media3.exoplayer.source.BehindLiveWindowException; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; @@ -180,6 +181,7 @@ public interface PlaybackInfoUpdateListener { private static final int MSG_SET_SCRUBBING_MODE_ENABLED = 36; private static final int MSG_SEEK_COMPLETED_IN_SCRUBBING_MODE = 37; private static final int MSG_SET_SCRUBBING_MODE_PARAMETERS = 38; + private static final int MSG_SET_IMAGE_METADATA_LISTENER = 39; private static final long BUFFERING_MAXIMUM_INTERVAL_MS = Util.usToMs(Renderer.DEFAULT_DURATION_TO_PROGRESS_US); @@ -379,6 +381,16 @@ public ExoPlayerImplInternal( handler .obtainMessage(MSG_SET_VIDEO_FRAME_METADATA_LISTENER, internalVideoFrameMetadataListener) .sendToTarget(); + + ImageMetadataListener internalOnImageAvailableListener = + (presentationTimeUs, format) -> { + if (seekIsPendingWhileScrubbing) { + handler.obtainMessage(MSG_SEEK_COMPLETED_IN_SCRUBBING_MODE).sendToTarget(); + } + }; + handler + .obtainMessage(MSG_SET_IMAGE_METADATA_LISTENER, internalOnImageAvailableListener) + .sendToTarget(); } private MediaPeriodHolder createMediaPeriodHolder( @@ -539,6 +551,13 @@ private void setVideoFrameMetadataListenerInternal( } } + private void setImageMetadataListenerInternal(ImageMetadataListener imageMetadataListener) + throws ExoPlaybackException { + for (RendererHolder renderer : renderers) { + renderer.setImageMetadataListener(imageMetadataListener); + } + } + @Override public void sendMessage(PlayerMessage message) { if (releasedOnApplicationThread || !playbackLooper.getThread().isAlive()) { @@ -811,6 +830,9 @@ public boolean handleMessage(Message msg) { case MSG_SET_VIDEO_FRAME_METADATA_LISTENER: setVideoFrameMetadataListenerInternal((VideoFrameMetadataListener) msg.obj); break; + case MSG_SET_IMAGE_METADATA_LISTENER: + setImageMetadataListenerInternal((ImageMetadataListener) msg.obj); + break; case MSG_RELEASE: releaseInternal(/* processedCondition= */ (ConditionVariable) msg.obj); // Return immediately to not send playback info updates after release. @@ -1661,17 +1683,6 @@ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackExcepti } } - if (scrubbingModeEnabled) { - for (RendererHolder renderer : renderers) { - // TODO: b/451939261 - Remove video-only condition once image-playback scrubbing mode - // supports skipping intermittent seeks. - if (renderer.isRendererEnabled() && renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - seekIsPendingWhileScrubbing = true; - break; - } - } - } - newPeriodPositionUs = seekToPeriodPosition( periodId, @@ -1776,6 +1787,17 @@ private long seekToPeriodPosition( // Disable pre-warming as following logic will reset any pre-warming media periods. disableAndResetPrewarmingRenderers(); + if (scrubbingModeEnabled) { + for (RendererHolder renderer : renderers) { + if (renderer.isRendererEnabled() + && (renderer.getTrackType() == C.TRACK_TYPE_VIDEO + || renderer.getTrackType() == C.TRACK_TYPE_IMAGE)) { + seekIsPendingWhileScrubbing = true; + break; + } + } + } + // Do the actual seeking. if (newPlayingPeriodHolder != null) { queue.removeAfter(newPlayingPeriodHolder); @@ -2479,14 +2501,11 @@ private void handleMediaSourceListInfoRefreshed(Timeline timeline, boolean isSou queue, repeatMode, shuffleModeEnabled, + isSourceRefresh, window, period); MediaPeriodId newPeriodId = positionUpdate.periodId; - long newRequestedContentPositionUs = positionUpdate.requestedContentPositionUs; - boolean forceBufferingState = positionUpdate.forceBufferingState; long newPositionUs = positionUpdate.periodPositionUs; - boolean periodPositionChanged = - !playbackInfo.periodId.equals(newPeriodId) || newPositionUs != playbackInfo.positionUs; try { if (positionUpdate.endPlayback) { if (playbackInfo.playbackState != Player.STATE_IDLE) { @@ -2501,7 +2520,7 @@ private void handleMediaSourceListInfoRefreshed(Timeline timeline, boolean isSou for (RendererHolder rendererHolder : renderers) { rendererHolder.setTimeline(timeline); } - if (!periodPositionChanged) { + if (!positionUpdate.periodPositionChanged) { // We can keep the current playing period. Update the rest of the queued periods. long maxRendererReadPositionUs = queue.getReadingPeriod() == null @@ -2535,7 +2554,8 @@ private void handleMediaSourceListInfoRefreshed(Timeline timeline, boolean isSou } periodHolder = periodHolder.getNext(); } - newPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs, forceBufferingState); + newPositionUs = + seekToPeriodPosition(newPeriodId, newPositionUs, positionUpdate.forceBufferingState); } } finally { updatePlaybackSpeedSettingsForNewPeriod( @@ -2547,25 +2567,18 @@ private void handleMediaSourceListInfoRefreshed(Timeline timeline, boolean isSou ? newPositionUs : C.TIME_UNSET, /* forceSetTargetOffsetOverride= */ false); - if (periodPositionChanged - || newRequestedContentPositionUs != playbackInfo.requestedContentPositionUs) { - Object oldPeriodUid = playbackInfo.periodId.periodUid; - Timeline oldTimeline = playbackInfo.timeline; - boolean reportDiscontinuity = - periodPositionChanged - && isSourceRefresh - && !oldTimeline.isEmpty() - && !oldTimeline.getPeriodByUid(oldPeriodUid, period).isPlaceholder; + if (positionUpdate.periodPositionChanged + || positionUpdate.requestedContentPositionUs != playbackInfo.requestedContentPositionUs) { playbackInfo = handlePositionDiscontinuity( newPeriodId, newPositionUs, - newRequestedContentPositionUs, - reportDiscontinuity ? newPositionUs : playbackInfo.discontinuityStartPositionUs, - reportDiscontinuity, - timeline.getIndexOfPeriod(oldPeriodUid) == C.INDEX_UNSET - ? Player.DISCONTINUITY_REASON_REMOVE - : Player.DISCONTINUITY_REASON_SKIP); + positionUpdate.requestedContentPositionUs, + positionUpdate.reportDiscontinuity + ? newPositionUs + : playbackInfo.discontinuityStartPositionUs, + positionUpdate.reportDiscontinuity, + positionUpdate.discontinuityReason); } resetPendingPauseAtEndOfPeriod(); resolvePendingMessagePositions( @@ -3537,16 +3550,29 @@ private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange( MediaPeriodQueue queue, @RepeatMode int repeatMode, boolean shuffleModeEnabled, + boolean isSourceRefresh, Timeline.Window window, Timeline.Period period) { if (timeline.isEmpty()) { + MediaPeriodId newPeriodId = PlaybackInfo.getDummyPeriodForEmptyTimeline(); + boolean periodPositionChanged = + !newPeriodId.equals(playbackInfo.periodId) || playbackInfo.positionUs != 0; + boolean reportDiscontinuity = + periodPositionChanged + && isSourceRefresh + && !playbackInfo.timeline.isEmpty() + && !playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period) + .isPlaceholder; return new PositionUpdateForPlaylistChange( - PlaybackInfo.getDummyPeriodForEmptyTimeline(), + newPeriodId, /* periodPositionUs= */ 0, /* requestedContentPositionUs= */ C.TIME_UNSET, /* forceBufferingState= */ false, /* endPlayback= */ true, - /* setTargetLiveOffset= */ false); + /* setTargetLiveOffset= */ false, + /* periodPositionChanged= */ periodPositionChanged, + /* reportDiscontinuity= */ reportDiscontinuity, + /* discontinuityReason= */ Player.DISCONTINUITY_REASON_REMOVE); } MediaPeriodId oldPeriodId = playbackInfo.periodId; Object newPeriodUid = oldPeriodId.periodUid; @@ -3720,14 +3746,40 @@ private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange( newContentPositionUs = periodPositionUs; } } - + boolean periodPositionChanged = + !newPeriodId.equals(playbackInfo.periodId) || periodPositionUs != playbackInfo.positionUs; + @DiscontinuityReason + int discontinuityReason = + timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid) == C.INDEX_UNSET + ? Player.DISCONTINUITY_REASON_REMOVE + : Player.DISCONTINUITY_REASON_SKIP; + if (newPeriodId.periodUid.equals(playbackInfo.periodId.periodUid) + && newPeriodId.adGroupIndex != C.INDEX_UNSET) { + AdPlaybackState adPlaybackState = + timeline.getPeriodByUid(newPeriodId.periodUid, period).adPlaybackState; + AdGroup adGroup = adPlaybackState.getAdGroup(newPeriodId.adGroupIndex); + if (newPeriodId.adIndexInAdGroup >= adGroup.states.length + || adGroup.states[newPeriodId.adIndexInAdGroup] != AdPlaybackState.AD_STATE_SKIPPED) { + // An ad inserted into the timeline causes a sudden change of the playing period. + discontinuityReason = Player.DISCONTINUITY_REASON_AUTO_TRANSITION; + } + } + boolean reportDiscontinuity = + periodPositionChanged + && isSourceRefresh + && !playbackInfo.timeline.isEmpty() + && !playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period) + .isPlaceholder; return new PositionUpdateForPlaylistChange( newPeriodId, periodPositionUs, newContentPositionUs, forceBufferingState, endPlayback, - setTargetLiveOffset); + setTargetLiveOffset, + periodPositionChanged, + reportDiscontinuity, + discontinuityReason); } private static boolean isIgnorableServerSideAdInsertionPeriodChange( @@ -4052,6 +4104,9 @@ private static final class PositionUpdateForPlaylistChange { public final boolean forceBufferingState; public final boolean endPlayback; public final boolean setTargetLiveOffset; + private final boolean periodPositionChanged; + private final boolean reportDiscontinuity; + private final @DiscontinuityReason int discontinuityReason; public PositionUpdateForPlaylistChange( MediaPeriodId periodId, @@ -4059,13 +4114,19 @@ public PositionUpdateForPlaylistChange( long requestedContentPositionUs, boolean forceBufferingState, boolean endPlayback, - boolean setTargetLiveOffset) { + boolean setTargetLiveOffset, + boolean periodPositionChanged, + boolean reportDiscontinuity, + int discontinuityReason) { this.periodId = periodId; this.periodPositionUs = periodPositionUs; this.requestedContentPositionUs = requestedContentPositionUs; this.forceBufferingState = forceBufferingState; this.endPlayback = endPlayback; this.setTargetLiveOffset = setTargetLiveOffset; + this.periodPositionChanged = periodPositionChanged; + this.reportDiscontinuity = reportDiscontinuity; + this.discontinuityReason = discontinuityReason; } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetrieverInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetrieverInternal.java index 949c144db3c..c284c82ebf6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetrieverInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetrieverInternal.java @@ -341,45 +341,54 @@ public boolean handleMessage(Message msg) { if (released) { return true; } - switch (msg.what) { - case MESSAGE_PREPARE_SOURCE: - MediaItem mediaItem = (MediaItem) msg.obj; - mediaSource = mediaSourceFactory.createMediaSource(mediaItem); - mediaSource.prepareSource( - mediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET); - mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); - return true; - case MESSAGE_CHECK_FOR_FAILURE: - try { - if (mediaPeriod == null) { - checkNotNull(mediaSource).maybeThrowSourceInfoRefreshError(); - } else { - mediaPeriod.maybeThrowPrepareError(); + try { + switch (msg.what) { + case MESSAGE_PREPARE_SOURCE: + MediaItem mediaItem = (MediaItem) msg.obj; + mediaSource = mediaSourceFactory.createMediaSource(mediaItem); + mediaSource.prepareSource( + mediaSourceCaller, /* mediaTransferListener= */ null, PlayerId.UNSET); + mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); + return true; + case MESSAGE_CHECK_FOR_FAILURE: + try { + if (mediaPeriod == null) { + checkNotNull(mediaSource).maybeThrowSourceInfoRefreshError(); + } else { + mediaPeriod.maybeThrowPrepareError(); + } + mediaSourceHandler.sendEmptyMessageDelayed( + MESSAGE_CHECK_FOR_FAILURE, /* delayMs= */ ERROR_POLL_INTERVAL_MS); + } catch (IOException e) { + onFailureListener.onFailure(e); + release(); } - mediaSourceHandler.sendEmptyMessageDelayed( - MESSAGE_CHECK_FOR_FAILURE, /* delayMs= */ ERROR_POLL_INTERVAL_MS); - } catch (IOException e) { - onFailureListener.onFailure(e); - release(); - } - return true; - case MESSAGE_CONTINUE_LOADING: - checkNotNull(mediaPeriod) - .continueLoading(new LoadingInfo.Builder().setPlaybackPositionUs(0).build()); - return true; - case MESSAGE_RELEASE: - if (mediaPeriod != null) { - checkNotNull(mediaSource).releasePeriod(mediaPeriod); - } - if (mediaSource != null) { - mediaSource.releaseSource(mediaSourceCaller); - } - mediaSourceHandler.removeCallbacksAndMessages(/* token= */ null); - SHARED_WORKER_THREAD.removeWorker(); - released = true; - return true; - default: - return false; + return true; + case MESSAGE_CONTINUE_LOADING: + checkNotNull(mediaPeriod) + .continueLoading(new LoadingInfo.Builder().setPlaybackPositionUs(0).build()); + return true; + case MESSAGE_RELEASE: + try { + if (mediaPeriod != null) { + checkNotNull(mediaSource).releasePeriod(mediaPeriod); + } + if (mediaSource != null) { + mediaSource.releaseSource(mediaSourceCaller); + } + } finally { + mediaSourceHandler.removeCallbacksAndMessages(/* token= */ null); + SHARED_WORKER_THREAD.removeWorker(); + released = true; + } + return true; + default: + return false; + } + } catch (RuntimeException e) { + onFailureListener.onFailure(e); + release(); + return true; } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java index a005cb34cc0..2a957d9bbf8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java @@ -34,6 +34,7 @@ import androidx.media3.common.util.Util; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.audio.AudioOutputProvider; +import androidx.media3.exoplayer.image.ImageMetadataListener; import androidx.media3.exoplayer.image.ImageOutput; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; @@ -230,7 +231,8 @@ interface WakeupListener { MSG_SET_VIRTUAL_DEVICE_ID, MSG_SET_AUDIO_OUTPUT_PROVIDER, MSG_SET_CODEC_PARAMETERS, - MSG_SET_SUBSCRIBED_CODEC_PARAMETER_KEYS + MSG_SET_SUBSCRIBED_CODEC_PARAMETER_KEYS, + MSG_SET_IMAGE_METADATA_LISTENER }) public @interface MessageType {} @@ -409,6 +411,13 @@ interface WakeupListener { */ int MSG_SET_SUBSCRIBED_CODEC_PARAMETER_KEYS = 22; + /** + * The type of a message that can be passed to an image renderer via {@link + * ExoPlayer#createMessage(PlayerMessage.Target)}. The message payload should be an {@link + * ImageMetadataListener} instance, or null. + */ + int MSG_SET_IMAGE_METADATA_LISTENER = 23; + /** * Applications or extensions may define custom {@code MSG_*} constants that can be passed to * renderers. These custom constants must be greater than or equal to this value. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererHolder.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererHolder.java index a8c39a56c50..ffe62fd802d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererHolder.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererHolder.java @@ -32,6 +32,8 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Timeline; +import androidx.media3.common.util.Log; +import androidx.media3.exoplayer.image.ImageMetadataListener; import androidx.media3.exoplayer.metadata.MetadataRenderer; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource; @@ -49,6 +51,8 @@ /** Holds a {@link Renderer renderer}. */ /* package */ class RendererHolder { + private static final String TAG = "RendererHolder"; + private final Renderer primaryRenderer; // Index of renderer in renderer list held by the {@link Player}. private final int index; @@ -612,9 +616,19 @@ public void disablePrewarming(DefaultMediaClock mediaClock) { || prewarmingState == RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY; boolean isSecondaryActiveRenderer = prewarmingState == RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY; - disableRenderer( - isPrewarmingPrimary ? primaryRenderer : checkNotNull(secondaryRenderer), mediaClock); - maybeResetRenderer(/* resetPrimary= */ isPrewarmingPrimary); + try { + disableRenderer( + isPrewarmingPrimary ? primaryRenderer : checkNotNull(secondaryRenderer), mediaClock); + } catch (RuntimeException e) { + // There's nothing we can do. + Log.e(TAG, "Disable prewarming failed.", e); + } + try { + maybeResetRenderer(/* resetPrimary= */ isPrewarmingPrimary); + } catch (RuntimeException e) { + // There's nothing we can do. + Log.e(TAG, "Reset prewarming failed.", e); + } prewarmingState = isSecondaryActiveRenderer ? RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_SECONDARY @@ -820,7 +834,7 @@ public void setVideoOutput(@Nullable Object videoOutput) throws ExoPlaybackExcep public void setVideoFrameMetadataListener(VideoFrameMetadataListener videoFrameMetadataListener) throws ExoPlaybackException { - if (getTrackType() != TRACK_TYPE_VIDEO && getTrackType() != TRACK_TYPE_IMAGE) { + if (getTrackType() != TRACK_TYPE_VIDEO) { return; } primaryRenderer.handleMessage( @@ -831,6 +845,18 @@ public void setVideoFrameMetadataListener(VideoFrameMetadataListener videoFrameM } } + public void setImageMetadataListener(ImageMetadataListener imageMetadataListener) + throws ExoPlaybackException { + if (getTrackType() != TRACK_TYPE_IMAGE) { + return; + } + primaryRenderer.handleMessage(Renderer.MSG_SET_IMAGE_METADATA_LISTENER, imageMetadataListener); + if (secondaryRenderer != null) { + secondaryRenderer.handleMessage( + Renderer.MSG_SET_IMAGE_METADATA_LISTENER, imageMetadataListener); + } + } + /** Sets the volume on the renderer. */ public void setVolume(float volume) throws ExoPlaybackException { if (getTrackType() != TRACK_TYPE_AUDIO) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java index 2b672af46ad..7a2e0cfca50 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java @@ -68,11 +68,15 @@ public final class AudioCapabilities { private static final ImmutableList DEFAULT_SPEAKER_LAYOUT_CHANNEL_MASKS = ImmutableList.of(CHANNEL_OUT_STEREO); + private static final ImmutableList DEFAULT_EMPTY_SPATIALIZER_CHANNEL_MASKS = + ImmutableList.of(); + /** The minimum audio capabilities supported by all devices. */ public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES = new AudioCapabilities( ImmutableList.of(AudioProfile.DEFAULT_AUDIO_PROFILE), - DEFAULT_SPEAKER_LAYOUT_CHANNEL_MASKS); + DEFAULT_SPEAKER_LAYOUT_CHANNEL_MASKS, + DEFAULT_EMPTY_SPATIALIZER_CHANNEL_MASKS); /** Encodings supported when the device specifies external surround sound. */ @SuppressLint("InlinedApi") // Compile-time access to integer constants defined in API 21. @@ -115,6 +119,17 @@ public static AudioCapabilities getCapabilities(Context context) { return getCapabilities(context, AudioAttributes.DEFAULT, /* routedDevice= */ null); } + /** + * @deprecated Use {@link #getCapabilities(Context, AudioAttributes, AudioDeviceInfo, + * List)} instead. + */ + @Deprecated + public static AudioCapabilities getCapabilities( + Context context, AudioAttributes audioAttributes, @Nullable AudioDeviceInfo routedDevice) { + return getCapabilities( + context, audioAttributes, routedDevice, DEFAULT_EMPTY_SPATIALIZER_CHANNEL_MASKS); + } + /** * Returns the current audio capabilities. * @@ -122,21 +137,31 @@ public static AudioCapabilities getCapabilities(Context context) { * @param audioAttributes The {@link AudioAttributes} to obtain capabilities for. * @param routedDevice The {@link AudioDeviceInfo} audio will be routed to if known, or null to * assume the default route. + * @param spatializerChannelMasks The channel masks supported by the {@link + * android.media.Spatializer}, if enabled/available. * @return The current audio capabilities for the device. */ public static AudioCapabilities getCapabilities( - Context context, AudioAttributes audioAttributes, @Nullable AudioDeviceInfo routedDevice) { - return getCapabilitiesInternal(context, audioAttributes, routedDevice); + Context context, + AudioAttributes audioAttributes, + @Nullable AudioDeviceInfo routedDevice, + List spatializerChannelMasks) { + return getCapabilitiesInternal(context, audioAttributes, routedDevice, spatializerChannelMasks); } @SuppressWarnings("InlinedApi") @SuppressLint("UnprotectedReceiver") // ACTION_HDMI_AUDIO_PLUG is protected since API 16 /* package */ static AudioCapabilities getCapabilitiesInternal( - Context context, AudioAttributes audioAttributes, @Nullable AudioDeviceInfo routedDevice) { + Context context, + AudioAttributes audioAttributes, + @Nullable AudioDeviceInfo routedDevice, + List spatializerChannelMasks) { Intent intent = context.registerReceiver( /* receiver= */ null, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)); - return getCapabilitiesInternal(context, intent, audioAttributes, routedDevice); + + return getCapabilitiesInternal( + context, intent, audioAttributes, routedDevice, spatializerChannelMasks); } @SuppressLint("InlinedApi") @@ -144,7 +169,8 @@ public static AudioCapabilities getCapabilities( Context context, @Nullable Intent intent, AudioAttributes audioAttributes, - @Nullable AudioDeviceInfo routedDevice) { + @Nullable AudioDeviceInfo routedDevice, + List spatializerChannelMasks) { AudioManager audioManager = AudioManagerCompat.getAudioManager(context); AudioDeviceInfo currentDevice = routedDevice != null @@ -163,14 +189,16 @@ public static AudioCapabilities getCapabilities( // so the encodings we get from AudioManager.getDirectProfilesForAttributes should include // the PCM encodings and surround encodings for passthrough mode. return Api33.getCapabilitiesInternalForDirectPlayback( - audioManager, audioAttributes, speakerLayoutChannelMasks); + audioManager, audioAttributes, speakerLayoutChannelMasks, spatializerChannelMasks); } // If a connection to Bluetooth device is detected, we only return the minimum capabilities that // is supported by all the devices. if (isBluetoothConnected(audioManager, currentDevice)) { return new AudioCapabilities( - ImmutableList.of(AudioProfile.DEFAULT_AUDIO_PROFILE), speakerLayoutChannelMasks); + ImmutableList.of(AudioProfile.DEFAULT_AUDIO_PROFILE), + speakerLayoutChannelMasks, + spatializerChannelMasks); } ImmutableSet.Builder supportedEncodings = new ImmutableSet.Builder<>(); @@ -184,7 +212,8 @@ public static AudioCapabilities getCapabilities( supportedEncodings.addAll(Api29.getDirectPlaybackSupportedEncodings(audioAttributes)); return new AudioCapabilities( getAudioProfiles(Ints.toArray(supportedEncodings.build()), DEFAULT_MAX_CHANNEL_COUNT), - speakerLayoutChannelMasks); + speakerLayoutChannelMasks, + spatializerChannelMasks); } ContentResolver contentResolver = context.getContentResolver(); @@ -208,14 +237,16 @@ public static AudioCapabilities getCapabilities( intent.getIntExtra( AudioManager.EXTRA_MAX_CHANNEL_COUNT, /* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT)), - speakerLayoutChannelMasks); + speakerLayoutChannelMasks, + spatializerChannelMasks); } return new AudioCapabilities( getAudioProfiles( Ints.toArray(supportedEncodings.build()), /* maxChannelCount= */ DEFAULT_MAX_CHANNEL_COUNT), - speakerLayoutChannelMasks); + speakerLayoutChannelMasks, + spatializerChannelMasks); } /** @@ -232,6 +263,7 @@ public static AudioCapabilities getCapabilities( private final SparseArray encodingToAudioProfile; private final int maxChannelCount; private final ImmutableList speakerLayoutChannelMasks; + private final ImmutableList spatializerChannelMasks; /** * @deprecated Use {@link #getCapabilities(Context, AudioAttributes, AudioDeviceInfo)} instead. @@ -240,20 +272,27 @@ public static AudioCapabilities getCapabilities( public AudioCapabilities(@Nullable int[] supportedEncodings, int maxChannelCount) { this( getAudioProfiles(supportedEncodings, maxChannelCount), - DEFAULT_SPEAKER_LAYOUT_CHANNEL_MASKS); + DEFAULT_SPEAKER_LAYOUT_CHANNEL_MASKS, + DEFAULT_EMPTY_SPATIALIZER_CHANNEL_MASKS); } - /** Simple constructor for assertions in {@link AudioCapabilitiesTest}. */ + /** Simple constructor for assertions in tests. */ @VisibleForTesting /* package */ AudioCapabilities( @Nullable int[] supportedEncodings, int maxChannelCount, - List speakerLayoutChannelMasks) { - this(getAudioProfiles(supportedEncodings, maxChannelCount), speakerLayoutChannelMasks); + List speakerLayoutChannelMasks, + List spatializerChannelMasks) { + this( + getAudioProfiles(supportedEncodings, maxChannelCount), + speakerLayoutChannelMasks, + spatializerChannelMasks); } private AudioCapabilities( - List audioProfiles, List speakerLayoutChannelMasks) { + List audioProfiles, + List speakerLayoutChannelMasks, + List spatializerChannelMasks) { encodingToAudioProfile = new SparseArray<>(); for (int i = 0; i < audioProfiles.size(); i++) { AudioProfile audioProfile = audioProfiles.get(i); @@ -265,6 +304,7 @@ private AudioCapabilities( } this.maxChannelCount = maxChannelCount; this.speakerLayoutChannelMasks = ImmutableList.copyOf(speakerLayoutChannelMasks); + this.spatializerChannelMasks = ImmutableList.copyOf(spatializerChannelMasks); } /** @@ -296,6 +336,16 @@ public ImmutableList getSpeakerLayoutChannelMasks() { return speakerLayoutChannelMasks; } + /** + * Returns the channel masks supported by the {@link android.media.Spatializer}, if + * enabled/available. + * + *

This is only non-empty if the Spatialize is ready for use. + */ + public ImmutableList getSpatializerChannelMasks() { + return spatializerChannelMasks; + } + /** * @deprecated Use {@link #isPassthroughPlaybackSupported(Format, AudioAttributes)} instead. */ @@ -393,7 +443,8 @@ public boolean equals(@Nullable Object other) { AudioCapabilities audioCapabilities = (AudioCapabilities) other; return Util.contentEquals(encodingToAudioProfile, audioCapabilities.encodingToAudioProfile) && maxChannelCount == audioCapabilities.maxChannelCount - && Objects.equals(speakerLayoutChannelMasks, audioCapabilities.speakerLayoutChannelMasks); + && Objects.equals(speakerLayoutChannelMasks, audioCapabilities.speakerLayoutChannelMasks) + && Objects.equals(spatializerChannelMasks, audioCapabilities.spatializerChannelMasks); } @Override @@ -401,6 +452,7 @@ public int hashCode() { int result = maxChannelCount; result = 31 * result + Util.contentHashCode(encodingToAudioProfile); result = 31 * result + Objects.hashCode(speakerLayoutChannelMasks); + result = 31 * result + Objects.hashCode(spatializerChannelMasks); return result; } @@ -412,6 +464,8 @@ public String toString() { + encodingToAudioProfile + ", speakerLayoutChannelMasks=" + speakerLayoutChannelMasks + + ", spatializerChannelMasks=" + + spatializerChannelMasks + "]"; } @@ -665,11 +719,14 @@ private Api33() {} public static AudioCapabilities getCapabilitiesInternalForDirectPlayback( AudioManager audioManager, AudioAttributes audioAttributes, - List speakerLayoutChannelMasks) { + List speakerLayoutChannelMasks, + List spatializerChannelMasks) { List directAudioProfiles = audioManager.getDirectProfilesForAttributes(audioAttributes.getPlatformAudioAttributes()); return new AudioCapabilities( - getAudioProfiles(directAudioProfiles), speakerLayoutChannelMasks); + getAudioProfiles(directAudioProfiles), + speakerLayoutChannelMasks, + spatializerChannelMasks); } @Nullable diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java index 43f8b396470..ea72450d504 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java @@ -15,6 +15,7 @@ */ package androidx.media3.exoplayer.audio; +import static android.os.Build.VERSION.SDK_INT; import static com.google.common.base.Preconditions.checkNotNull; import android.content.BroadcastReceiver; @@ -32,6 +33,9 @@ import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.util.SpatializerWrapper; +import com.google.common.collect.ImmutableList; +import java.util.List; import java.util.Objects; /** @@ -58,6 +62,7 @@ public interface Listener { private final AudioDeviceCallback audioDeviceCallback; private final BroadcastReceiver hdmiAudioPlugBroadcastReceiver; @Nullable private final ExternalSurroundSoundSettingObserver externalSurroundSoundSettingObserver; + @Nullable private SpatializerWrapper spatializer; @Nullable private AudioCapabilities audioCapabilities; @Nullable private AudioDeviceInfo routedDevice; @@ -123,7 +128,8 @@ public void setAudioAttributes(AudioAttributes audioAttributes) { this.audioAttributes = audioAttributes; onNewAudioCapabilities( - AudioCapabilities.getCapabilitiesInternal(context, audioAttributes, routedDevice)); + AudioCapabilities.getCapabilitiesInternal( + context, audioAttributes, routedDevice, getSpatializerChannelMasks())); } /** @@ -138,7 +144,8 @@ public void setRoutedDevice(@Nullable AudioDeviceInfo routedDevice) { } this.routedDevice = routedDevice; onNewAudioCapabilities( - AudioCapabilities.getCapabilitiesInternal(context, audioAttributes, this.routedDevice)); + AudioCapabilities.getCapabilitiesInternal( + context, audioAttributes, this.routedDevice, getSpatializerChannelMasks())); } /** @@ -159,15 +166,21 @@ public AudioCapabilities register() { } AudioManagerCompat.getAudioManager(context) .registerAudioDeviceCallback(audioDeviceCallback, handler); + if (SDK_INT >= 32 && spatializer == null) { + boolean deviceIsTv = Util.isTv(context); + spatializer = + new SpatializerWrapper(context, this::updateCurrentAudioCapabilities, deviceIsTv); + } Intent stickyIntent = context.registerReceiver( hdmiAudioPlugBroadcastReceiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG), /* broadcastPermission= */ null, handler); + List spatializerChannelMasks = getSpatializerChannelMasks(); audioCapabilities = AudioCapabilities.getCapabilitiesInternal( - context, stickyIntent, audioAttributes, routedDevice); + context, stickyIntent, audioAttributes, routedDevice, spatializerChannelMasks); return audioCapabilities; } @@ -181,6 +194,10 @@ public void unregister() { } audioCapabilities = null; AudioManagerCompat.getAudioManager(context).unregisterAudioDeviceCallback(audioDeviceCallback); + if (SDK_INT >= 32 && spatializer != null) { + spatializer.release(); + spatializer = null; + } context.unregisterReceiver(hdmiAudioPlugBroadcastReceiver); if (externalSurroundSoundSettingObserver != null) { externalSurroundSoundSettingObserver.unregister(); @@ -188,6 +205,12 @@ public void unregister() { registered = false; } + private List getSpatializerChannelMasks() { + return SDK_INT >= 32 && spatializer != null + ? spatializer.getSpatializedChannelMasks() + : ImmutableList.of(); + } + private void onNewAudioCapabilities(AudioCapabilities newAudioCapabilities) { if (registered && !newAudioCapabilities.equals(audioCapabilities)) { audioCapabilities = newAudioCapabilities; @@ -195,14 +218,22 @@ private void onNewAudioCapabilities(AudioCapabilities newAudioCapabilities) { } } + private void updateCurrentAudioCapabilities() { + List spatializerChannelMasks = getSpatializerChannelMasks(); + onNewAudioCapabilities( + AudioCapabilities.getCapabilitiesInternal( + context, audioAttributes, routedDevice, spatializerChannelMasks)); + } + private final class HdmiAudioPlugBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (!isInitialStickyBroadcast()) { + List spatializerChannelMasks = getSpatializerChannelMasks(); onNewAudioCapabilities( AudioCapabilities.getCapabilitiesInternal( - context, intent, audioAttributes, routedDevice)); + context, intent, audioAttributes, routedDevice, spatializerChannelMasks)); } } } @@ -229,16 +260,14 @@ public void unregister() { @Override public void onChange(boolean selfChange) { - onNewAudioCapabilities( - AudioCapabilities.getCapabilitiesInternal(context, audioAttributes, routedDevice)); + updateCurrentAudioCapabilities(); } } private final class AudioDeviceCallback extends android.media.AudioDeviceCallback { @Override public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { - onNewAudioCapabilities( - AudioCapabilities.getCapabilitiesInternal(context, audioAttributes, routedDevice)); + updateCurrentAudioCapabilities(); } @Override @@ -246,8 +275,7 @@ public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { if (Util.contains(removedDevices, routedDevice)) { routedDevice = null; } - onNewAudioCapabilities( - AudioCapabilities.getCapabilitiesInternal(context, audioAttributes, routedDevice)); + updateCurrentAudioCapabilities(); } } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java index 209833c16d7..e1b5f42ba99 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java @@ -231,7 +231,6 @@ public DecoderAudioRenderer( super(C.TRACK_TYPE_AUDIO); eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.audioSink = audioSink; - audioSink.setListener(new AudioSinkListener()); flagsOnlyBuffer = DecoderInputBuffer.newNoDataInstance(); decoderReinitializationState = REINITIALIZATION_STATE_NONE; audioTrackNeedsConfigure = true; @@ -671,6 +670,7 @@ protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) } audioSink.setPlayerId(getPlayerId()); audioSink.setClock(getClock()); + audioSink.setListener(new AudioSinkListener()); } @Override 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 666e2023739..6ba89a6a26e 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 @@ -753,8 +753,8 @@ public void configure(Format inputFormat, int specifiedBufferSize, @Nullable int pipelineProcessors.add(toFloatPcmAudioProcessor); } else { pipelineProcessors.add(toInt16PcmAudioProcessor); - pipelineProcessors.add(audioProcessorChain.getAudioProcessors()); } + pipelineProcessors.add(audioProcessorChain.getAudioProcessors()); audioProcessingPipeline = new AudioProcessingPipeline(pipelineProcessors.build()); // If the underlying processors of the new pipeline are the same as the existing pipeline, @@ -1935,7 +1935,13 @@ public void onOffloadPresentationEnded() { // Stale event. return; } - handledOffloadOnPresentationEnded = true; + // Without checking `stoppedAudioOutput`, during playlist playback with offloaded audio, + // `hasPendingData` could return false prematurely. We only update + // `handledOffloadOnPresentationEnded` to true if the audio output has actually finished + // playing. + if (stoppedAudioOutput) { + handledOffloadOnPresentationEnded = true; + } } @Override diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/IamfUtil.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/IamfUtil.java index 6584ece4813..90d8fd2aa38 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/IamfUtil.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/IamfUtil.java @@ -19,15 +19,11 @@ import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static java.lang.annotation.RetentionPolicy.SOURCE; -import android.content.Context; import android.media.AudioFormat; -import android.media.AudioManager; -import android.media.Spatializer; import androidx.annotation.IntDef; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; -import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.util.Log; import com.google.common.collect.ImmutableSet; import java.lang.annotation.Documented; @@ -211,7 +207,11 @@ private IamfUtil() {} @VisibleForTesting public static final ImmutableSet IAMF_SUPPORTED_CHANNEL_MASKS = (SDK_INT < 32) - ? ImmutableSet.of() + ? ImmutableSet.of( + AudioFormat.CHANNEL_OUT_STEREO, + AudioFormat.CHANNEL_OUT_5POINT1, + AudioFormat.CHANNEL_OUT_7POINT1_SURROUND, + AudioFormat.CHANNEL_OUT_MONO) : ImmutableSet.of( // The same order as appears in getOutputLayoutForChannelMask. AudioFormat.CHANNEL_OUT_STEREO, @@ -370,82 +370,76 @@ public static int getChannelMaskForOutputLayout(@OutputLayout int outputLayout) } /** - * Returns an appropriate output layout for the current audio output configuration. + * Returns an appropriate output layout for the current {@link AudioCapabilities}. * - * @param context The {@link Context} to use for querying the current audio output configuration. - * @param useIntegratedBinauralRenderer When it seems the user is using headphones (based on the - * {@link Context}), if {@code useIntegratedBinauralRenderer} is {@code true}, this method - * will return {@link OUTPUT_LAYOUT_BINAURAL} to get binaural audio directly from the decoder, - * otherwise, it will return an output layout appropriate for the {@link - * android.media.Spatializer}. + *

This is used to configure the IAMF extension decoder, which requires an {@link OutputLayout} + * to configure. + * + * @param audioCapabilities The current {@link AudioCapabilities}. + * @param useIntegratedBinauralRenderer When it seems the user is using headphones (because {@link + * AudioCapabilities#getSpatializerChannelMasks()} is non-empty), if {@code + * useIntegratedBinauralRenderer} is {@code true}, this method will return {@link + * OUTPUT_LAYOUT_BINAURAL} to get binaural audio directly from the decoder, otherwise, it will + * return an output layout appropriate for the {@link android.media.Spatializer} based on the + * available Spatializer channel masks. */ public static @OutputLayout int getOutputLayoutForCurrentConfiguration( - Context context, boolean useIntegratedBinauralRenderer) { - int spatializerChannelMask = getSpatializerChannelMaskIfSpatializationSupported(context); - if (spatializerChannelMask != AudioFormat.CHANNEL_INVALID) { + AudioCapabilities audioCapabilities, boolean useIntegratedBinauralRenderer) { + + if (!audioCapabilities.getSpatializerChannelMasks().isEmpty()) { // If Spatializer enabled and available, we will use that as a signal that the user is likely // using headphones and wants spatial audio, but we will use decoder built-in binaural. if (useIntegratedBinauralRenderer) { // Directly request binaural audio from the decoder. return OUTPUT_LAYOUT_BINAURAL; } - return getOutputLayoutForChannelMask(spatializerChannelMask); + int spatializerChannelMask = + getFirstCompatibleChannelMask(audioCapabilities.getSpatializerChannelMasks()); + if (spatializerChannelMask != AudioFormat.CHANNEL_INVALID) { + return getOutputLayoutForChannelMask(spatializerChannelMask); + } + Log.w( + TAG, + "No spatializer channel mask is compatible with IAMF, falling back to speaker layout."); } + int speakerLayoutChannelMask = + getFirstCompatibleChannelMask(audioCapabilities.getSpeakerLayoutChannelMasks()); + if (speakerLayoutChannelMask != AudioFormat.CHANNEL_INVALID) { + return getOutputLayoutForChannelMask(speakerLayoutChannelMask); + } return OUTPUT_LAYOUT_ITU2051_SOUND_SYSTEM_A_0_2_0; // Default to stereo. - // TODO(b/392950453): Define other branches for other device types like HDMI, built-in, etc. } /** - * Returns an appropriate channel mask for the current audio output configuration. + * Returns an appropriate channel mask for the current {@link AudioCapabilities}. * *

This is used to set the channel count and channel mask when using the framework Codec2 * software decoder, which takes a ChannelMask to indicate the output layout and does not * currently support direct binaural output from the decoder. */ - public static int getOutputChannelMaskForCurrentConfiguration(Context context) { - int spatializerChannelMask = getSpatializerChannelMaskIfSpatializationSupported(context); + public static int getOutputChannelMaskForCurrentConfiguration( + AudioCapabilities audioCapabilities) { + int spatializerChannelMask = + getFirstCompatibleChannelMask(audioCapabilities.getSpatializerChannelMasks()); if (spatializerChannelMask != AudioFormat.CHANNEL_INVALID) { return spatializerChannelMask; } + int speakerLayoutChannelMask = + getFirstCompatibleChannelMask(audioCapabilities.getSpeakerLayoutChannelMasks()); + if (speakerLayoutChannelMask != AudioFormat.CHANNEL_INVALID) { + return speakerLayoutChannelMask; + } return AudioFormat.CHANNEL_OUT_STEREO; // Default to stereo. - // TODO(b/392950453): Define shared logic for other branches for other device types, as above. } - /** Returns the first ChannelMask that the Spatializer (if usable) inherently supports. */ - private static int getSpatializerChannelMaskIfSpatializationSupported(Context context) { - // Spatializer is only available on API 32 and above. - if (SDK_INT < 32) { - Log.w(TAG, "Spatializer is not available on API < 32."); - return AudioFormat.CHANNEL_INVALID; - } - AudioManager audioManager = AudioManagerCompat.getAudioManager(context); - if (audioManager == null) { - Log.w(TAG, "Audio Manager is null."); - return AudioFormat.CHANNEL_INVALID; - } - Spatializer spatializer = audioManager.getSpatializer(); - boolean canSpatialize = - spatializer.getImmersiveAudioLevel() != Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE - && spatializer.isAvailable() - && spatializer.isEnabled(); - if (!canSpatialize) { - return AudioFormat.CHANNEL_INVALID; - } - if (SDK_INT >= 36) { - // Starting in 36, we can query the Spatializer for its inherently supported layouts, that is - // to say, the highest channel-count layouts which the Spatializer is able to use as input - // without downsampling. - List supportedChannelMasks = spatializer.getSpatializedChannelMasks(); - for (int channelMask : supportedChannelMasks) { - if (IAMF_SUPPORTED_CHANNEL_MASKS.contains(channelMask)) { - return channelMask; - } + private static int getFirstCompatibleChannelMask(List channelMasks) { + for (int channelMask : channelMasks) { + if (IAMF_SUPPORTED_CHANNEL_MASKS.contains(channelMask)) { + return channelMask; } - Log.w(TAG, "No Spatializer channel mask is supported by IAMF Decoder."); } - // If we are not in 36 or greater, we assume 5.1 is the inherently supported layout. - return AudioFormat.CHANNEL_OUT_5POINT1; + return AudioFormat.CHANNEL_INVALID; } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java index 0b8882367fc..6be5195d066 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java @@ -325,7 +325,6 @@ public MediaCodecAudioRenderer( rendererPriority = C.PRIORITY_PLAYBACK; eventDispatcher = new EventDispatcher(eventHandler, eventListener); nextBufferToWritePresentationTimeUs = C.TIME_UNSET; - audioSink.setListener(new AudioSinkListener()); } @Override @@ -511,7 +510,10 @@ protected MediaCodecAdapter.Configuration getMediaCodecConfiguration( @Override protected DecoderReuseEvaluation canReuseCodec( - MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + MediaCodecInfo codecInfo, + Format oldFormat, + Format newFormat, + boolean isAdaptiveFormatChange) { DecoderReuseEvaluation evaluation = codecInfo.canReuseCodec(oldFormat, newFormat); @DecoderDiscardReasons int discardReasons = evaluation.discardReasons; @@ -705,6 +707,7 @@ protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) } audioSink.setPlayerId(getPlayerId()); audioSink.setClock(getClock()); + audioSink.setListener(new AudioSinkListener()); } @Override @@ -1095,10 +1098,19 @@ protected MediaFormat getMediaFormat( // IAMF can support many different output Layouts, which are communicated to the Media Codec // (IAMF Codec2 software decoder) as a channel mask. We want to choose one appropriate // for the current audio output configuration. - int channelMask = IamfUtil.getOutputChannelMaskForCurrentConfiguration(context); - int channelCount = Integer.bitCount(channelMask); - mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, channelMask); - mediaFormat.setInteger(MediaFormat.KEY_MAX_OUTPUT_CHANNEL_COUNT, channelCount); + AudioCapabilities audioCapabilities = audioSink.getAudioCapabilities(); + if (audioCapabilities == null) { + Log.w( + TAG, + "AudioCapabilities from the AudioSink are null, using default stereo output layout."); + mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_OUT_STEREO); + mediaFormat.setInteger(MediaFormat.KEY_MAX_OUTPUT_CHANNEL_COUNT, 2); + } else { + int channelMask = IamfUtil.getOutputChannelMaskForCurrentConfiguration(audioCapabilities); + int channelCount = Integer.bitCount(channelMask); + mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, channelMask); + mediaFormat.setInteger(MediaFormat.KEY_MAX_OUTPUT_CHANNEL_COUNT, channelCount); + } } applyCodecParametersToMediaFormat(mediaFormat); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/image/ImageMetadataListener.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/image/ImageMetadataListener.java new file mode 100644 index 00000000000..be7401a40d8 --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/image/ImageMetadataListener.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 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.image; + +import androidx.media3.common.Format; +import androidx.media3.common.util.UnstableApi; + +/** A listener for image metadata. */ +@UnstableApi +public interface ImageMetadataListener { + /** + * Called on the playback thread when an image is about to be available. + * + *

This method is called prior to invoking {@link ImageOutput#onImageAvailable(long, + * android.graphics.Bitmap)}. + * + * @param presentationTimeUs The presentation time of the image, in microseconds. + * @param format The format associated with the image. + */ + void onImageAboutToBeAvailable(long presentationTimeUs, Format format); +} diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/image/ImageRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/image/ImageRenderer.java index 9399a15af2f..5b44897ecdd 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/image/ImageRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/image/ImageRenderer.java @@ -110,6 +110,7 @@ public class ImageRenderer extends BaseRenderer { @Nullable private ImageDecoder decoder; @Nullable private DecoderInputBuffer inputBuffer; private ImageOutput imageOutput; + @Nullable private ImageMetadataListener imageMetadataListener; @Nullable private Bitmap outputBitmap; private boolean readyToOutputTiles; @Nullable private TileInfo tileInfo; @@ -282,6 +283,9 @@ public void handleMessage(@MessageType int messageType, @Nullable Object message ImageOutput imageOutput = message instanceof ImageOutput ? (ImageOutput) message : null; setImageOutput(imageOutput); break; + case MSG_SET_IMAGE_METADATA_LISTENER: + imageMetadataListener = (ImageMetadataListener) message; + break; default: super.handleMessage(messageType, message); } @@ -404,6 +408,10 @@ protected boolean processOutputBuffer( // image. long earlyUs = bufferPresentationTimeUs - positionUs; if (shouldForceRender() || earlyUs < IMAGE_PRESENTATION_WINDOW_THRESHOLD_US) { + if (imageMetadataListener != null) { + imageMetadataListener.onImageAboutToBeAvailable( + bufferPresentationTimeUs - outputStreamInfo.streamOffsetUs, checkNotNull(inputFormat)); + } imageOutput.onImageAvailable( bufferPresentationTimeUs - outputStreamInfo.streamOffsetUs, outputBitmap); return true; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java index a5233e8f2c4..8aaf3a33a4a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java @@ -58,6 +58,7 @@ public static final class Factory implements MediaCodecAdapter.Factory { private final Supplier queueingThreadSupplier; private boolean enableSynchronousBufferQueueingWithAsyncCryptoFlag; + private boolean enableAsyncCryptoSynchronization; /** * Creates an factory for {@link AsynchronousMediaCodecAdapter} instances. @@ -101,6 +102,22 @@ public void experimentalSetAsyncCryptoFlagEnabled(boolean enableAsyncCryptoFlag) enableSynchronousBufferQueueingWithAsyncCryptoFlag = enableAsyncCryptoFlag; } + /** + * Sets whether to force synchronization for queuing input buffers on API 31 and above for + * {@link AsynchronousMediaCodecAdapter} instances. + * + *

A known bug in the Android framework (b/149908061) prior to API 31 can cause garbled video + * when audio and video are sharing the same DRM session. A workaround was implemented that + * forces synchronization for queuing input buffers. This workaround is disabled for devices + * with API level >= 31 but can be enabled using this method. + * + *

The default is {@code false}. + */ + @ExperimentalApi // TODO: b/502930657 - Remove this method. + public void setAsyncCryptoSynchronizationEnabled(boolean enableAsyncCryptoSynchronization) { + this.enableAsyncCryptoSynchronization = enableAsyncCryptoSynchronization; + } + @Override public AsynchronousMediaCodecAdapter createAdapter(Configuration configuration) throws IOException { @@ -118,7 +135,8 @@ && useSynchronousBufferQueueingWithAsyncCryptoFlag()) { flags |= MediaCodec.CONFIGURE_FLAG_USE_CRYPTO_ASYNC; } else { bufferEnqueuer = - new AsynchronousMediaCodecBufferEnqueuer(codec, queueingThreadSupplier.get()); + new AsynchronousMediaCodecBufferEnqueuer( + codec, queueingThreadSupplier.get(), enableAsyncCryptoSynchronization); } codecAdapter = new AsynchronousMediaCodecAdapter( diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java index c88a172a6ea..618cb214f20 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java @@ -57,6 +57,7 @@ private @MonotonicNonNull Handler handler; private final AtomicReference<@NullableType RuntimeException> pendingRuntimeException; private final ConditionVariable conditionVariable; + private final boolean asyncCryptoSynchronizationEnabled; private boolean started; /** @@ -65,16 +66,25 @@ * @param codec The {@link MediaCodec} to submit input buffers to. * @param queueingThread The {@link HandlerThread} to use for queueing buffers. */ - public AsynchronousMediaCodecBufferEnqueuer(MediaCodec codec, HandlerThread queueingThread) { - this(codec, queueingThread, /* conditionVariable= */ new ConditionVariable()); + public AsynchronousMediaCodecBufferEnqueuer( + MediaCodec codec, HandlerThread queueingThread, boolean enableAsyncCryptoSynchronization) { + this( + codec, + queueingThread, + /* conditionVariable= */ new ConditionVariable(), + enableAsyncCryptoSynchronization); } @VisibleForTesting /* package */ AsynchronousMediaCodecBufferEnqueuer( - MediaCodec codec, HandlerThread handlerThread, ConditionVariable conditionVariable) { + MediaCodec codec, + HandlerThread handlerThread, + ConditionVariable conditionVariable, + boolean enableAsyncCryptoSynchronization) { this.codec = codec; this.handlerThread = handlerThread; this.conditionVariable = conditionVariable; + this.asyncCryptoSynchronizationEnabled = enableAsyncCryptoSynchronization; pendingRuntimeException = new AtomicReference<>(); } @@ -224,11 +234,15 @@ private void doQueueInputBuffer( private void doQueueSecureInputBuffer( int index, int offset, MediaCodec.CryptoInfo info, long presentationTimeUs, int flags) { try { - // Synchronize calls to MediaCodec.queueSecureInputBuffer() to avoid race conditions inside - // the crypto module when audio and video are sharing the same DRM session + // Below 31, synchronize calls to MediaCodec.queueSecureInputBuffer() to avoid race conditions + // inside the crypto module when audio and video are sharing the same DRM session // (see [Internal: b/149908061]). - synchronized (QUEUE_SECURE_LOCK) { + if (SDK_INT >= 31 && !asyncCryptoSynchronizationEnabled) { codec.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); + } else { + synchronized (QUEUE_SECURE_LOCK) { + codec.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); + } } } catch (RuntimeException e) { pendingRuntimeException.compareAndSet(null, e); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java index c83f614c45c..759621681bb 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java @@ -66,6 +66,7 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. private @Mode int asynchronousMode; private boolean asyncCryptoFlagEnabled; + private boolean asyncCryptoSynchronizationEnabled; /** * @deprecated Use {@link #DefaultMediaCodecAdapterFactory(Context)} instead. @@ -73,7 +74,6 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. @Deprecated public DefaultMediaCodecAdapterFactory() { asynchronousMode = MODE_DEFAULT; - asyncCryptoFlagEnabled = true; context = null; callbackThreadSupplier = null; queueingThreadSupplier = null; @@ -146,6 +146,27 @@ public DefaultMediaCodecAdapterFactory experimentalSetAsyncCryptoFlagEnabled( return this; } + /** + * Sets whether to force synchronization for queuing input buffers on API 31 and above for {@link + * AsynchronousMediaCodecAdapter} instances. + * + *

A known bug in the Android framework (b/149908061) prior to API 31 can cause garbled video + * when audio and video are sharing the same DRM session. A workaround was implemented that forces + * synchronization for queuing input buffers. This workaround is disabled for devices with API + * level >= 31 but can be enabled using this method. + * + *

The default is {@code false}. + * + * @return This factory, for convenience. + */ + @CanIgnoreReturnValue + @ExperimentalApi // TODO: b/502930657 - Remove this method. + public DefaultMediaCodecAdapterFactory setAsyncCryptoSynchronizationEnabled( + boolean enableAsyncCryptoSynchronization) { + asyncCryptoSynchronizationEnabled = enableAsyncCryptoSynchronization; + return this; + } + @Override public MediaCodecAdapter createAdapter(MediaCodecAdapter.Configuration configuration) throws IOException { @@ -162,6 +183,7 @@ public MediaCodecAdapter createAdapter(MediaCodecAdapter.Configuration configura callbackThreadSupplier, queueingThreadSupplier) : new AsynchronousMediaCodecAdapter.Factory(trackType); factory.experimentalSetAsyncCryptoFlagEnabled(asyncCryptoFlagEnabled); + factory.setAsyncCryptoSynchronizationEnabled(asyncCryptoSynchronizationEnabled); return factory.createAdapter(configuration); } return new SynchronousMediaCodecAdapter.Factory().createAdapter(configuration); 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 1e9685e04d5..cec7fdbc1e6 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 @@ -51,6 +51,7 @@ import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.PlaybackException; +import androidx.media3.common.util.CodecSpecificDataUtil; import androidx.media3.common.util.ExperimentalApi; import androidx.media3.common.util.Log; import androidx.media3.common.util.TimedValueQueue; @@ -812,6 +813,7 @@ protected void onPositionReset( waitingForFirstSampleInFormat = true; } outputStreamInfo.formatQueue.clear(); + outputStreamInfo.queuedBufferAfterReset = false; } @Override @@ -1624,7 +1626,9 @@ private boolean feedInputBuffer() throws ExoPlaybackException { } if (waitingForFirstSampleInFormat) { - getLastOutputStreamInfo().formatQueue.add(presentationTimeUs, checkNotNull(inputFormat)); + OutputStreamInfo lastStreamInfo = getLastOutputStreamInfo(); + lastStreamInfo.formatQueue.add(presentationTimeUs, checkNotNull(inputFormat)); + lastStreamInfo.queuedBufferAfterReset = true; waitingForFirstSampleInFormat = false; } largestQueuedPresentationTimeUs = max(largestQueuedPresentationTimeUs, presentationTimeUs); @@ -1773,7 +1777,8 @@ protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) || Objects.equals(newFormat.sampleMimeType, MimeTypes.VIDEO_VP9) || (Objects.equals(newFormat.sampleMimeType, MimeTypes.VIDEO_DOLBY_VISION) && Objects.equals( - MediaCodecUtil.getAlternativeCodecMimeType(newFormat), MimeTypes.VIDEO_AV1))) + CodecSpecificDataUtil.getDolbyVisionBaseLayerMimeType(newFormat), + MimeTypes.VIDEO_AV1))) && !newFormat.initializationData.isEmpty()) { newFormat = newFormat.buildUpon().setInitializationData(null).build(); } @@ -1813,7 +1818,12 @@ protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) } boolean drainAndUpdateCodecDrmSession = sourceDrmSession != codecDrmSession; - DecoderReuseEvaluation evaluation = canReuseCodec(codecInfo, oldFormat, newFormat); + DecoderReuseEvaluation evaluation = + canReuseCodec( + codecInfo, + oldFormat, + newFormat, + /* isAdaptiveFormatChange= */ getLastOutputStreamInfo().queuedBufferAfterReset); @DecoderDiscardReasons int overridingDiscardReasons = 0; switch (evaluation.result) { case REUSE_RESULT_NO: @@ -2001,17 +2011,23 @@ protected void onProcessedStreamChange() { /** * Evaluates whether the existing {@link MediaCodec} can be kept for a new {@link Format}, and if - * it can whether it requires reconfiguration. + * it can, whether it requires reconfiguration. * *

The default implementation does not allow decoder reuse. * * @param codecInfo A {@link MediaCodecInfo} describing the decoder. * @param oldFormat The {@link Format} for which the existing instance is configured. * @param newFormat The new {@link Format}. + * @param isAdaptiveFormatChange Whether the format change happens within the same stream after + * having queued samples for {@code oldFormat}, typically indicating an adaptive format + * change. * @return The result of the evaluation. */ protected DecoderReuseEvaluation canReuseCodec( - MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + MediaCodecInfo codecInfo, + Format oldFormat, + Format newFormat, + boolean isAdaptiveFormatChange) { return new DecoderReuseEvaluation( codecInfo.name, oldFormat, @@ -2877,20 +2893,21 @@ private static boolean codecNeedsEosFlushWorkaround(String name) { private static final class OutputStreamInfo { - public static final OutputStreamInfo UNSET = + private static final OutputStreamInfo UNSET = new OutputStreamInfo( /* previousStreamLastBufferTimeUs= */ C.TIME_UNSET, /* startPositionUs= */ C.TIME_UNSET, /* streamOffsetUs= */ C.TIME_UNSET); - public final long previousStreamLastBufferTimeUs; - public final long startPositionUs; - public final long streamOffsetUs; - public final TimedValueQueue formatQueue; + private final long previousStreamLastBufferTimeUs; + private final long startPositionUs; + private final long streamOffsetUs; + private final TimedValueQueue formatQueue; - public long lastBufferTimeUs; + private boolean queuedBufferAfterReset; + private long lastBufferTimeUs; - public OutputStreamInfo( + private OutputStreamInfo( long previousStreamLastBufferTimeUs, long startPositionUs, long streamOffsetUs) { this.previousStreamLastBufferTimeUs = previousStreamLastBufferTimeUs; this.startPositionUs = startPositionUs; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BaseMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BaseMediaSource.java index 673795556e8..5625f04e8f1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BaseMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/BaseMediaSource.java @@ -234,14 +234,6 @@ public final void removeDrmEventListener(DrmSessionEventListener eventListener) drmEventDispatcher.removeEventListener(eventListener); } - @UnstableApi - @SuppressWarnings("deprecation") // Overriding deprecated method to make it final. - @Override - public final void prepareSource( - MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { - prepareSource(caller, mediaTransferListener, PlayerId.UNSET); - } - @UnstableApi @Override public final void prepareSource( diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaSource.java index ec771ed554d..1240772fea7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaSource.java @@ -492,17 +492,6 @@ default boolean canUpdateMediaItem(MediaItem mediaItem) { @UnstableApi default void updateMediaItem(MediaItem mediaItem) {} - /** - * @deprecated Implement {@link #prepareSource(MediaSourceCaller, TransferListener, PlayerId)} - * instead. - */ - @UnstableApi - @Deprecated - default void prepareSource( - MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { - prepareSource(caller, mediaTransferListener, PlayerId.UNSET); - } - /** * Registers a {@link MediaSourceCaller}. Starts source preparation if needed and enables the * source for the creation of {@link MediaPeriod MediaPerods}. 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 263a514428b..3d7c8b1c0f9 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 @@ -315,7 +315,7 @@ public ProgressiveMediaSource createMediaSource(MediaItem mediaItem) { } /** - * The default number of bytes that should be loaded between each each invocation of {@link + * The default number of bytes that should be loaded between each invocation of {@link * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. */ public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java index 2ef406f1091..945b7c34e24 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/AdsMediaSource.java @@ -302,11 +302,12 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star if (adMediaSourceHolder == null) { long endPositionUs = C.TIME_END_OF_SOURCE; if (useAdMediaSourceClipping) { - long adDurationUs = - checkNotNull(adPlaybackState.getAdGroup(id.adGroupIndex)) - .durationsUs[id.adIndexInAdGroup]; - if (adDurationUs != C.TIME_UNSET) { - endPositionUs = adDurationUs; + AdGroup adGroup = checkNotNull(adPlaybackState.getAdGroup(id.adGroupIndex)); + if (adGroup.durationsUs.length > adIndexInAdGroup) { + long adDurationUs = adGroup.durationsUs[adIndexInAdGroup]; + if (adDurationUs != C.TIME_UNSET) { + endPositionUs = adDurationUs; + } } } adMediaSourceHolder = new AdMediaSourceHolder(id, endPositionUs); @@ -523,7 +524,7 @@ private long[][] getAdDurationsUs() { for (int i = 0; i < adMediaSourceHolders.length; i++) { int adCount = useAdMediaSourceClipping - ? adPlaybackState.getAdGroup(i).count + ? Math.max(adPlaybackState.getAdGroup(i).count, 0) : adMediaSourceHolders[i].length; adDurationsUs[i] = new long[adCount]; for (int j = 0; j < adCount; j++) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java index dd34fc81e5d..95c7ed68aa8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java @@ -280,6 +280,7 @@ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParame public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; needToEvaluateInitialDiscontinuity = false; + hasInitialDiscontinuity = false; if (isPendingReset()) { // A reset is already pending. We only need to update its position. pendingResetPositionUs = positionUs; @@ -314,6 +315,13 @@ public void seekToUs(long positionUs) { seekInsideBuffer = primarySampleQueue.seekTo(positionUs, allowTimeBeyondBuffer); } + if (seekInsideBuffer && canceledMediaChunk != null) { + // Do not allow seeking into a chunk that is being canceled. + if (canceledMediaChunk.getFirstSampleIndex(0) <= primarySampleQueue.getReadIndex()) { + seekInsideBuffer = false; + } + } + if (seekInsideBuffer) { // We can seek inside the buffer. nextNotifyPrimaryFormatMediaChunkIndex = diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java index 54dc0b27747..fc21327f6a2 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/preload/PreloadMediaSource.java @@ -519,6 +519,7 @@ private class PreloadMediaPeriodCallback implements MediaPeriod.Callback { private boolean prepared; private int continueLoadingRetryCountBeforeCallingPreloadControlAgain; @Nullable private Runnable maybeContinueLoadingRunnable; + private boolean onLoadToTheEndOfSourceNotified; public PreloadMediaPeriodCallback(long periodStartPositionUs) { this.periodStartPositionUs = periodStartPositionUs; @@ -530,7 +531,7 @@ public void onPrepared(MediaPeriod mediaPeriod) { prepared = true; preloadHandler.post( () -> { - if (isUsedByPlayer()) { + if (preloadingMediaPeriodAndKey == null || isUsedByPlayer()) { return; } PreloadMediaPeriod preloadMediaPeriod = (PreloadMediaPeriod) mediaPeriod; @@ -572,15 +573,14 @@ public void onPrepared(MediaPeriod mediaPeriod) { public void onContinueLoadingRequested(MediaPeriod mediaPeriod) { preloadHandler.post( () -> { - if (isUsedByPlayer()) { + if (preloadingMediaPeriodAndKey == null || isUsedByPlayer()) { return; } PreloadMediaPeriod preloadMediaPeriod = (PreloadMediaPeriod) mediaPeriod; if (prepared) { long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { - preloadControl.onLoadedToTheEndOfSource(PreloadMediaSource.this); - stopPreloading(); + onLoadToTheEndOfSource(); return; } if (!preloadControl.onContinueLoadingRequested( @@ -615,6 +615,9 @@ private void maybeContinueLoading(MediaPeriod mediaPeriod, MediaPeriodId mediaPe continueLoadingRetryCountBeforeCallingPreloadControlAgain = C.LENGTH_UNSET; mediaPeriod.continueLoading( new LoadingInfo.Builder().setPlaybackPositionUs(periodStartPositionUs).build()); + if (prepared && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE) { + onLoadToTheEndOfSource(); + } return; } @@ -659,6 +662,14 @@ private LoadControl.Parameters createLoadControlParameters( /* targetLiveOffsetUs= */ C.TIME_UNSET, /* lastRebufferRealtimeMs= */ C.TIME_UNSET); } + + private void onLoadToTheEndOfSource() { + if (!onLoadToTheEndOfSourceNotified) { + preloadControl.onLoadedToTheEndOfSource(PreloadMediaSource.this); + onLoadToTheEndOfSourceNotified = true; + } + stopPreloading(); + } } private static class MediaPeriodKey { 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 ee9d690bef3..b3824c00da1 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 @@ -44,6 +44,7 @@ import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.C.RoleFlags; +import androidx.media3.common.ColorInfo; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.Timeline; @@ -63,6 +64,7 @@ import androidx.media3.exoplayer.RendererCapabilities.Capabilities; import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.audio.AudioSink; +import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.util.SpatializerWrapper; @@ -3833,9 +3835,12 @@ public static ImmutableList createForTrackGroup( private final int selectedAudioLanguageScore; private final boolean allowMixedMimeTypes; private final @SelectionEligibility int selectionEligibility; + private final boolean usesPrimaryOrFallbackDecoder; private final boolean usesPrimaryDecoder; private final boolean usesHardwareAcceleration; private final int codecPreferenceScore; + private final boolean isHdr; + @Nullable private final String resolvedMimeType; public VideoTrackInfo( int rendererIndex, @@ -3902,23 +3907,37 @@ public VideoTrackInfo( normalizeUndeterminedLanguageToNull(selectedAudioLanguage) == null; selectedAudioLanguageScore = getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined); + + String resolvedMimeType = format.sampleMimeType; + @RendererCapabilities.DecoderSupport + int decoderSupport = RendererCapabilities.getDecoderSupport(formatSupport); + if (decoderSupport == RendererCapabilities.DECODER_SUPPORT_FALLBACK_MIMETYPE) { + String fallbackMimeType = MediaCodecUtil.getAlternativeCodecMimeType(format); + if (fallbackMimeType != null) { + resolvedMimeType = fallbackMimeType; + } + } + int bestMimeTypeMatchIndex = Integer.MAX_VALUE; for (int i = 0; i < parameters.preferredVideoMimeTypes.size(); i++) { - if (format.sampleMimeType != null - && format.sampleMimeType.equals(parameters.preferredVideoMimeTypes.get(i))) { + if (resolvedMimeType != null + && resolvedMimeType.equals(parameters.preferredVideoMimeTypes.get(i))) { bestMimeTypeMatchIndex = i; break; } } preferredMimeTypeMatchIndex = bestMimeTypeMatchIndex; preferredLabelMatchIndex = getBestLabelMatchIndex(format, parameters.preferredVideoLabels); - usesPrimaryDecoder = - RendererCapabilities.getDecoderSupport(formatSupport) - == RendererCapabilities.DECODER_SUPPORT_PRIMARY; + usesPrimaryOrFallbackDecoder = + decoderSupport == RendererCapabilities.DECODER_SUPPORT_PRIMARY + || decoderSupport == RendererCapabilities.DECODER_SUPPORT_FALLBACK_MIMETYPE; + usesPrimaryDecoder = decoderSupport == RendererCapabilities.DECODER_SUPPORT_PRIMARY; usesHardwareAcceleration = RendererCapabilities.getHardwareAccelerationSupport(formatSupport) == RendererCapabilities.HARDWARE_ACCELERATION_SUPPORTED; - codecPreferenceScore = getVideoCodecPreferenceScore(format.sampleMimeType); + this.resolvedMimeType = resolvedMimeType; + codecPreferenceScore = getVideoCodecPreferenceScore(resolvedMimeType); + isHdr = usesPrimaryDecoder && ColorInfo.isTransferHdr(format.colorInfo); selectionEligibility = evaluateSelectionEligibility(formatSupport, requiredAdaptiveSupport); } @@ -3930,9 +3949,9 @@ public VideoTrackInfo( @Override public boolean isCompatibleForAdaptationWith(VideoTrackInfo otherTrack) { return (allowMixedMimeTypes - || Objects.equals(format.sampleMimeType, otherTrack.format.sampleMimeType)) + || Objects.equals(this.resolvedMimeType, otherTrack.resolvedMimeType)) && (parameters.allowVideoMixedDecoderSupportAdaptiveness - || (this.usesPrimaryDecoder == otherTrack.usesPrimaryDecoder + || (this.usesPrimaryOrFallbackDecoder == otherTrack.usesPrimaryOrFallbackDecoder && this.usesHardwareAcceleration == otherTrack.usesHardwareAcceleration)); } @@ -3988,12 +4007,9 @@ private static int compareNonQualityPreferences(VideoTrackInfo info1, VideoTrack info2.preferredMimeTypeMatchIndex, Ordering.natural().reverse()) // 5. Compare match with renderer capability preferences. - .compareFalseFirst(info1.usesPrimaryDecoder, info2.usesPrimaryDecoder) + .compareFalseFirst( + info1.usesPrimaryOrFallbackDecoder, info2.usesPrimaryOrFallbackDecoder) .compareFalseFirst(info1.usesHardwareAcceleration, info2.usesHardwareAcceleration); - - if (info1.usesPrimaryDecoder && info1.usesHardwareAcceleration) { - chain = chain.compare(info1.codecPreferenceScore, info2.codecPreferenceScore); - } return chain.result(); } @@ -4015,8 +4031,17 @@ private static int compareQualityPreferences(VideoTrackInfo info1, VideoTrackInf comparisonChain = comparisonChain.compare(info1.bitrate, info2.bitrate, FORMAT_VALUE_ORDERING.reverse()); } - return comparisonChain - .compare(info1.pixelCount, info2.pixelCount, qualityOrdering) + ComparisonChain chain = + comparisonChain + .compareFalseFirst(info1.isHdr, info2.isHdr) + .compare(info1.pixelCount, info2.pixelCount, qualityOrdering); + + if (info1.usesPrimaryOrFallbackDecoder && info1.usesHardwareAcceleration) { + chain = chain.compare(info1.codecPreferenceScore, info2.codecPreferenceScore); + } + + return chain + .compareFalseFirst(info1.usesPrimaryDecoder, info2.usesPrimaryDecoder) .compare(info1.bitrate, info2.bitrate, qualityOrdering) .result(); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultAllocator.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultAllocator.java index 368e877bdc0..8727d5ef111 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultAllocator.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultAllocator.java @@ -119,8 +119,6 @@ public synchronized Allocation allocate() { public synchronized void release(Allocation allocation) { availableAllocations[availableCount++] = allocation; allocatedCount--; - // Wake up threads waiting for the allocated size to drop. - notifyAll(); } @Override @@ -130,8 +128,6 @@ public synchronized void release(@Nullable AllocationNode allocationNode) { allocatedCount--; allocationNode = allocationNode.next(); } - // Wake up threads waiting for the allocated size to drop. - notifyAll(); } @Override diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/SpatializerWrapper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/SpatializerWrapper.java index 3c51257c556..2231f216a11 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/SpatializerWrapper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/SpatializerWrapper.java @@ -62,7 +62,7 @@ public class SpatializerWrapper { */ public SpatializerWrapper( @Nullable Context context, - Runnable spatializerChangedCallback, + @Nullable Runnable spatializerChangedCallback, @Nullable Boolean deviceIsTv) { @Nullable AudioManager audioManager = @@ -77,20 +77,32 @@ public SpatializerWrapper( this.spatializer = audioManager.getSpatializer(); this.spatializationSupported = spatializer.getImmersiveAudioLevel() != Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; - this.listener = - new Spatializer.OnSpatializerStateChangedListener() { - @Override - public void onSpatializerEnabledChanged(Spatializer spatializer, boolean enabled) { - spatializerChangedCallback.run(); - } + if (spatializerChangedCallback == null) { + handler = null; + listener = null; + } else { + this.handler = new Handler(checkNotNull(Looper.myLooper())); + this.listener = + new Spatializer.OnSpatializerStateChangedListener() { + @Override + public void onSpatializerEnabledChanged(Spatializer spatializer, boolean enabled) { + spatializerChangedCallback.run(); + } - @Override - public void onSpatializerAvailableChanged(Spatializer spatializer, boolean available) { - spatializerChangedCallback.run(); - } - }; - this.handler = new Handler(checkNotNull(Looper.myLooper())); - spatializer.addOnSpatializerStateChangedListener(handler::post, listener); + @Override + public void onSpatializerAvailableChanged(Spatializer spatializer, boolean available) { + spatializerChangedCallback.run(); + } + }; + spatializer.addOnSpatializerStateChangedListener(handler::post, listener); + } + } + + /** + * A convenience method for the combined checks to see if spatialization is actually available. + */ + public boolean isSupportedAvailableAndEnabled() { + return spatializer != null && spatializationSupported && isAvailable() && isEnabled(); } /** @@ -107,7 +119,7 @@ public boolean isSpatializationSupported() { *

This is delegated to {@link Spatializer#isAvailable()}. */ public boolean isAvailable() { - return checkNotNull(spatializer).isAvailable(); + return spatializer != null && spatializer.isAvailable(); } /** @@ -116,7 +128,7 @@ public boolean isAvailable() { *

This is delegated to {@link Spatializer#isEnabled()}. */ public boolean isEnabled() { - return checkNotNull(spatializer).isEnabled(); + return spatializer != null && spatializer.isEnabled(); } /** @@ -125,6 +137,9 @@ public boolean isEnabled() { *

Includes special logic for immersive audio formats. */ public boolean canBeSpatialized(AudioAttributes audioAttributes, Format format) { + if (!isSupportedAvailableAndEnabled()) { + return false; + } int linearChannelCount; if (Objects.equals(format.sampleMimeType, MimeTypes.AUDIO_E_AC3_JOC)) { // For E-AC3 JOC, the format is object based. When the channel count is 16, this maps to 12 @@ -167,7 +182,10 @@ public boolean canBeSpatialized(AudioAttributes audioAttributes, Format format) *

On API 36+, this is delegated to the {@link Spatializer#getSpatializedChannelMasks()}. On * API 32-35, the default channel mask is 5.1. */ - List getSpatializedChannelMasks() { + public List getSpatializedChannelMasks() { + if (!isSupportedAvailableAndEnabled()) { + return ImmutableList.of(); + } if (SDK_INT >= 36) { return checkNotNull(spatializer).getSpatializedChannelMasks(); } 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 af8b900bfe3..dabfc2f9299 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 @@ -27,8 +27,10 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static java.lang.Math.abs; import static java.lang.Math.max; import static java.lang.Math.min; +import static java.lang.Math.round; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -194,6 +196,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer */ private static final int MAX_CONSECUTIVE_DROPPED_INPUT_BUFFERS_COUNT_TO_DISCARD_HEADER = 0; + /** + * The maximum difference of the frame rate ratio from a whole number to assume the frame rate + * switch is seamless enough to keep the codec below API 30. This accounts for floating point + * calculation inaccuracies and allows transitions between almost equivalent frame rates like + * 29.97 and 30 that can't be resolved by screen rate refresh changes anyway. + */ + private static final float MAX_FRAME_RATE_RATIO_DIFF_TO_KEEP_CODEC = 0.01f; + private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround; private static boolean deviceNeedsSetOutputSurfaceWorkaround; @@ -1389,7 +1399,10 @@ private void maybeSetKeyAllowFrameDrop(MediaFormat mediaFormat) { @Override protected DecoderReuseEvaluation canReuseCodec( - MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + MediaCodecInfo codecInfo, + Format oldFormat, + Format newFormat, + boolean isAdaptiveFormatChange) { DecoderReuseEvaluation evaluation = codecInfo.canReuseCodec(oldFormat, newFormat); @DecoderDiscardReasons int discardReasons = evaluation.discardReasons; @@ -1400,11 +1413,8 @@ protected DecoderReuseEvaluation canReuseCodec( if (getMaxInputSize(codecInfo, newFormat) > codecMaxValues.inputSize) { discardReasons |= DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED; } - if (changeFrameRateStrategy != C.VIDEO_CHANGE_FRAME_RATE_STRATEGY_OFF - && oldFormat.frameRate != Format.NO_VALUE - && newFormat.frameRate != Format.NO_VALUE - && Math.abs(newFormat.frameRate - oldFormat.frameRate) > 1f - && cannotChangeSurfaceFrameRateMidPlayback()) { + if (shouldDiscardCodecForFrameRateChange( + codecInfo, oldFormat, newFormat, isAdaptiveFormatChange)) { discardReasons |= DISCARD_REASON_VIDEO_FRAME_RATE_CHANGED; } @@ -2861,12 +2871,40 @@ private static int getMaxSampleSize(int pixelCount, int minCompressionRatio) { return (pixelCount * 3) / (2 * minCompressionRatio); } - private static boolean cannotChangeSurfaceFrameRateMidPlayback() { - // We can't tell the surface about the new frame rate before API 30, so it's likely to keep - // the initially estimated screen refresh rate from the previous format. - // See https://github.com/androidx/media/issues/2941. MiTV boxes were also reported to - // still have this problem on API 30. - return SDK_INT < 30 || (SDK_INT == 30 && Build.MODEL.startsWith("MiTV")); + private boolean shouldDiscardCodecForFrameRateChange( + MediaCodecInfo mediaCodecInfo, + Format oldFormat, + Format newFormat, + boolean isAdaptiveFormatChange) { + if (changeFrameRateStrategy == C.VIDEO_CHANGE_FRAME_RATE_STRATEGY_OFF) { + // We are not attempting to change the surface frame rate anyway. + return false; + } + if (SDK_INT >= 31 || (SDK_INT == 30 && !Build.MODEL.startsWith("MiTV"))) { + // We can tell the surface about the new frame rate so it can handle this seamlessly. This + // wasn't possible before API 30 where the initially estimated screen refresh rate from the + // previous format is likely kept. See https://github.com/androidx/media/issues/2941. MiTV + // boxes were also reported to still have this problem on API 30. + return false; + } + if (oldFormat.frameRate == Format.NO_VALUE || newFormat.frameRate == Format.NO_VALUE) { + // Not enough information to tell if there was a frame rate change. + return false; + } + if (mediaCodecInfo.secure && isAdaptiveFormatChange) { + // Many secure codecs introduce a black frame when discarded, which is more distracting for an + // adaptive switch than the potential display refresh mismatch. For non-adaptive switches + // (e.g. playlist transitions), the codec can still be discarded. + // See https://github.com/androidx/media/issues/3120. + return false; + } + float frameRateRatio = + max(newFormat.frameRate, oldFormat.frameRate) + / min(newFormat.frameRate, oldFormat.frameRate); + // Only need to discard the codec if the frame rates are not the same or a close multiple of + // each other (where the change could be handled seamlessly even without a surface refresh + // rate change). + return abs(frameRateRatio - round(frameRateRatio)) > MAX_FRAME_RATE_RATIO_DIFF_TO_KEEP_CODEC; } private static boolean evaluateDeviceNeedsSetOutputSurfaceWorkaround() { diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerAdTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerAdTest.java index 62bde76c3b6..e2cf4f4fb73 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerAdTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerAdTest.java @@ -47,6 +47,7 @@ import androidx.media3.common.Player.Listener; import androidx.media3.common.Player.PositionInfo; import androidx.media3.common.Timeline; +import androidx.media3.common.Timeline.Period; import androidx.media3.common.Timeline.Window; import androidx.media3.common.util.SystemClock; import androidx.media3.common.util.Util; @@ -72,6 +73,7 @@ import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition; import androidx.media3.test.utils.FakeVideoRenderer; import androidx.media3.test.utils.TestExoPlayerBuilder; +import androidx.media3.test.utils.robolectric.RobolectricUtil; import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig; import androidx.test.core.app.ApplicationProvider; import com.google.common.base.Supplier; @@ -80,6 +82,7 @@ import com.google.common.collect.Range; import java.util.List; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; @@ -266,6 +269,77 @@ public void adGroupWithLoadError_withFurtherAdGroup_isSkipped() throws Exception Player.DISCONTINUITY_REASON_AUTO_TRANSITION); } + @Test + public void playAds_preRollsUnavailableWhenStartingToPlay_doesNotCrash() + throws PlaybackException, TimeoutException, InterruptedException { + // Test regression reported with https://github.com/androidx/media/issues/3125 + Timeline primaryContentTimeline = + new FakeTimeline(new TimelineWindowDefinition.Builder().setDurationUs(60_000_000L).build()); + AdPlaybackState adPlaybackState = new AdPlaybackState("adsId", 0L, 10_000L, 20_000L); + AdPlaybackState adPlaybackStateWithAvailableMidroll = + adPlaybackState.withAvailableAdMediaItem( + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + MediaItem.fromUri("http://www.example.com")); + AdPlaybackState adPlaybackStateWithAvailablePreroll = + adPlaybackStateWithAvailableMidroll.withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + MediaItem.fromUri("http://www.example.com")); + FakeAdsLoader fakeAdsLoader = new FakeAdsLoader(); + AdsMediaSource adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(primaryContentTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new DataSpec(Uri.EMPTY), + "adsId", + /* adMediaSourceFactory= */ new FakeMediaSourceFactory( + new TimelineWindowDefinition.Builder().setDurationUs(10_000_000L)), + fakeAdsLoader, + /* adViewProvider= */ () -> null, + /* useLazyContentSourcePreparation= */ true, + /* useAdMediaSourceClipping= */ true); + ExoPlayer player = parameterizeTestExoPlayerBuilder(new TestExoPlayerBuilder(context)).build(); + AtomicBoolean midRollInitialized = new AtomicBoolean(); + player.addListener( + new Player.Listener() { + @Override + public void onTimelineChanged(Timeline timeline, int reason) { + Period period = timeline.getPeriod(/* periodIndex= */ 0, new Period()); + if (period.adPlaybackState.adGroupCount > 1 + && period.adPlaybackState.getAdGroup(/* adGroupIndex= */ 1).states.length > 0) { + midRollInitialized.set(true); + } + } + }); + player.setMediaSource(adsMediaSource); + player.prepare(); + advance(player) + .untilBackgroundThreadCondition( + (Supplier) () -> fakeAdsLoader.eventListeners.get("adsId") != null); + fakeAdsLoader.eventListeners.get("adsId").onAdPlaybackState(adPlaybackState); + player.play(); + advance(player).untilState(Player.STATE_BUFFERING); + + // Set the media item to midroll to trigger a timeline update with the preroll still not + // initialized to emulate the situation with IMA deferring preroll initialization. + fakeAdsLoader + .eventListeners + .get("adsId") + .onAdPlaybackState(adPlaybackStateWithAvailableMidroll); + RobolectricUtil.runMainLooperUntil(() -> midRollInitialized.get()); + // Set the media item to the preroll to transition to READY state. + fakeAdsLoader + .eventListeners + .get("adsId") + .onAdPlaybackState(adPlaybackStateWithAvailablePreroll); + + // If we get to STATE_READY for the pre-roll, we are fine. + advance(player).untilState(Player.STATE_READY); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + player.release(); + } + @Test public void playAds_midRollsWithAndWithoutDuration_clippedOrNotClippedAccordingly() throws PlaybackException, TimeoutException { @@ -435,6 +509,139 @@ public void playAds_withContentResumptionOffset_correctPositionAtDiscontinuity() assertThat(newPositions.get(4).contentPositionMs).isEqualTo(59_999L); } + @Test + public void play_adSkippedByTimelineRefresh_reportsSkipDiscontinuity() + throws PlaybackException, TimeoutException { + Timeline primaryContentTimeline = + new FakeTimeline( + new TimelineWindowDefinition.Builder() + .setWindowPositionInFirstPeriodUs(0L) + .setDurationUs(60_000_000L) + .build()); + AdPlaybackState initialAdPlaybackState = + new AdPlaybackState("adsId") + .withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ 0L) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, 10_000_000L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 2_000_000L) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + MediaItem.fromUri("http://example.com/ad_0_0")); + AdPlaybackState populatedAdPlaybackState = + initialAdPlaybackState.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + FakeAdsLoader fakeAdsLoader = new FakeAdsLoader(); + AdsMediaSource adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(primaryContentTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new DataSpec(Uri.EMPTY), + "adsId", + /* adMediaSourceFactory= */ new FakeMediaSourceFactory( + new TimelineWindowDefinition.Builder()), + fakeAdsLoader, + /* adViewProvider= */ () -> null, + /* useLazyContentSourcePreparation= */ false, + /* useAdMediaSourceClipping= */ true); + ExoPlayer player = parameterizeTestExoPlayerBuilder(new TestExoPlayerBuilder(context)).build(); + Player.Listener mockListener = mock(Player.Listener.class); + player.addListener(mockListener); + player.setMediaSource(adsMediaSource); + player.prepare(); + advance(player) + .untilBackgroundThreadCondition( + (Supplier) () -> fakeAdsLoader.eventListeners.get("adsId") != null); + fakeAdsLoader.eventListeners.get("adsId").onAdPlaybackState(initialAdPlaybackState); + player.play(); + advance(player).untilState(Player.STATE_READY); + + fakeAdsLoader.eventListeners.get("adsId").onAdPlaybackState(populatedAdPlaybackState); + + advance(player).untilState(Player.STATE_ENDED); + player.release(); + ArgumentCaptor oldPositionsCaptor = ArgumentCaptor.forClass(PositionInfo.class); + ArgumentCaptor newPositionsCaptor = ArgumentCaptor.forClass(PositionInfo.class); + ArgumentCaptor reasonsCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mockListener) + .onPositionDiscontinuity( + oldPositionsCaptor.capture(), newPositionsCaptor.capture(), reasonsCaptor.capture()); + assertThat(reasonsCaptor.getAllValues()) + .containsExactly(DISCONTINUITY_REASON_SKIP); // preroll skipped; + List oldPositions = oldPositionsCaptor.getAllValues(); + List newPositions = newPositionsCaptor.getAllValues(); + assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(0); // from preroll + assertThat(oldPositions.get(0).adIndexInAdGroup).isEqualTo(0); + assertThat(newPositions.get(0).adGroupIndex).isEqualTo(C.INDEX_UNSET); // to content + } + + @Test + public void play_adInsertedByTimelineRefresh_reportsAutoTransitionDiscontinuity() + throws PlaybackException, TimeoutException { + Timeline primaryContentTimeline = + new FakeTimeline( + new TimelineWindowDefinition.Builder() + .setWindowPositionInFirstPeriodUs(0L) + .setDurationUs(60_000_000L) + .build()); + AdPlaybackState initialAdPlaybackState = new AdPlaybackState("adsId"); + AdPlaybackState populatedAdPlaybackState = + initialAdPlaybackState + .withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ 500L) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, 10_000_000L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 2_000_000L) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + MediaItem.fromUri("http://example.com/ad_0_0")); + FakeAdsLoader fakeAdsLoader = new FakeAdsLoader(); + AdsMediaSource adsMediaSource = + new AdsMediaSource( + new FakeMediaSource(primaryContentTimeline, ExoPlayerTestRunner.VIDEO_FORMAT), + new DataSpec(Uri.EMPTY), + "adsId", + /* adMediaSourceFactory= */ new FakeMediaSourceFactory( + new TimelineWindowDefinition.Builder()), + fakeAdsLoader, + /* adViewProvider= */ () -> null, + /* useLazyContentSourcePreparation= */ false, + /* useAdMediaSourceClipping= */ true); + ExoPlayer player = parameterizeTestExoPlayerBuilder(new TestExoPlayerBuilder(context)).build(); + Player.Listener mockListener = mock(Player.Listener.class); + player.addListener(mockListener); + player.setMediaSource(adsMediaSource); + player.prepare(); + advance(player) + .untilBackgroundThreadCondition( + (Supplier) () -> fakeAdsLoader.eventListeners.get("adsId") != null); + fakeAdsLoader.eventListeners.get("adsId").onAdPlaybackState(initialAdPlaybackState); + player.play(); + advance(player).untilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 1_000L); + + fakeAdsLoader.eventListeners.get("adsId").onAdPlaybackState(populatedAdPlaybackState); + + advance(player).untilState(Player.STATE_ENDED); + player.release(); + ArgumentCaptor oldPositionsCaptor = ArgumentCaptor.forClass(PositionInfo.class); + ArgumentCaptor newPositionsCaptor = ArgumentCaptor.forClass(PositionInfo.class); + ArgumentCaptor reasonsCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mockListener, times(2)) + .onPositionDiscontinuity( + oldPositionsCaptor.capture(), newPositionsCaptor.capture(), reasonsCaptor.capture()); + assertThat(reasonsCaptor.getAllValues()) + .containsExactly( + DISCONTINUITY_REASON_AUTO_TRANSITION, // content to midroll as an AUTO transition + DISCONTINUITY_REASON_AUTO_TRANSITION); // midroll to content; + List oldPositions = oldPositionsCaptor.getAllValues(); + List newPositions = newPositionsCaptor.getAllValues(); + assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(C.INDEX_UNSET); // from content + assertThat(newPositions.get(0).adGroupIndex).isEqualTo(0); // to midroll + assertThat(newPositions.get(0).adIndexInAdGroup).isEqualTo(0); + // midroll to content + assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(0); // from midroll + assertThat(oldPositions.get(1).adIndexInAdGroup).isEqualTo(0); + assertThat(newPositions.get(1).adGroupIndex).isEqualTo(C.INDEX_UNSET); // to content + } + @Test public void skipAd_withContentResumption_correctPositionAtDiscontinuity() throws PlaybackException, TimeoutException { @@ -1459,7 +1666,7 @@ public void seekTo_toPlayedSAIMidRolls_requestedContentPositionNotPropagatedInto advance(player).untilState(Player.STATE_ENDED); player.release(); - verify(listener, times(1)) + verify(listener) .onPositionDiscontinuity( oldPositionArgumentCaptor.capture(), newPositionArgumentCaptor.capture(), diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerScrubbingTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerScrubbingTest.java index e8930fcd4a2..6b0ae3eef92 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerScrubbingTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerScrubbingTest.java @@ -431,8 +431,8 @@ public void onDisabled() { player.seekTo(20_000L); player.seekTo(30_000L); - // TODO: After implementing ignore for intermittent seeks, the 10s seek will also be completed. - advance(player).untilBackgroundThreadCondition(() -> renderedBitmaps.size() > 1); + // Wait for 10s and 30s seeks to complete. + advance(player).untilBackgroundThreadCondition(() -> renderedBitmaps.size() > 2); player.seekTo(40_000L); player.seekTo(50_000L); @@ -444,10 +444,10 @@ public void onDisabled() { player.release(); surface.release(); - // TODO: After implementing ignore for intermittent seeks, renderedBitmaps should have size 4. - assertThat(renderedBitmaps).hasSize(3); - assertThat(renderedBitmaps.get(1).first).isEqualTo(30_000_000L); - assertThat(renderedBitmaps.get(2).first).isEqualTo(50_000_000L); + assertThat(renderedBitmaps).hasSize(4); + assertThat(renderedBitmaps.get(1).first).isEqualTo(10_000_000L); + assertThat(renderedBitmaps.get(2).first).isEqualTo(30_000_000L); + assertThat(renderedBitmaps.get(3).first).isEqualTo(50_000_000L); // Confirm every seek request still resulted in a position discontinuity callback. ArgumentCaptor newPositionCaptor = ArgumentCaptor.forClass(PositionInfo.class); @@ -460,9 +460,8 @@ public void onDisabled() { .containsExactly(10_000L, 20_000L, 30_000L, 40_000L, 50_000L) .inOrder(); - // TODO: After implementing ignore for intermittent seeks, an onDroppedSeeksWhileScrubbing event - // will be reported. - verify(mockAnalyticsListener, never()).onDroppedSeeksWhileScrubbing(any(), anyInt()); + // Check the dropped 20000 and 40000 seeks are reported. + verify(mockAnalyticsListener).onDroppedSeeksWhileScrubbing(any(), eq(2)); } @Test 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 6743481e76d..2e7f46694e3 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -146,6 +146,7 @@ import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.mediacodec.MediaCodecRenderer; import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; +import androidx.media3.exoplayer.mediacodec.MediaCodecUtil.DecoderQueryException; import androidx.media3.exoplayer.metadata.MetadataOutput; import androidx.media3.exoplayer.source.ClippingMediaSource; import androidx.media3.exoplayer.source.ConcatenatingMediaSource; @@ -13680,6 +13681,155 @@ public void handleMessage(@MessageType int messageType, @Nullable Object message assertThat(listenerIds.getAllValues()).containsExactly(12, 34, 56).inOrder(); } + @Test + public void multipleAudioRenderers_listenerRegisteredOnEnabledRendererOnly() throws Exception { + DefaultAudioSink defaultAudioSink = new DefaultAudioSink.Builder(context).build(); + TestAudioSink testAudioSink = new TestAudioSink(defaultAudioSink); + AudioRendererEventListener listenerA = mock(AudioRendererEventListener.class); + AudioRendererEventListener listenerB = mock(AudioRendererEventListener.class); + RenderersFactory renderersFactory = + new DefaultRenderersFactory(context) { + @Override + protected void buildAudioRenderers( + Context context, + @ExtensionRendererMode int extensionRendererMode, + MediaCodecSelector mediaCodecSelector, + boolean enableDecoderFallback, + AudioSink audioSinkParam, + Handler eventHandler, + AudioRendererEventListener eventListener, + ArrayList out) { + out.add( + new MediaCodecAudioRenderer( + context, + mediaCodecSelector, + enableDecoderFallback, + eventHandler, + listenerA, + testAudioSink)); + out.add( + new MediaCodecAudioRenderer( + context, + mediaCodecSelector, + enableDecoderFallback, + eventHandler, + listenerB, + testAudioSink)); + } + }; + ExoPlayer player = + new TestExoPlayerBuilder(context).setRenderersFactory(renderersFactory).build(); + player.setMediaSource( + new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT)); + player.prepare(); + player.play(); + + advance(player).untilState(Player.STATE_READY); + testAudioSink.listener.onAudioSinkError(new Exception("Test Error")); + advance(player).untilPendingCommandsAreFullyHandled(); + player.release(); + + verify(listenerA).onAudioSinkError(any()); + verify(listenerB, never()).onAudioSinkError(any()); + } + + @Test + public void listenerUpdatedOnRendererSwitch() throws Exception { + DefaultAudioSink defaultAudioSink = new DefaultAudioSink.Builder(context).build(); + TestAudioSink testAudioSink = new TestAudioSink(defaultAudioSink); + AudioRendererEventListener listenerA = mock(AudioRendererEventListener.class); + AudioRendererEventListener listenerB = mock(AudioRendererEventListener.class); + RenderersFactory renderersFactory = + new DefaultRenderersFactory(context) { + @Override + protected void buildAudioRenderers( + Context context, + @ExtensionRendererMode int extensionRendererMode, + MediaCodecSelector mediaCodecSelector, + boolean enableDecoderFallback, + AudioSink audioSinkParam, + Handler eventHandler, + AudioRendererEventListener eventListener, + ArrayList out) { + out.add( + new MediaCodecAudioRenderer( + context, + mediaCodecSelector, + enableDecoderFallback, + eventHandler, + listenerA, + testAudioSink) { + @Override + protected @Capabilities int supportsFormat( + MediaCodecSelector mediaCodecSelector, Format format) + throws DecoderQueryException { + if (MimeTypes.AUDIO_AAC.equals(format.sampleMimeType)) { + return RendererCapabilities.create(C.FORMAT_HANDLED); + } + return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); + } + }); + out.add( + new MediaCodecAudioRenderer( + context, + mediaCodecSelector, + enableDecoderFallback, + eventHandler, + listenerB, + testAudioSink) { + @Override + protected @Capabilities int supportsFormat( + MediaCodecSelector mediaCodecSelector, Format format) + throws DecoderQueryException { + if (MimeTypes.AUDIO_OPUS.equals(format.sampleMimeType)) { + return RendererCapabilities.create(C.FORMAT_HANDLED); + } + return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); + } + }); + } + }; + + ExoPlayer player = + new TestExoPlayerBuilder(context).setRenderersFactory(renderersFactory).build(); + Format formatA = + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_AAC) + .setPcmEncoding(C.ENCODING_PCM_16BIT) + .setChannelCount(2) + .setSampleRate(44100) + .build(); + Format formatB = + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_OPUS) + .setChannelCount(2) + .setSampleRate(48000) + .build(); + MediaSource sourceA = new FakeMediaSource(new FakeTimeline(), formatA); + MediaSource sourceB = new FakeMediaSource(new FakeTimeline(), formatB); + player.setMediaSources(ImmutableList.of(sourceA, sourceB)); + player.prepare(); + player.play(); + + advance(player).untilState(Player.STATE_READY); + testAudioSink.listener.onAudioSinkError(new Exception("Test Error A")); + advance(player).untilPendingCommandsAreFullyHandled(); + + verify(listenerA).onAudioSinkError(any()); + verify(listenerB, never()).onAudioSinkError(any()); + reset(listenerA); + reset(listenerB); + + player.seekToNextMediaItem(); + advance(player).untilState(Player.STATE_READY); + testAudioSink.listener.onAudioSinkError(new Exception("Test Error B")); + advance(player).untilPendingCommandsAreFullyHandled(); + player.release(); + + verify(listenerA, never()).onAudioSinkError(any()); + verify(listenerB).onAudioSinkError(any()); + } + @Test public void multipleAudioSessionIdChangesOnAudioRenderer_propagatesToRenderersAndListener() throws Exception { @@ -13833,6 +13983,63 @@ protected AudioSink buildAudioSink( assertThat(listenerIds.getAllValues()).containsExactly(5678, 910, 1234).inOrder(); } + @Test + public void builderBuild_propagatesAudioSessionIdToRenderersBeforeOnEnabled() throws Exception { + ArrayList configuredAudioSessionIdsOnVideoRenderer = new ArrayList<>(); + AtomicBoolean audioSessionIdReceivedBeforeOnEnabled = new AtomicBoolean(); + AtomicBoolean isRendererEnabled = new AtomicBoolean(); + RenderersFactory renderersFactory = + new DefaultRenderersFactory(context) { + @Override + protected void buildVideoRenderers( + Context context, + @ExtensionRendererMode int extensionRendererMode, + MediaCodecSelector mediaCodecSelector, + boolean enableDecoderFallback, + Handler eventHandler, + VideoRendererEventListener eventListener, + long allowedVideoJoiningTimeMs, + ArrayList out) { + out.add( + new FakeRenderer(C.TRACK_TYPE_VIDEO) { + @Override + public void handleMessage(@MessageType int messageType, @Nullable Object message) + throws ExoPlaybackException { + if (messageType == Renderer.MSG_SET_AUDIO_SESSION_ID) { + configuredAudioSessionIdsOnVideoRenderer.add((int) message); + } + super.handleMessage(messageType, message); + } + + @Override + protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) + throws ExoPlaybackException { + super.onEnabled(joining, mayRenderStartOfStream); + if (!configuredAudioSessionIdsOnVideoRenderer.isEmpty()) { + audioSessionIdReceivedBeforeOnEnabled.set(true); + } + isRendererEnabled.set(true); + } + }); + } + }; + ExoPlayer player = + new TestExoPlayerBuilder(context).setRenderersFactory(renderersFactory).build(); + MediaSource source = + new FakeMediaSource( + new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT); + player.setMediaSource(source); + player.prepare(); + advance(player).untilBackgroundThreadCondition(isRendererEnabled::get); + int audioSessionIdAfterInit = player.getAudioSessionId(); + + assertThat(audioSessionIdReceivedBeforeOnEnabled.get()).isTrue(); + assertThat(audioSessionIdAfterInit).isNotEqualTo(C.AUDIO_SESSION_ID_UNSET); + // Audio Session ID will be provided twice, sent by both the Playback and Application Threads. + assertThat(configuredAudioSessionIdsOnVideoRenderer) + .containsExactly(audioSessionIdAfterInit, audioSessionIdAfterInit); + } + @Test public void setVideoScalingMode_isSetOnPrimaryAndSecondaryVideoRenderers() throws Exception { AtomicBoolean videoScalingSetOnAudioRenderer = new AtomicBoolean(); @@ -15005,4 +15212,18 @@ ImmutableList getReasonChangedList() { return playbackSuppressionList.build(); } } + + private static class TestAudioSink extends ForwardingAudioSink { + @Nullable private Listener listener = null; + + public TestAudioSink(AudioSink sink) { + super(sink); + } + + @Override + public void setListener(Listener listener) { + this.listener = listener; + super.setListener(listener); + } + } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java index 02c970602fe..0c7d496c8b3 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java @@ -1206,7 +1206,7 @@ public void seek_pastReadingPeriodWithSecondaryRendererOnPlayingPeriod_swapsToPr new TestExoPlayerBuilder(context) .setClock(fakeClock) .setRenderersFactory( - new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows( + new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrowsOnRender( fakeClock, attemptedRenderWithSecondaryRenderer, shouldSecondaryRendererThrow)) .build(); player.addListener(listener); @@ -1246,7 +1246,7 @@ public void seek_pastReadingPeriodWithSecondaryRendererOnPlayingPeriod_swapsToPr new TestExoPlayerBuilder(context) .setClock(fakeClock) .setRenderersFactory( - new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows( + new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrowsOnRender( fakeClock, attemptedRenderWithSecondaryRenderer, shouldSecondaryRendererThrow)) .build(); player.addListener(listener); @@ -1295,7 +1295,7 @@ public void play_errorByPrewarmingSecondaryRenderer_primaryRendererIsUsedOnSubse new TestExoPlayerBuilder(context) .setClock(fakeClock) .setRenderersFactory( - new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows( + new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrowsOnRender( fakeClock, attemptedRenderWithSecondaryRenderer, shouldSecondaryRendererThrow)) .build(); player.addListener(listener); @@ -1343,7 +1343,7 @@ public void play_errorByPrewarmingSecondaryRenderer_primaryRendererIsUsedOnSubse new TestExoPlayerBuilder(context) .setClock(fakeClock) .setRenderersFactory( - new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows( + new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrowsOnRender( fakeClock, attemptedRenderWithSecondaryRenderer, shouldSecondaryRendererThrow)) .build(); player.addListener(listener); @@ -1569,7 +1569,7 @@ public void render(long positionUs, long elapsedRealtimeUs) new TestExoPlayerBuilder(context) .setClock(fakeClock) .setRenderersFactory( - new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows( + new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrowsOnRender( fakeClock, attemptedRenderWithSecondaryRenderer, shouldSecondaryRendererThrow)) .build(); player.addListener(listener); @@ -1695,6 +1695,46 @@ public void render(long positionUs, long elapsedRealtimeUs) assertThat(secondaryVideoState3).isEqualTo(Renderer.STATE_STARTED); } + @Test + public void + play_errorInDisablingPrewarmingSecondaryRendererDuringErrorRecovery_doesNotResetPrimaryRenderer() + throws Exception { + Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); + Player.Listener listener = mock(Player.Listener.class); + AtomicBoolean attemptedRenderWithSecondaryRenderer = new AtomicBoolean(false); + AtomicBoolean shouldSecondaryRendererThrow = new AtomicBoolean(true); + ExoPlayer player = + new TestExoPlayerBuilder(context) + .setClock(fakeClock) + .setRenderersFactory( + new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrowsOnRenderDisableAndReset( + fakeClock, attemptedRenderWithSecondaryRenderer, shouldSecondaryRendererThrow)) + .build(); + player.addListener(listener); + Renderer videoRenderer = player.getRenderer(/* index= */ 0); + Renderer secondaryVideoRenderer = player.getSecondaryRenderer(/* index= */ 0); + // Set a playlist that allows a new renderer to be enabled early. + player.setMediaSources( + ImmutableList.of( + // Use FakeBlockingMediaSource so that reading period is not advanced when pre-warming. + new FakeBlockingMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT))); + player.prepare(); + + // Play a bit until the second renderer is enabled and throws errors. + advance(player).untilState(Player.STATE_READY); + player.play(); + advance(player).untilPendingCommandsAreFullyHandled(); + @Renderer.State int videoState = videoRenderer.getState(); + @Renderer.State int secondaryVideoState = secondaryVideoRenderer.getState(); + player.release(); + + assertThat(attemptedRenderWithSecondaryRenderer.get()).isTrue(); + verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt()); + assertThat(videoState).isEqualTo(Renderer.STATE_STARTED); + assertThat(secondaryVideoState).isEqualTo(Renderer.STATE_DISABLED); + } + @Test public void play_withNoSampleRendererAndInitialDiscontinuity_usesPrewarmingAndTransitionsWithoutError() @@ -1865,12 +1905,12 @@ public Renderer createSecondaryRenderer( } } - private static final class FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows + private static final class FakeRenderersFactorySupportingSecondaryVideoRendererThatThrowsOnRender extends FakeRenderersFactorySupportingSecondaryVideoRenderer { private final AtomicBoolean attemptedRenderWithSecondaryRenderer; private final AtomicBoolean shouldSecondaryRendererThrow; - public FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows( + FakeRenderersFactorySupportingSecondaryVideoRendererThatThrowsOnRender( Clock clock, AtomicBoolean attemptedRenderWithSecondaryRenderer, AtomicBoolean shouldSecondaryRendererThrow) { @@ -1909,4 +1949,65 @@ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx return null; } } + + private static final + class FakeRenderersFactorySupportingSecondaryVideoRendererThatThrowsOnRenderDisableAndReset + extends FakeRenderersFactorySupportingSecondaryVideoRenderer { + private final AtomicBoolean attemptedRenderWithSecondaryRenderer; + private final AtomicBoolean shouldSecondaryRendererThrow; + + FakeRenderersFactorySupportingSecondaryVideoRendererThatThrowsOnRenderDisableAndReset( + Clock clock, + AtomicBoolean attemptedRenderWithSecondaryRenderer, + AtomicBoolean shouldSecondaryRendererThrow) { + super(clock); + this.attemptedRenderWithSecondaryRenderer = attemptedRenderWithSecondaryRenderer; + this.shouldSecondaryRendererThrow = shouldSecondaryRendererThrow; + } + + @Override + public Renderer createSecondaryRenderer( + Renderer renderer, + Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput) { + if (renderer instanceof FakeVideoRenderer) { + return new FakeVideoRenderer( + clock.createHandler(eventHandler.getLooper(), /* callback= */ null), + videoRendererEventListener) { + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + attemptedRenderWithSecondaryRenderer.set(true); + if (shouldSecondaryRendererThrow.get()) { + throw createRendererException( + new MediaCodecRenderer.DecoderInitializationException( + new Format.Builder().build(), new IllegalArgumentException(), false, 0), + this.getFormatHolder().format, + PlaybackException.ERROR_CODE_DECODER_INIT_FAILED); + } + super.render(positionUs, elapsedRealtimeUs); + } + + @Override + public void onDisabled() { + if (shouldSecondaryRendererThrow.get()) { + throw new IllegalStateException(); + } + super.onDisabled(); + } + + @Override + public void onReset() { + if (shouldSecondaryRendererThrow.get()) { + throw new IllegalStateException(); + } + super.onReset(); + } + }; + } + return null; + } + } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/AudioCapabilitiesTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/AudioCapabilitiesTest.java index 4f8df74d4e8..24539e4f59f 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/AudioCapabilitiesTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/AudioCapabilitiesTest.java @@ -228,6 +228,7 @@ public void getCapabilities_noBluetoothButHdmiConnected_returnsHdmiCapabilities( AudioDeviceInfo[] audioDeviceInfos = shadowOf(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS); + ImmutableList spatializerChannelMasks = ImmutableList.of(); AudioCapabilities audioCapabilities = AudioCapabilities.getCapabilities( ApplicationProvider.getApplicationContext(), @@ -252,7 +253,8 @@ public void getCapabilities_noBluetoothButHdmiConnected_returnsHdmiCapabilities( /* maxChannelCount= */ 6, expectedDevice != null ? SpeakerLayoutUtil.getLoudspeakerLayoutChannelMasks(expectedDevice) - : ImmutableList.of(AudioFormat.CHANNEL_OUT_STEREO))); + : ImmutableList.of(AudioFormat.CHANNEL_OUT_STEREO), + spatializerChannelMasks)); } @Config(maxSdk = 32) // Fallback test for APIs before 33 @@ -332,9 +334,13 @@ public void getCapabilities_noBluetoothButHdmiConnected_returnsHdmiCapabilities( routedDevice = deviceInfo; } } + ImmutableList spatializerChannelMasks = ImmutableList.of(); AudioCapabilities audioCapabilities = AudioCapabilities.getCapabilities( - ApplicationProvider.getApplicationContext(), AudioAttributes.DEFAULT, routedDevice); + ApplicationProvider.getApplicationContext(), + AudioAttributes.DEFAULT, + routedDevice, + spatializerChannelMasks); assertThat(routedDevice).isNotNull(); assertThat(getDeviceTypes(audioDeviceInfos)) @@ -349,7 +355,8 @@ public void getCapabilities_noBluetoothButHdmiConnected_returnsHdmiCapabilities( AudioFormat.ENCODING_E_AC3_JOC }, /* maxChannelCount= */ 10, - SpeakerLayoutUtil.getLoudspeakerLayoutChannelMasks(routedDevice))); + SpeakerLayoutUtil.getLoudspeakerLayoutChannelMasks(routedDevice), + spatializerChannelMasks)); } @Test @@ -366,11 +373,13 @@ public void getCapabilities_noBluetoothButHdmiConnected_returnsHdmiCapabilities( AudioDeviceInfo[] audioDeviceInfos = shadowOf(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS); + ImmutableList spatializerChannelMasks = ImmutableList.of(); AudioCapabilities audioCapabilities = AudioCapabilities.getCapabilities( ApplicationProvider.getApplicationContext(), AudioAttributes.DEFAULT, - /* routedDevice= */ null); + /* routedDevice= */ null, + spatializerChannelMasks); assertThat(getDeviceTypes(audioDeviceInfos)) .containsAtLeast(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_HDMI); @@ -392,7 +401,8 @@ public void getCapabilities_noBluetoothButHdmiConnected_returnsHdmiCapabilities( AudioFormat.ENCODING_E_AC3_JOC }, /* maxChannelCount= */ 10, - SpeakerLayoutUtil.getLoudspeakerLayoutChannelMasks(expectedDevice))); + SpeakerLayoutUtil.getLoudspeakerLayoutChannelMasks(expectedDevice), + spatializerChannelMasks)); } @Test @@ -406,6 +416,20 @@ public void getCapabilities_noExternalOutputs_notTvNorAutomotive_returnsDefaultC assertThat(audioCapabilities).isEqualTo(AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES); } + @Test + public void getCapabilities_usesSpatializerChannelMasks() { + ImmutableList spatializerChannelMasks = + ImmutableList.of(AudioFormat.CHANNEL_OUT_5POINT1); + AudioCapabilities audioCapabilities = + AudioCapabilities.getCapabilities( + ApplicationProvider.getApplicationContext(), + AudioAttributes.DEFAULT, + /* routedDevice= */ null, + spatializerChannelMasks); + + assertThat(audioCapabilities.getSpatializerChannelMasks()).isEqualTo(spatializerChannelMasks); + } + @Config(minSdk = 29) @Test public void diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java index 13662b88e9b..66f2f08f61b 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java @@ -377,8 +377,28 @@ protected void onOutputFormatChanged(Format format, @Nullable MediaFormat mediaF } @Test - public void - render_callsAudioRendererEventListener_whenAudioSinkListenerOnAudioSinkErrorIsCalled() { + public void render_callsAudioRendererEventListener_whenAudioSinkListenerOnAudioSinkErrorIsCalled() + throws Exception { + Format format = AUDIO_AAC; + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ format, + ImmutableList.of(END_OF_STREAM_ITEM)); + mediaCodecAudioRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {format}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ false, + /* startPositionUs= */ 0, + /* offsetUs= */ 0, + new MediaSource.MediaPeriodId(new Object())); + final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(AudioSink.Listener.class); verify(audioSink, atLeastOnce()).setListener(listenerCaptor.capture()); @@ -395,7 +415,28 @@ protected void onOutputFormatChanged(Format format, @Nullable MediaFormat mediaF @Test public void - renderer_callsRendererCapabilitiesListener_whenAudioSinkListenerOnAudioCapabilitiesChangedIsCalled() { + renderer_callsRendererCapabilitiesListener_whenAudioSinkListenerOnAudioCapabilitiesChangedIsCalled() + throws Exception { + Format format = AUDIO_AAC; + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ format, + ImmutableList.of(END_OF_STREAM_ITEM)); + mediaCodecAudioRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {format}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ false, + /* startPositionUs= */ 0, + /* offsetUs= */ 0, + new MediaSource.MediaPeriodId(new Object())); + final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(AudioSink.Listener.class); verify(audioSink, atLeastOnce()).setListener(listenerCaptor.capture()); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java index 615eb9ce992..02a3bcf4b05 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuerTest.java @@ -57,7 +57,11 @@ public void setUp() throws IOException { codec.start(); handlerThread = new TestHandlerThread("TestHandlerThread"); enqueuer = - new AsynchronousMediaCodecBufferEnqueuer(codec, handlerThread, mockConditionVariable); + new AsynchronousMediaCodecBufferEnqueuer( + codec, + handlerThread, + mockConditionVariable, + /* enableAsyncCryptoSynchronization= */ false); } @After diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java index 30601885ec5..212b08073bc 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java @@ -1098,7 +1098,10 @@ protected void onCodecParametersChanged(CodecParameters codecParameters) { @Override protected DecoderReuseEvaluation canReuseCodec( - MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + MediaCodecInfo codecInfo, + Format oldFormat, + Format newFormat, + boolean isAdaptiveFormatChange) { return codecInfo.canReuseCodec(oldFormat, newFormat); } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtilTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtilTest.java index b134893c431..3ae619218dd 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtilTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtilTest.java @@ -20,6 +20,8 @@ import android.media.MediaCodecInfo; import android.util.Pair; import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.ColorInfo; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -226,6 +228,38 @@ public void getHevcBaseLayerCodecProfileAndLevel_rejectsFormatWithNoInitializati assertThat(MediaCodecUtil.getHevcBaseLayerCodecProfileAndLevel(format)).isNull(); } + @Test + public void getAlternativeCodecMimeType_withNonFallbackCompatibleFormat_returnsNull() { + // Profile 10.0 (Full Range PQ) which does NOT allow fallback. + Format formatDav1NoFallbackPossible = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) + .setCodecs("dav1.10.01") + .setColorInfo( + new ColorInfo.Builder() + .setColorSpace(C.COLOR_SPACE_BT2020) + .setColorTransfer(C.COLOR_TRANSFER_ST2084) + .setColorRange(C.COLOR_RANGE_FULL) + .build()) + .build(); + // Profile 10.1 (Limited Range PQ) which allows fallback to AV1. + Format formatDav1FallbackToAv1 = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) + .setCodecs("dav1.10.01") + .setColorInfo( + new ColorInfo.Builder() + .setColorSpace(C.COLOR_SPACE_BT2020) + .setColorTransfer(C.COLOR_TRANSFER_ST2084) + .setColorRange(C.COLOR_RANGE_LIMITED) + .build()) + .build(); + + assertThat(MediaCodecUtil.getAlternativeCodecMimeType(formatDav1NoFallbackPossible)).isNull(); + assertThat(MediaCodecUtil.getAlternativeCodecMimeType(formatDav1FallbackToAv1)) + .isEqualTo(MimeTypes.VIDEO_AV1); + } + private static void assertHevcBaseLayerCodecProfileAndLevelForFormat( Format format, int profile, int level) { @Nullable diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java index 57d6e5a76b4..011c0d3a078 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriodTest.java @@ -21,7 +21,6 @@ import android.net.Uri; import androidx.annotation.Nullable; import androidx.media3.common.C; -import androidx.media3.common.TrackGroup; import androidx.media3.common.util.Consumer; import androidx.media3.common.util.NullableType; import androidx.media3.datasource.AssetDataSource; @@ -32,9 +31,6 @@ import androidx.media3.exoplayer.drm.DrmSessionEventListener; import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; -import androidx.media3.exoplayer.source.chunk.MediaChunk; -import androidx.media3.exoplayer.source.chunk.MediaChunkIterator; -import androidx.media3.exoplayer.trackselection.BaseTrackSelection; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; @@ -45,9 +41,9 @@ import androidx.media3.extractor.mp4.Mp4Extractor; import androidx.media3.extractor.png.PngExtractor; import androidx.media3.extractor.text.SubtitleParser; +import androidx.media3.test.utils.FakeTrackSelection; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeoutException; @@ -119,7 +115,8 @@ public void readData_forUnselectedTrack_returnsFormatAndNothingElse() throws Exc boolean[] streamResetFlags = new boolean[trackGroups.length]; // Select only track 0. - selections[0] = new FakeTrackSelection(trackGroups.get(0), 0); + selections[0] = + new FakeTrackSelection(trackGroups.get(0), new int[] {0}, /* selectedIndex= */ 0); long unused = mediaPeriod.selectTracks( selections, @@ -313,37 +310,4 @@ public void run() { super.run(); } } - - public static final class FakeTrackSelection extends BaseTrackSelection { - - public FakeTrackSelection(TrackGroup group, int... tracks) { - super(group, tracks); - } - - @Override - public void updateSelectedTrack( - long playbackPositionUs, - long bufferedDurationUs, - long availableDurationUs, - List queue, - MediaChunkIterator[] mediaChunkIterators) { - // Do nothing. - } - - @Override - public int getSelectedIndex() { - return 0; - } - - @Override - public int getSelectionReason() { - return C.SELECTION_REASON_UNKNOWN; - } - - @Nullable - @Override - public Object getSelectionData() { - return null; - } - } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStreamTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStreamTest.java new file mode 100644 index 00000000000..0c0c97a37ed --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStreamTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.source.chunk; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.datasource.DataSource; +import androidx.media3.datasource.DataSpec; +import androidx.media3.exoplayer.LoadingInfo; +import androidx.media3.exoplayer.drm.DrmSessionEventListener; +import androidx.media3.exoplayer.drm.DrmSessionManager; +import androidx.media3.exoplayer.source.MediaSourceEventListener; +import androidx.media3.exoplayer.source.SequenceableLoader; +import androidx.media3.exoplayer.upstream.DefaultAllocator; +import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; +import androidx.media3.extractor.TrackOutput; +import androidx.media3.test.utils.FakeAdaptiveDataSet; +import androidx.media3.test.utils.FakeChunkSource; +import androidx.media3.test.utils.FakeDataSource; +import androidx.media3.test.utils.FakeTrackSelection; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.IOException; +import java.util.List; +import java.util.Random; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link ChunkSampleStream}. */ +@RunWith(AndroidJUnit4.class) +public final class ChunkSampleStreamTest { + + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + @Mock private ChunkSource mockChunkSource; + @Mock private SequenceableLoader.Callback> mockCallback; + @Mock private DrmSessionEventListener.EventDispatcher mockDrmEventDispatcher; + @Mock private MediaSourceEventListener.EventDispatcher mockMediaSourceEventDispatcher; + + private ChunkSampleStream chunkSampleStream; + private DefaultAllocator allocator; + + @Before + public void setUp() { + allocator = + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 65536); + chunkSampleStream = + new ChunkSampleStream<>( + /* primaryTrackType= */ C.TRACK_TYPE_VIDEO, + /* embeddedTrackTypes= */ null, + /* embeddedTrackFormats= */ null, + mockChunkSource, + mockCallback, + allocator, + /* positionUs= */ 0, + DrmSessionManager.DRM_UNSUPPORTED, + mockDrmEventDispatcher, + new DefaultLoadErrorHandlingPolicy(), + mockMediaSourceEventDispatcher, + /* handleInitialDiscontinuity= */ false, + /* firstChunkStartTimeUs= */ C.TIME_UNSET, + /* downloadExecutor= */ null); + } + + @Test + public void seekToUs_intoCanceledChunk_fallsBackToReset() throws Exception { + Format format = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_MP4) + .setAverageBitrate(1000000) + .build(); + TrackGroup trackGroup = new TrackGroup(format); + FakeAdaptiveDataSet.Factory dataSetFactory = + new FakeAdaptiveDataSet.Factory( + /* chunkDurationUs= */ 500000, /* bitratePercentStdDev= */ 0.0, new Random(0)); + FakeAdaptiveDataSet dataSet = + dataSetFactory.createDataSet(trackGroup, /* mediaDurationUs= */ 1000000); + FakeDataSource.Factory dataSourceFactory = new FakeDataSource.Factory(); + dataSourceFactory.setFakeDataSet(dataSet); + FakeDataSource fakeDataSource = dataSourceFactory.createDataSource(); + FakeTrackSelection trackSelection = + new FakeTrackSelection(trackGroup) { + @Override + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { + // Do nothing. + } + + @Override + public boolean shouldCancelChunkLoad( + long playbackPositionUs, Chunk loadingChunk, List queue) { + // Always return true to simulate the cancellation of the first chunk. + return true; + } + }; + final BaseMediaChunkOutput[] capturedOutput = new BaseMediaChunkOutput[/* size= */ 1]; + BaseMediaChunk testChunk = + new BaseMediaChunk( + fakeDataSource, + new DataSpec(Uri.parse(dataSet.getUri(0))), + format, + /* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* startTimeUs= */ 0, + /* endTimeUs= */ 1000000, + /* clippedStartTimeUs= */ C.TIME_UNSET, + /* clippedEndTimeUs= */ C.TIME_UNSET, + /* chunkIndex= */ 0) { + @Override + public void init(BaseMediaChunkOutput output) { + super.init(output); + capturedOutput[/* index= */ 0] = output; + } + + @Override + public void cancelLoad() {} + + @Override + public void load() throws IOException {} + + @Override + public boolean isLoadCompleted() { + return false; + } + }; + FakeChunkSource fakeChunkSource = + new FakeChunkSource(trackSelection, fakeDataSource, dataSet) { + @Override + protected MediaChunk createMediaChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + @C.SelectionReason int trackSelectionReason, + @Nullable Object trackSelectionData, + long startTimeUs, + long endTimeUs, + long chunkIndex, + @C.TrackType int trackType, + Format sampleFormat) { + return testChunk; + } + }; + chunkSampleStream = + new ChunkSampleStream<>( + /* primaryTrackType= */ C.TRACK_TYPE_VIDEO, + /* embeddedTrackTypes= */ null, + /* embeddedTrackFormats= */ null, + fakeChunkSource, + mockCallback, + allocator, + /* positionUs= */ 0, + DrmSessionManager.DRM_UNSUPPORTED, + mockDrmEventDispatcher, + new DefaultLoadErrorHandlingPolicy(), + mockMediaSourceEventDispatcher, + /* handleInitialDiscontinuity= */ false, + /* firstChunkStartTimeUs= */ C.TIME_UNSET, + /* downloadExecutor= */ null); + LoadingInfo loadingInfo = + new LoadingInfo.Builder().setPlaybackPositionUs(/* playbackPositionUs= */ 0).build(); + assertThat(chunkSampleStream.continueLoading(loadingInfo)).isTrue(); + TrackOutput trackOutput = + capturedOutput[/* index= */ 0].track(/* id= */ 0, /* type= */ C.TRACK_TYPE_VIDEO); + trackOutput.format(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_MP4).build()); + byte[] sampleBytes = new byte[/* size= */ 10]; + ParsableByteArray data = new ParsableByteArray(sampleBytes); + trackOutput.sampleData(data, /* length= */ 10); + trackOutput.sampleMetadata( + /* timeUs= */ 0, + /* flags= */ C.BUFFER_FLAG_KEY_FRAME, + /* size= */ 10, + /* offset= */ 0, + /* cryptoData= */ null); + data.setPosition(/* position= */ 0); + trackOutput.sampleData(data, /* length= */ 10); + trackOutput.sampleMetadata( + /* timeUs= */ 500000, + /* flags= */ C.BUFFER_FLAG_KEY_FRAME, + /* size= */ 10, + /* offset= */ 0, + /* cryptoData= */ null); + + // Simulate cancellation, seek into the canceled range, and delivery of cancellation event. + chunkSampleStream.reevaluateBuffer(/* positionUs= */ 0); + chunkSampleStream.seekToUs(/* positionUs= */ 500000); + chunkSampleStream.onLoadCanceled( + testChunk, /* elapsedRealtimeMs= */ 0, /* loadDurationMs= */ 0, /* released= */ false); + + // Verify that the seek fell back to reset instead of succeeding inside the buffer. + assertThat(chunkSampleStream.isPendingReset()).isTrue(); + } +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java index dcddaa332d6..d1d091d9ef5 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/preload/PreloadMediaSourceTest.java @@ -509,7 +509,7 @@ public MediaPeriod createPeriod( startPositionUs, new MediaSourceEventListener.EventDispatcher()) { @Override - public void prepare(Callback callback, long positionUs) { + public synchronized void prepare(Callback callback, long positionUs) { // Do nothing to simulate that something wrong happens and onPrepared will not // be called. } @@ -986,6 +986,116 @@ public void release() { assertThat(preloadingMediaPeriodReleased.get()).isTrue(); } + @Test + public void clear_whenContentPeriodJustPrepared_preloadTerminatesWithoutException() { + AtomicReference fakeMediaPeriodReference = new AtomicReference<>(); + MediaSource.Factory mockMediaSourceFactory = mock(MediaSource.Factory.class); + when(mockMediaSourceFactory.createMediaSource(any())) + .thenReturn( + new FakeMediaSource() { + @Override + protected MediaPeriod createMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, + DrmSessionManager drmSessionManager, + DrmSessionEventListener.EventDispatcher drmEventDispatcher, + @Nullable TransferListener transferListener) { + FakeMediaPeriod fakeMediaPeriod = + new FakeMediaPeriod( + trackGroupArray, + allocator, + FakeTimeline.TimelineWindowDefinition + .DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + mediaSourceEventDispatcher, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* deferOnPrepared= */ true); + fakeMediaPeriodReference.set(fakeMediaPeriod); + return fakeMediaPeriod; + } + }); + TrackSelector trackSelector = new FakeTrackSelector(); + trackSelector.init(() -> {}, bandwidthMeter); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mockMediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + preloadMediaSource.preload(/* startPositionUs= */ 0L); + shadowOf(Looper.getMainLooper()).idle(); + + fakeMediaPeriodReference.get().setPreparationComplete(); + preloadMediaSource.clear(); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mockPreloadControl, never()).onTracksSelected(eq(preloadMediaSource)); + } + + @Test + public void + clear_whenContentPeriodJustRequestedContinueLoading_preloadTerminatesWithoutException() { + AtomicReference capturedCallback = new AtomicReference<>(); + AtomicReference fakeMediaPeriodReference = new AtomicReference<>(); + MediaSource.Factory mockMediaSourceFactory = mock(MediaSource.Factory.class); + when(mockMediaSourceFactory.createMediaSource(any())) + .thenReturn( + new FakeMediaSource() { + @Override + protected MediaPeriod createMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, + DrmSessionManager drmSessionManager, + DrmSessionEventListener.EventDispatcher drmEventDispatcher, + @Nullable TransferListener transferListener) { + FakeMediaPeriod fakeMediaPeriod = + new FakeMediaPeriod( + trackGroupArray, + allocator, + FakeTimeline.TimelineWindowDefinition + .DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, + mediaSourceEventDispatcher) { + @Override + public synchronized void prepare(Callback callback, long positionUs) { + capturedCallback.set(callback); + super.prepare(callback, positionUs); + } + }; + fakeMediaPeriodReference.set(fakeMediaPeriod); + return fakeMediaPeriod; + } + }); + TrackSelector trackSelector = new FakeTrackSelector(); + trackSelector.init(() -> {}, bandwidthMeter); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mockMediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + preloadMediaSource.preload(/* startPositionUs= */ 0L); + shadowOf(Looper.getMainLooper()).idle(); + + capturedCallback.get().onContinueLoadingRequested(fakeMediaPeriodReference.get()); + preloadMediaSource.clear(); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mockPreloadControl, never()) + .onContinueLoadingRequested(eq(preloadMediaSource), anyLong()); + } + @Test public void releaseSourceByAllExternalCallers_preloadNotCalledBefore_releaseInternalSource() { AtomicReference internalSourceReference = new AtomicReference<>(); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java index 0bb85c78777..60bb77928df 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java @@ -48,6 +48,7 @@ import android.media.Spatializer; import android.view.accessibility.CaptioningManager; import androidx.media3.common.C; +import androidx.media3.common.ColorInfo; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.Timeline; @@ -2851,12 +2852,16 @@ public void selectTracks_withPreferredVideoMimeTypes_selectsTrackWithPreferredMi @Test public void selectTracks_withDecoderSupportFallbackMimetype_selectsTrackWithPrimaryDecoder() throws Exception { - Format formatDV = - new Format.Builder().setId("0").setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION).build(); + Format formatDv = + new Format.Builder() + .setId("0") + .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) + .setCodecs("dvh1.04.01") + .build(); Format formatHevc = new Format.Builder().setId("1").setSampleMimeType(MimeTypes.VIDEO_H265).build(); TrackGroupArray trackGroups = - new TrackGroupArray(new TrackGroup(formatDV), new TrackGroup(formatHevc)); + new TrackGroupArray(new TrackGroup(formatDv), new TrackGroup(formatHevc)); @Capabilities int capabilitiesDecoderSupportPrimary = RendererCapabilities.create( @@ -2903,7 +2908,222 @@ public void selectTracks_withDecoderSupportFallbackMimetype_selectsTrackWithPrim periodId, TIMELINE); - assertFixedSelection(result.selections[0], trackGroups, formatDV); + assertFixedSelection(result.selections[0], trackGroups, formatDv); + } + + @Test + public void selectTracks_withAvcAndDolbyVisionFallbackToHevc_selectsDolbyVision() + throws Exception { + Format formatAvc = + new Format.Builder().setId("avc").setSampleMimeType(MimeTypes.VIDEO_H264).build(); + Format formatDv = + new Format.Builder().setId("dv").setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION).build(); + TrackGroupArray trackGroups = + new TrackGroupArray(new TrackGroup(formatAvc), new TrackGroup(formatDv)); + + @Capabilities + int capabilitiesAvc = + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + HARDWARE_ACCELERATION_SUPPORTED, + DECODER_SUPPORT_PRIMARY); + int capabilitiesDv = + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + HARDWARE_ACCELERATION_SUPPORTED, + DECODER_SUPPORT_FALLBACK_MIMETYPE); + + ImmutableMap rendererCapabilitiesMap = + ImmutableMap.of("avc", capabilitiesAvc, "dv", capabilitiesDv); + RendererCapabilities rendererCapabilities = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_VIDEO, rendererCapabilitiesMap); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {rendererCapabilities}, trackGroups, periodId, TIMELINE); + + assertFixedSelection(result.selections[0], trackGroups, formatDv); + } + + @Test + public void selectTracks_withHevcHdrAndDolbyVisionFallbackToHevc_prefersHevcHdrOverFallbackDv() + throws Exception { + ColorInfo hdrColorInfo = + new ColorInfo.Builder().setColorTransfer(C.COLOR_TRANSFER_ST2084).build(); + Format formatHevcHdr = + new Format.Builder() + .setId("hevc_hdr") + .setSampleMimeType(MimeTypes.VIDEO_H265) + .setColorInfo(hdrColorInfo) + .build(); + Format formatDv = + new Format.Builder().setId("dv").setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION).build(); + TrackGroupArray trackGroups = + new TrackGroupArray(new TrackGroup(formatHevcHdr), new TrackGroup(formatDv)); + + @Capabilities + int capabilitiesHevc = + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + HARDWARE_ACCELERATION_SUPPORTED, + DECODER_SUPPORT_PRIMARY); + int capabilitiesDv = + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + HARDWARE_ACCELERATION_SUPPORTED, + DECODER_SUPPORT_FALLBACK_MIMETYPE); + + ImmutableMap rendererCapabilitiesMap = + ImmutableMap.of("hevc_hdr", capabilitiesHevc, "dv", capabilitiesDv); + RendererCapabilities rendererCapabilities = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_VIDEO, rendererCapabilitiesMap); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {rendererCapabilities}, trackGroups, periodId, TIMELINE); + + assertFixedSelection(result.selections[0], trackGroups, formatHevcHdr); + } + + @Test + public void selectTracks_withAvc1080pAndHevc720p_prefersResolutionOverCodec() throws Exception { + Format formatAvc1080p = + new Format.Builder() + .setId("avc_1080p") + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setWidth(1920) + .setHeight(1080) + .build(); + Format formatHevc720p = + new Format.Builder() + .setId("hevc_720p") + .setSampleMimeType(MimeTypes.VIDEO_H265) + .setWidth(1280) + .setHeight(720) + .build(); + TrackGroupArray trackGroups = + new TrackGroupArray(new TrackGroup(formatAvc1080p), new TrackGroup(formatHevc720p)); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE); + + assertFixedSelection(result.selections[0], trackGroups, formatAvc1080p); + } + + @Test + public void selectTracks_with720pHdrAnd1080pSdr_prefersHdrOverResolution() throws Exception { + ColorInfo hdrColorInfo = + new ColorInfo.Builder().setColorTransfer(C.COLOR_TRANSFER_ST2084).build(); + Format format720pHdr = + new Format.Builder() + .setId("720p_hdr") + .setSampleMimeType(MimeTypes.VIDEO_H265) + .setWidth(1280) + .setHeight(720) + .setColorInfo(hdrColorInfo) + .build(); + Format format1080pSdr = + new Format.Builder() + .setId("1080p_sdr") + .setSampleMimeType(MimeTypes.VIDEO_H265) + .setWidth(1920) + .setHeight(1080) + .build(); + TrackGroupArray trackGroups = + new TrackGroupArray(new TrackGroup(format720pHdr), new TrackGroup(format1080pSdr)); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE); + + assertFixedSelection(result.selections[0], trackGroups, format720pHdr); + } + + @Test + public void selectTracks_withAvcAndHevcSameResolution_prefersHevcOverAvc() throws Exception { + Format formatAvc = + new Format.Builder() + .setId("avc") + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setWidth(1920) + .setHeight(1080) + .setAverageBitrate(5000000) + .build(); + Format formatHevc = + new Format.Builder() + .setId("hevc") + .setSampleMimeType(MimeTypes.VIDEO_H265) + .setWidth(1920) + .setHeight(1080) + .setAverageBitrate(2000000) + .build(); + TrackGroupArray trackGroups = + new TrackGroupArray(new TrackGroup(formatAvc), new TrackGroup(formatHevc)); + + int capabilities = + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + HARDWARE_ACCELERATION_SUPPORTED, + DECODER_SUPPORT_PRIMARY); + RendererCapabilities videoCapabilitiesWithHw = + new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO, capabilities); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {videoCapabilitiesWithHw}, trackGroups, periodId, TIMELINE); + + assertFixedSelection(result.selections[0], trackGroups, formatHevc); + } + + @Test + public void selectTracks_withAvcHdrAndHevcSdrSameResolution_prefersHdrOverCodec() + throws Exception { + ColorInfo hdrColorInfo = + new ColorInfo.Builder().setColorTransfer(C.COLOR_TRANSFER_ST2084).build(); + Format formatAvcHdr = + new Format.Builder() + .setId("avc_hdr") + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setWidth(1920) + .setHeight(1080) + .setColorInfo(hdrColorInfo) + .build(); + Format formatHevcSdr = + new Format.Builder() + .setId("hevc_sdr") + .setSampleMimeType(MimeTypes.VIDEO_H265) + .setWidth(1920) + .setHeight(1080) + .build(); + TrackGroupArray trackGroups = + new TrackGroupArray(new TrackGroup(formatAvcHdr), new TrackGroup(formatHevcSdr)); + + int capabilities = + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + HARDWARE_ACCELERATION_SUPPORTED, + DECODER_SUPPORT_PRIMARY); + RendererCapabilities videoCapabilitiesWithHw = + new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO, capabilities); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {videoCapabilitiesWithHw}, trackGroups, periodId, TIMELINE); + + assertFixedSelection(result.selections[0], trackGroups, formatAvcHdr); } @Test 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 aa655bd3d33..54936bbf900 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 @@ -42,10 +42,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.annotation.Config.ALL_SDKS; import android.content.Context; import android.graphics.SurfaceTexture; @@ -62,6 +65,7 @@ import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.ColorInfo; +import androidx.media3.common.DrmInitData; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.TrackGroup; @@ -79,6 +83,7 @@ import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.ScrubbingModeParameters; import androidx.media3.exoplayer.analytics.PlayerId; +import androidx.media3.exoplayer.drm.DrmSession; import androidx.media3.exoplayer.drm.DrmSessionEventListener; import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.mediacodec.ForwardingMediaCodecAdapter; @@ -97,6 +102,7 @@ import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.test.utils.FakeClock; +import androidx.media3.test.utils.FakeCryptoConfig; import androidx.media3.test.utils.FakeMediaPeriod; import androidx.media3.test.utils.FakeSampleStream; import androidx.media3.test.utils.FakeTimeline; @@ -106,11 +112,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import java.io.IOException; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.PriorityQueue; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import org.junit.After; @@ -143,6 +149,17 @@ public class MediaCodecVideoRendererTest { private static final Format VIDEO_H264_24FPS = VIDEO_H264.buildUpon().setFrameRate(24).build(); private static final Format VIDEO_H264_30FPS = VIDEO_H264.buildUpon().setFrameRate(30).build(); + private static final Format VIDEO_H264_60FPS = VIDEO_H264.buildUpon().setFrameRate(60).build(); + private static final Format VIDEO_H264_24FPS_SECURE = + VIDEO_H264_24FPS + .buildUpon() + .setDrmInitData(new DrmInitData(/* schemeDatas= */ ImmutableList.of())) + .build(); + private static final Format VIDEO_H264_30FPS_SECURE = + VIDEO_H264_30FPS + .buildUpon() + .setDrmInitData(new DrmInitData(/* schemeDatas= */ ImmutableList.of())) + .build(); private static final Format VIDEO_AV1 = new Format.Builder() @@ -203,7 +220,7 @@ public void setUp() throws Exception { /* softwareOnly= */ true, /* vendor= */ false, /* forceDisableAdaptive= */ false, - /* forceSecure= */ false)); + /* forceSecure= */ requiresSecureDecoder)); mediaCodecVideoRenderer = new MediaCodecVideoRenderer( new MediaCodecVideoRenderer.Builder(ApplicationProvider.getApplicationContext()) @@ -229,7 +246,13 @@ protected void onOutputFormatChanged(Format format, @Nullable MediaFormat mediaF }; mediaCodecVideoRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT); - surface = new Surface(new SurfaceTexture(/* texName= */ 0)); + surface = + new Surface(new SurfaceTexture(/* texName= */ 0)) { + @Override + public boolean isValid() { + return true; // Pretend to be valid, so the renderer always uses the surface for output. + } + }; mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface); } @@ -1097,7 +1120,7 @@ public void render_sendsVideoSizeChangeWithCurrentFormatValues() throws Exceptio new MediaSource.MediaPeriodId(new Object())); mediaCodecVideoRenderer.start(); mediaCodecVideoRenderer.render(/* positionUs= */ 0, msToUs(SystemClock.elapsedRealtime())); - ShadowSystemClock.advanceBy(10, TimeUnit.MILLISECONDS); + ShadowSystemClock.advanceBy(Duration.ofMillis(10)); mediaCodecVideoRenderer.render(/* positionUs= */ 10_000, msToUs(SystemClock.elapsedRealtime())); fakeSampleStream.append( @@ -1114,7 +1137,7 @@ public void render_sendsVideoSizeChangeWithCurrentFormatValues() throws Exceptio int positionUs = 20_000; do { - ShadowSystemClock.advanceBy(2, TimeUnit.MILLISECONDS); + ShadowSystemClock.advanceBy(Duration.ofMillis(2)); mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime())); codecAdapterFactory.idleQueueingAndCallbackThreads(); positionUs += 2_000; @@ -1173,7 +1196,7 @@ public void render_includingResetPosition_keepsOutputFormatInVideoFrameMetadataL @Config(maxSdk = 29) @Test - public void render_withFrameRateChangeUpToSdk29_discardsCodec() throws Exception { + public void render_withIncompatibleFrameRateChangeUpToSdk29_discardsCodec() throws Exception { SystemClock.setCurrentTimeMillis(876_000_000); FakeSampleStream fakeSampleStream = new FakeSampleStream( @@ -1202,7 +1225,7 @@ public void render_withFrameRateChangeUpToSdk29_discardsCodec() throws Exception codecAdapterFactory.idleQueueingAndCallbackThreads(); shadowOf(testMainLooper).idle(); - // Feed format change. + // Feed format change from 24->30 fps, which is not generally compatible and should reset codec. fakeSampleStream.append( ImmutableList.of( format(VIDEO_H264_30FPS), @@ -1213,7 +1236,7 @@ public void render_withFrameRateChangeUpToSdk29_discardsCodec() throws Exception // Render until format change is processed and renderer reaches end of stream. int positionUs = 40_000; while (!mediaCodecVideoRenderer.isEnded()) { - ShadowSystemClock.advanceBy(40, TimeUnit.MILLISECONDS); + ShadowSystemClock.advanceBy(Duration.ofMillis(40)); mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime())); codecAdapterFactory.idleQueueingAndCallbackThreads(); positionUs += 40_000; @@ -1226,7 +1249,7 @@ public void render_withFrameRateChangeUpToSdk29_discardsCodec() throws Exception @Config(minSdk = 30) @Test - public void render_withFrameRateChangeFromSdk30_keepsCodec() throws Exception { + public void render_withIncompatibleFrameRateChangeFromSdk30_keepsCodec() throws Exception { SystemClock.setCurrentTimeMillis(876_000_000); FakeSampleStream fakeSampleStream = new FakeSampleStream( @@ -1266,7 +1289,259 @@ public void render_withFrameRateChangeFromSdk30_keepsCodec() throws Exception { // Render until format change is processed and renderer reaches end of stream. int positionUs = 40_000; while (!mediaCodecVideoRenderer.isEnded()) { - ShadowSystemClock.advanceBy(40, TimeUnit.MILLISECONDS); + ShadowSystemClock.advanceBy(Duration.ofMillis(40)); + mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime())); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + positionUs += 40_000; + } + shadowOf(testMainLooper).idle(); + + verify(eventListener, never()).onVideoDecoderReleased(any()); + verify(eventListener).onVideoDecoderInitialized(any(), anyLong(), anyLong()); + } + + @Config(sdk = ALL_SDKS) + @Test + public void render_withCompatibleFrameRateChange_keepsCodec() throws Exception { + SystemClock.setCurrentTimeMillis(876_000_000); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ VIDEO_H264_30FPS, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 40_000))); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264_30FPS, VIDEO_H264_60FPS}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0, + new MediaSource.MediaPeriodId(new Object())); + mediaCodecVideoRenderer.start(); + // Render first sample to initialize codec. + mediaCodecVideoRenderer.render(/* positionUs= */ 0, msToUs(SystemClock.elapsedRealtime())); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + shadowOf(testMainLooper).idle(); + + // Feed format change from 30->60 fps, which should be compatible without reset. + fakeSampleStream.append( + ImmutableList.of( + format(VIDEO_H264_60FPS), + oneByteSample(/* timeUs= */ 80_000, C.BUFFER_FLAG_KEY_FRAME), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 40_000); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + // Render until format change is processed and renderer reaches end of stream. + int positionUs = 40_000; + while (!mediaCodecVideoRenderer.isEnded()) { + ShadowSystemClock.advanceBy(Duration.ofMillis(40)); + mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime())); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + positionUs += 40_000; + } + shadowOf(testMainLooper).idle(); + + verify(eventListener, never()).onVideoDecoderReleased(any()); + verify(eventListener).onVideoDecoderInitialized(any(), anyLong(), anyLong()); + } + + @Config(sdk = ALL_SDKS) + @Test + public void render_withIncompatibleFrameRateChangeAtSecureAdaptiveSwitch_keepsCodec() + throws Exception { + SystemClock.setCurrentTimeMillis(876_000_000); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + getFakeDrmSessionManager(), + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ VIDEO_H264_24FPS_SECURE, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 40_000))); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264_24FPS_SECURE, VIDEO_H264_30FPS_SECURE}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0, + new MediaSource.MediaPeriodId(new Object())); + mediaCodecVideoRenderer.start(); + // Render first sample to initialize codec. + mediaCodecVideoRenderer.render(/* positionUs= */ 0, msToUs(SystemClock.elapsedRealtime())); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + shadowOf(testMainLooper).idle(); + + // Feed format change from 24->30 fps, which is not generally compatible, but should still keep + // the codec to avoid a black frame. + fakeSampleStream.append( + ImmutableList.of( + format(VIDEO_H264_30FPS_SECURE), + oneByteSample(/* timeUs= */ 80_000, C.BUFFER_FLAG_KEY_FRAME), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 40_000); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + // Render until format change is processed and renderer reaches end of stream. + int positionUs = 40_000; + while (!mediaCodecVideoRenderer.isEnded()) { + ShadowSystemClock.advanceBy(Duration.ofMillis(40)); + mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime())); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + positionUs += 40_000; + } + shadowOf(testMainLooper).idle(); + + verify(eventListener, never()).onVideoDecoderReleased(any()); + verify(eventListener).onVideoDecoderInitialized(any(), anyLong(), anyLong()); + } + + @Config(maxSdk = 29) + @Test + public void render_withIncompatibleFrameRateChangeAtSecureTransitionUpToSdk29_discardsCodec() + throws Exception { + SystemClock.setCurrentTimeMillis(876_000_000); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + getFakeDrmSessionManager(), + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ VIDEO_H264_24FPS_SECURE, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 40_000), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + FakeSampleStream fakeSampleStream2 = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + getFakeDrmSessionManager(), + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ VIDEO_H264_30FPS_SECURE, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 40_000), + END_OF_STREAM_ITEM)); + fakeSampleStream2.writeData(/* startPositionUs= */ 0); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264_24FPS_SECURE}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0, + new MediaSource.MediaPeriodId(new Object())); + mediaCodecVideoRenderer.start(); + + // Render until all samples of first stream are read. + int positionUs = 0; + while (!mediaCodecVideoRenderer.hasReadStreamToEnd()) { + ShadowSystemClock.advanceBy(Duration.ofMillis(40)); + mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime())); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + positionUs += 40_000; + } + shadowOf(testMainLooper).idle(); + + // Replace stream from 24->30 fps, which is not generally compatible and should reset the codec. + mediaCodecVideoRenderer.replaceStream( + new Format[] {VIDEO_H264_30FPS_SECURE}, + fakeSampleStream2, + /* startPositionUs= */ 80_000, + /* offsetUs= */ 80_000, + new MediaSource.MediaPeriodId(new Object())); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + // Render until end of stream. + while (!mediaCodecVideoRenderer.isEnded()) { + ShadowSystemClock.advanceBy(Duration.ofMillis(40)); + mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime())); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + positionUs += 40_000; + } + shadowOf(testMainLooper).idle(); + + verify(eventListener).onVideoDecoderReleased(any()); + verify(eventListener, times(2)).onVideoDecoderInitialized(any(), anyLong(), anyLong()); + } + + @Config(minSdk = 30) + @Test + public void render_withIncompatibleFrameRateChangeAtSecureTransitionFromSdk30_keepsCodec() + throws Exception { + SystemClock.setCurrentTimeMillis(876_000_000); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + getFakeDrmSessionManager(), + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ VIDEO_H264_24FPS_SECURE, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 40_000), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + FakeSampleStream fakeSampleStream2 = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + getFakeDrmSessionManager(), + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ VIDEO_H264_30FPS_SECURE, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 40_000), + END_OF_STREAM_ITEM)); + fakeSampleStream2.writeData(/* startPositionUs= */ 0); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264_24FPS_SECURE}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0, + new MediaSource.MediaPeriodId(new Object())); + mediaCodecVideoRenderer.start(); + + // Render until all samples of first stream are read. + int positionUs = 0; + while (!mediaCodecVideoRenderer.hasReadStreamToEnd()) { + ShadowSystemClock.advanceBy(Duration.ofMillis(40)); + mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime())); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + positionUs += 40_000; + } + shadowOf(testMainLooper).idle(); + + // Replace stream from 24->30 fps, which is not generally compatible and should reset the codec. + mediaCodecVideoRenderer.replaceStream( + new Format[] {VIDEO_H264_30FPS_SECURE}, + fakeSampleStream2, + /* startPositionUs= */ 80_000, + /* offsetUs= */ 80_000, + new MediaSource.MediaPeriodId(new Object())); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + // Render until end of stream. + while (!mediaCodecVideoRenderer.isEnded()) { + ShadowSystemClock.advanceBy(Duration.ofMillis(40)); mediaCodecVideoRenderer.render(positionUs, msToUs(SystemClock.elapsedRealtime())); codecAdapterFactory.idleQueueingAndCallbackThreads(); positionUs += 40_000; @@ -2144,6 +2419,208 @@ public void render_afterSeek_samplesWithoutDependencies_doesNotDropFrames() thro assertThat(decoderCounters.droppedToKeyframeCount).isEqualTo(0); } + @Test + @Config(minSdk = 34) + public void render_withDisabledMediaCodecBufferDecodeOnlyFlag_rendererSkipsEarlyBuffers() + throws Exception { + MediaCodecVideoRenderer mediaCodecVideoRenderer = + new MediaCodecVideoRenderer.Builder(ApplicationProvider.getApplicationContext()) + .setCodecAdapterFactory(codecAdapterFactory) + .setMediaCodecSelector(mediaCodecSelector) + .setAllowedJoiningTimeMs(0) + .setEnableDecoderFallback(false) + .setEventHandler(new Handler(testMainLooper)) + .setEventListener(eventListener) + .setMaxDroppedFramesToNotify(1) + .experimentalSetEnableMediaCodecBufferDecodeOnlyFlag(false) + .build(); + mediaCodecVideoRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT); + mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface); + 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= */ VIDEO_H264, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), // First buffer. + oneByteSample(/* timeUs= */ 50_000L), + oneByteSample(/* timeUs= */ 80_000L), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream, + /* positionUs= */ 80_000L, + /* joining= */ true, + /* mayRenderStartOfStream= */ false, + /* startPositionUs= */ 80_000L, + /* offsetUs= */ 0, + /* mediaPeriodId= */ new MediaSource.MediaPeriodId(new Object())); + mediaCodecVideoRenderer.start(); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + + while (!mediaCodecVideoRenderer.isEnded()) { + mediaCodecVideoRenderer.render( + /* positionUs= */ 80_000L, SystemClock.elapsedRealtime() * 1000); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + } + shadowOf(testMainLooper).idle(); + + verify(eventListener).onVideoEnabled(argumentDecoderCounters.capture()); + assertThat(argumentDecoderCounters.getValue().queuedInputBufferCount).isEqualTo(3); + assertThat(argumentDecoderCounters.getValue().skippedOutputBufferCount).isEqualTo(2); + assertThat(argumentDecoderCounters.getValue().renderedOutputBufferCount).isEqualTo(1); + } + + @Test + @Config(minSdk = 34) + public void render_withEnabledSetMediaCodecBufferDecodeOnlyFlag_decoderSkipsEarlyBuffers() + throws Exception { + MediaCodecVideoRenderer mediaCodecVideoRenderer = + new MediaCodecVideoRenderer.Builder(ApplicationProvider.getApplicationContext()) + .setCodecAdapterFactory(codecAdapterFactory) + .setMediaCodecSelector(mediaCodecSelector) + .setAllowedJoiningTimeMs(0) + .setEnableDecoderFallback(false) + .setEventHandler(new Handler(testMainLooper)) + .setEventListener(eventListener) + .setMaxDroppedFramesToNotify(1) + .experimentalSetEnableMediaCodecBufferDecodeOnlyFlag(true) + .build(); + mediaCodecVideoRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT); + mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface); + ArgumentCaptor argumentDecoderCounters = + ArgumentCaptor.forClass(DecoderCounters.class); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition.Builder() + .setWindowPositionInFirstPeriodUs(0) + .setDurationUs(200_000L) + .build()); + mediaCodecVideoRenderer.setTimeline(timeline); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ VIDEO_H264, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), // First buffer. + oneByteSample(/* timeUs= */ 50_000L), + oneByteSample(/* timeUs= */ 80_000L), + oneByteSample(/* timeUs= */ 110_000L, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 140_000L), + oneByteSample(/* timeUs= */ 170_000L), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream, + /* positionUs= */ 80_000L, + /* joining= */ true, + /* mayRenderStartOfStream= */ false, + /* startPositionUs= */ 80_000L, + /* offsetUs= */ 0, + /* mediaPeriodId= */ new MediaSource.MediaPeriodId(timeline.getUidOfPeriod(0))); + mediaCodecVideoRenderer.start(); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + + for (int i = 0; i < 6; i++) { + mediaCodecVideoRenderer.render( + /* positionUs= */ 80_000L, SystemClock.elapsedRealtime() * 1000); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + } + shadowOf(testMainLooper).idle(); + + verify(eventListener).onVideoEnabled(argumentDecoderCounters.capture()); + // The pts=110_000 buffer is also queued, so it's ready for when the position advances. + assertThat(argumentDecoderCounters.getValue().queuedInputBufferCount).isEqualTo(4); + assertThat(argumentDecoderCounters.getValue().skippedOutputBufferCount).isEqualTo(0); + assertThat(argumentDecoderCounters.getValue().renderedOutputBufferCount).isEqualTo(1); + } + + // Testing behavior outlined in b/427470511 + // TODO: b/429350524 - Add a test with out-of-order frames at the end when it's supported by + // ShadowMediaCodec. + @Test + @Config(minSdk = 34) + public void render_withEnabledSetMediaCodecBufferDecodeOnlyFlag_flagNotSetOnLastBuffer() + throws Exception { + MediaCodecVideoRenderer mediaCodecVideoRenderer = + new MediaCodecVideoRenderer.Builder(ApplicationProvider.getApplicationContext()) + .setCodecAdapterFactory(codecAdapterFactory) + .setMediaCodecSelector(mediaCodecSelector) + .setAllowedJoiningTimeMs(0) + .setEnableDecoderFallback(false) + .setEventHandler(new Handler(testMainLooper)) + .setEventListener(eventListener) + .setMaxDroppedFramesToNotify(1) + .experimentalSetEnableMediaCodecBufferDecodeOnlyFlag(true) + .build(); + mediaCodecVideoRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT); + mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface); + ArgumentCaptor argumentDecoderCounters = + ArgumentCaptor.forClass(DecoderCounters.class); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition.Builder() + .setWindowPositionInFirstPeriodUs(0) + .setDurationUs(140_000L) + .build()); + mediaCodecVideoRenderer.setTimeline(timeline); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ VIDEO_H264, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), // First buffer. + oneByteSample(/* timeUs= */ 50_000L), + oneByteSample(/* timeUs= */ 80_000L), + oneByteSample(/* timeUs= */ 110_000L), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + // Start playback from just after the final frame (common case when seeking to the end of a + // piece of media). + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream, + /* positionUs= */ 110_001L, + /* joining= */ true, + /* mayRenderStartOfStream= */ false, + /* startPositionUs= */ 110_001L, + /* offsetUs= */ 0, + /* mediaPeriodId= */ new MediaSource.MediaPeriodId(timeline.getUidOfPeriod(0))); + mediaCodecVideoRenderer.start(); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + + while (!mediaCodecVideoRenderer.isEnded()) { + mediaCodecVideoRenderer.render( + /* positionUs= */ 110_001L, SystemClock.elapsedRealtime() * 1000); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + } + shadowOf(testMainLooper).idle(); + + verify(eventListener).onVideoEnabled(argumentDecoderCounters.capture()); + assertThat(argumentDecoderCounters.getValue().queuedInputBufferCount).isEqualTo(4); + // TODO: b/428687290 - Remove this comment when DECODE_ONLY buffers are counted in + // skippedOutputBufferCount. + // The 50ms and 80ms buffers are decoded and skipped afterwards because they are within 100ms of + // of the end of the stream. + assertThat(argumentDecoderCounters.getValue().skippedOutputBufferCount).isEqualTo(2); + assertThat(argumentDecoderCounters.getValue().renderedOutputBufferCount).isEqualTo(1); + } + @Test public void render_setPlaybackSpeedWithLateBufferAndOutOfOrderSamplesWithoutDependencies_rendersFramesAsExpected() @@ -3427,6 +3904,182 @@ public void render_afterSeekWithSkipKeyFrameReset_rendersFramesAsExpected() thro .onRenderedFirstFrame(eq(surface), /* renderTimeMs= */ anyLong()); } + @Test + @Config(minSdk = 34) + public void render_afterSeekWithFlushingDisabledAndDecodeOnlyFlags_playsFrameAsExpected() + throws Exception { + MediaCodecVideoRenderer mediaCodecVideoRenderer = + new MediaCodecVideoRenderer.Builder(ApplicationProvider.getApplicationContext()) + .setCodecAdapterFactory(codecAdapterFactory) + .setMediaCodecSelector(mediaCodecSelector) + .setAllowedJoiningTimeMs(0) + .setEnableDecoderFallback(false) + .setEventHandler(new Handler(testMainLooper)) + .setEventListener(eventListener) + .setMaxDroppedFramesToNotify(1) + .experimentalSetEnableMediaCodecBufferDecodeOnlyFlag(true) + .build(); + mediaCodecVideoRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT); + mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface); + ArgumentCaptor argumentDecoderCounters = + ArgumentCaptor.forClass(DecoderCounters.class); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition.Builder() + .setWindowPositionInFirstPeriodUs(0) + .setDurationUs(300_000L) + .build()); + mediaCodecVideoRenderer.setTimeline(timeline); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ VIDEO_H264, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), // First buffer. + 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); + FakeTimeline fakeTimeline = new FakeTimeline(); + mediaCodecVideoRenderer.setTimeline(fakeTimeline); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream, + /* positionUs= */ 60_000, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 60_000, + /* offsetUs= */ 0, + /* mediaPeriodId= */ new MediaSource.MediaPeriodId(fakeTimeline.getUidOfPeriod(0))); + + mediaCodecVideoRenderer.start(); + // Execute render() enough times to dequeue & render the pts=100_000 buffer. + // Empirically, this seems to be at least 2*(The number of frames to process). + for (int i = 0; i < 6; i++) { + mediaCodecVideoRenderer.render( + /* positionUs= */ 60_000, SystemClock.elapsedRealtime() * 1000); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + } + + mediaCodecVideoRenderer.handleMessage( + Renderer.MSG_SET_SCRUBBING_MODE, + new ScrubbingModeParameters.Builder().setAllowSkippingMediaCodecFlush(true).build()); + + if (mediaCodecVideoRenderer.getState() == Renderer.STATE_STARTED) { + mediaCodecVideoRenderer.stop(); + } + fakeSampleStream.seekToUs(200_000, true); + mediaCodecVideoRenderer.resetPosition(200_000, /* sampleStreamIsResetToKeyFrame= */ true); + + // Execute render() enough times to dequeue & render the pts=200_000 buffer. + for (int i = 0; i < 4; i++) { + mediaCodecVideoRenderer.render( + /* positionUs= */ 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 + @Config(minSdk = 34) + public void render_afterBackwardsSeekWithFlushingDisabledAndDecodeOnlyFlags_playsFrameAsExpected() + throws Exception { + MediaCodecVideoRenderer mediaCodecVideoRenderer = + new MediaCodecVideoRenderer.Builder(ApplicationProvider.getApplicationContext()) + .setCodecAdapterFactory(codecAdapterFactory) + .setMediaCodecSelector(mediaCodecSelector) + .setAllowedJoiningTimeMs(0) + .setEnableDecoderFallback(false) + .setEventHandler(new Handler(testMainLooper)) + .setEventListener(eventListener) + .setMaxDroppedFramesToNotify(1) + .experimentalSetEnableMediaCodecBufferDecodeOnlyFlag(true) + .build(); + mediaCodecVideoRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT); + mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface); + ArgumentCaptor argumentDecoderCounters = + ArgumentCaptor.forClass(DecoderCounters.class); + FakeTimeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition.Builder() + .setWindowPositionInFirstPeriodUs(0) + .setDurationUs(300_000L) + .build()); + mediaCodecVideoRenderer.setTimeline(timeline); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ VIDEO_H264, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(/* timeUs= */ 50_000), + oneByteSample(/* timeUs= */ 100_000), + oneByteSample(/* timeUs= */ 150_000), + oneByteSample(/* timeUs= */ 200_000), + oneByteSample(/* timeUs= */ 250_000), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + FakeTimeline fakeTimeline = new FakeTimeline(); + mediaCodecVideoRenderer.setTimeline(fakeTimeline); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream, + /* positionUs= */ 170_000, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 170_000, + /* offsetUs= */ 0, + /* mediaPeriodId= */ new MediaSource.MediaPeriodId(fakeTimeline.getUidOfPeriod(0))); + + mediaCodecVideoRenderer.start(); + // Execute render() enough times to dequeue & render the pts=200_000 buffer. + // Empirically, this seems to be at least 2*(The number of frames to process). + for (int i = 0; i < 10; i++) { + mediaCodecVideoRenderer.render( + /* positionUs= */ 170_000, SystemClock.elapsedRealtime() * 1000); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + } + + mediaCodecVideoRenderer.handleMessage( + Renderer.MSG_SET_SCRUBBING_MODE, + new ScrubbingModeParameters.Builder().setAllowSkippingMediaCodecFlush(true).build()); + + if (mediaCodecVideoRenderer.getState() == Renderer.STATE_STARTED) { + mediaCodecVideoRenderer.stop(); + } + fakeSampleStream.seekToUs(100_000, true); + mediaCodecVideoRenderer.resetPosition(100_000, /* sampleStreamIsResetToKeyFrame= */ true); + + // Execute render() enough times to dequeue & render the pts=100_000 buffer. + for (int i = 0; i < 6; i++) { + mediaCodecVideoRenderer.render( + /* positionUs= */ 100_000, SystemClock.elapsedRealtime() * 1000); + codecAdapterFactory.idleQueueingAndCallbackThreads(); + } + shadowOf(testMainLooper).idle(); + + verify(eventListener).onVideoEnabled(argumentDecoderCounters.capture()); + + assertThat(argumentDecoderCounters.getValue().skippedOutputBufferCount).isEqualTo(0); + assertThat(argumentDecoderCounters.getValue().renderedOutputBufferCount).isEqualTo(2); + } + @Test public void supportsResetPositionWithoutKeyFrameReset_withEarlierPosition_returnsFalse() throws Exception { @@ -5780,4 +6433,27 @@ public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { return outputIndex; } } + + private static DrmSessionManager getFakeDrmSessionManager() { + return new DrmSessionManager() { + @Override + public void setPlayer(Looper playbackLooper, PlayerId playerId) {} + + @Nullable + @Override + public DrmSession acquireSession( + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) { + DrmSession session = mock(DrmSession.class); + when(session.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); + when(session.getCryptoConfig()).thenReturn(new FakeCryptoConfig()); + when(session.requiresSecureDecoder(any())).thenReturn(true); + return session; + } + + @Override + public @C.CryptoType int getCryptoType(Format format) { + return C.CRYPTO_TYPE_CUSTOM_BASE; + } + }; + } } diff --git a/libraries/exoplayer_dash/build.gradle b/libraries/exoplayer_dash/build.gradle index 4c2c3c9456a..45e465ba5fe 100644 --- a/libraries/exoplayer_dash/build.gradle +++ b/libraries/exoplayer_dash/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.exoplayer.dash' diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashPreloadMediaSourceTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashPreloadMediaSourceTest.java new file mode 100644 index 00000000000..6a862e47972 --- /dev/null +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashPreloadMediaSourceTest.java @@ -0,0 +1,327 @@ +/* + * Copyright 2026 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 org.junit.Assert.assertThrows; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +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.mockito.Mockito.when; + +import android.content.Context; +import android.net.Uri; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.SystemClock; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.DefaultLoadControl; +import androidx.media3.exoplayer.LoadControl; +import androidx.media3.exoplayer.Renderer; +import androidx.media3.exoplayer.RendererCapabilities; +import androidx.media3.exoplayer.RenderersFactory; +import androidx.media3.exoplayer.analytics.PlayerId; +import androidx.media3.exoplayer.audio.AudioRendererEventListener; +import androidx.media3.exoplayer.metadata.MetadataOutput; +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; +import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.exoplayer.source.preload.PreloadMediaSource; +import androidx.media3.exoplayer.text.TextOutput; +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; +import androidx.media3.exoplayer.trackselection.TrackSelector; +import androidx.media3.exoplayer.upstream.BandwidthMeter; +import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter; +import androidx.media3.exoplayer.video.VideoRendererEventListener; +import androidx.media3.test.utils.FakeAudioRenderer; +import androidx.media3.test.utils.FakeVideoRenderer; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit test for {@link PreloadMediaSource} with DASH stream. */ +@RunWith(AndroidJUnit4.class) +public final class DashPreloadMediaSourceTest { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private static final int TARGET_PRELOAD_DURATION_US = 10000; + private static final int LARGE_TARGET_BUFFER_BYTES_FOR_PRELOAD = Integer.MAX_VALUE; + private static final int SMALL_TARGET_BUFFER_BYTES_FOR_PRELOAD = 1024; + + private MediaSource.Factory mediaSourceFactory; + private LoadControl loadControl; + private BandwidthMeter bandwidthMeter; + private RenderersFactory renderersFactory; + private MediaItem mediaItem; + @Mock private PreloadMediaSource.PreloadControl mockPreloadControl; + + @Before + public void setUp() { + mediaSourceFactory = + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); + loadControl = + mock( + LoadControl.class, + delegatesTo( + new DefaultLoadControl.Builder() + .setPlayerTargetBufferBytes( + PlayerId.PRELOAD.name, LARGE_TARGET_BUFFER_BYTES_FOR_PRELOAD) + .build())); + bandwidthMeter = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); + renderersFactory = + (handler, videoListener, audioListener, textOutput, metadataOutput) -> + new Renderer[] { + new FakeVideoRenderer( + SystemClock.DEFAULT.createHandler(handler.getLooper(), /* callback= */ null), + videoListener), + new FakeAudioRenderer( + SystemClock.DEFAULT.createHandler(handler.getLooper(), /* callback= */ null), + audioListener) + }; + mediaItem = + new MediaItem.Builder() + .setUri(Uri.parse("asset://android_asset/media/dash/multi-track/sample.mpd")) + .build(); + when(mockPreloadControl.onSourcePrepared(any())).thenReturn(true); + when(mockPreloadControl.onTracksSelected(any())).thenReturn(true); + when(mockPreloadControl.onContinueLoadingRequested(any(), anyLong())).thenReturn(true); + } + + @Test + public void preload_loadPeriodToTargetPreloadPosition() throws Exception { + AtomicBoolean preloadTerminated = new AtomicBoolean(); + when(mockPreloadControl.onContinueLoadingRequested(any(), anyLong())) + .thenAnswer( + invocation -> { + long bufferedDurationUs = invocation.getArgument(1); + if (bufferedDurationUs >= TARGET_PRELOAD_DURATION_US) { + preloadTerminated.set(true); + return false; + } + return true; + }); + TrackSelector trackSelector = + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()); + trackSelector.init(() -> {}, bandwidthMeter); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + + preloadMediaSource.preload(/* startPositionUs= */ 0L); + runMainLooperUntil(preloadTerminated::get); + + verify(loadControl).onPrepared(PlayerId.PRELOAD); + verify(mockPreloadControl).onSourcePrepared(eq(preloadMediaSource)); + verify(mockPreloadControl).onTracksSelected(eq(preloadMediaSource)); + verify(mockPreloadControl, atLeastOnce()) + .onContinueLoadingRequested(eq(preloadMediaSource), anyLong()); + verify(mockPreloadControl, never()).onUsedByPlayer(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onLoadingUnableToContinue(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onPreloadError(any(), eq(preloadMediaSource)); + } + + @Test + public void preload_stopWhenTracksSelectedByPreloadControl() throws Exception { + AtomicBoolean preloadTerminated = new AtomicBoolean(); + when(mockPreloadControl.onTracksSelected(any())) + .thenAnswer( + invocation -> { + preloadTerminated.set(true); + return false; + }); + TrackSelector trackSelector = + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()); + trackSelector.init(() -> {}, bandwidthMeter); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + + preloadMediaSource.preload(/* startPositionUs= */ 0L); + runMainLooperUntil(preloadTerminated::get); + + verify(loadControl).onPrepared(PlayerId.PRELOAD); + verify(mockPreloadControl).onSourcePrepared(eq(preloadMediaSource)); + verify(mockPreloadControl).onTracksSelected(eq(preloadMediaSource)); + verify(mockPreloadControl, never()) + .onContinueLoadingRequested(eq(preloadMediaSource), anyLong()); + verify(mockPreloadControl, never()).onUsedByPlayer(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onLoadingUnableToContinue(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onPreloadError(any(), eq(preloadMediaSource)); + } + + @Test + public void preload_stopWhenSourcePreparedByPreloadControl() throws Exception { + AtomicBoolean preloadTerminated = new AtomicBoolean(); + when(mockPreloadControl.onSourcePrepared(any())) + .thenAnswer( + invocation -> { + preloadTerminated.set(true); + return false; + }); + TrackSelector trackSelector = + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()); + trackSelector.init(() -> {}, bandwidthMeter); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + + preloadMediaSource.preload(/* startPositionUs= */ 0L); + runMainLooperUntil(preloadTerminated::get); + + verify(loadControl).onPrepared(PlayerId.PRELOAD); + verify(mockPreloadControl).onSourcePrepared(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onTracksSelected(eq(preloadMediaSource)); + verify(mockPreloadControl, never()) + .onContinueLoadingRequested(eq(preloadMediaSource), anyLong()); + verify(mockPreloadControl, never()).onUsedByPlayer(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onLoadingUnableToContinue(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onPreloadError(any(), eq(preloadMediaSource)); + } + + @Test + public void preload_loadToTheEndOfSource() throws Exception { + AtomicBoolean preloadTerminated = new AtomicBoolean(); + doAnswer( + invocation -> { + preloadTerminated.set(true); + return null; + }) + .when(mockPreloadControl) + .onLoadedToTheEndOfSource(any()); + TrackSelector trackSelector = + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()); + trackSelector.init(() -> {}, bandwidthMeter); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + + preloadMediaSource.preload(/* startPositionUs= */ 0L); + runMainLooperUntil(preloadTerminated::get); + + verify(loadControl).onPrepared(PlayerId.PRELOAD); + verify(mockPreloadControl).onSourcePrepared(eq(preloadMediaSource)); + verify(mockPreloadControl).onTracksSelected(eq(preloadMediaSource)); + // We expect onContinueLoadingRequested() to be called at least once before reaching the end of + // the source. + verify(mockPreloadControl, atLeastOnce()) + .onContinueLoadingRequested(eq(preloadMediaSource), anyLong()); + verify(mockPreloadControl).onLoadedToTheEndOfSource(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onUsedByPlayer(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onLoadingUnableToContinue(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onPreloadError(any(), eq(preloadMediaSource)); + } + + @Test + public void preload_withSmallTargetBufferBytesThreshold_onLoadingUnableToContinueCalled() { + AtomicBoolean preloadTerminated = new AtomicBoolean(); + doAnswer( + invocation -> { + preloadTerminated.set(true); + return null; + }) + .when(mockPreloadControl) + .onLoadedToTheEndOfSource(any()); + TrackSelector trackSelector = + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()); + trackSelector.init(() -> {}, bandwidthMeter); + LoadControl loadControl = + mock( + LoadControl.class, + delegatesTo( + new DefaultLoadControl.Builder() + .setPlayerTargetBufferBytes( + PlayerId.PRELOAD.name, SMALL_TARGET_BUFFER_BYTES_FOR_PRELOAD) + .build())); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + + preloadMediaSource.preload(/* startPositionUs= */ 0L); + + assertThrows(TimeoutException.class, () -> runMainLooperUntil(preloadTerminated::get)); + verify(loadControl).onPrepared(PlayerId.PRELOAD); + verify(mockPreloadControl).onSourcePrepared(eq(preloadMediaSource)); + verify(mockPreloadControl).onTracksSelected(eq(preloadMediaSource)); + verify(mockPreloadControl, atLeastOnce()) + .onContinueLoadingRequested(eq(preloadMediaSource), anyLong()); + verify(mockPreloadControl, never()).onUsedByPlayer(eq(preloadMediaSource)); + verify(mockPreloadControl, atLeastOnce()).onLoadingUnableToContinue(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onPreloadError(any(), eq(preloadMediaSource)); + } + + private static RendererCapabilities[] getRendererCapabilities(RenderersFactory renderersFactory) { + Renderer[] renderers = + renderersFactory.createRenderers( + Util.createHandlerForCurrentLooper(), + mock(VideoRendererEventListener.class), + mock(AudioRendererEventListener.class), + mock(TextOutput.class), + mock(MetadataOutput.class)); + RendererCapabilities[] rendererCapabilities = new RendererCapabilities[renderers.length]; + for (int i = 0; i < renderers.length; i++) { + rendererCapabilities[i] = renderers[i].getCapabilities(); + } + return rendererCapabilities; + } +} diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java index 0a6b026ce80..e661d7b3464 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java @@ -1095,6 +1095,58 @@ public void seekToEnd_afterLoadingFinished_doesNotLoadAgain() throws Exception { /* dumpFile= */ "playbackdumps/dash/scrubbing-skipKeyFrameReset-sameSyncNextGoP.dump"); } + @Test + public void play_multiSegmentWithEmsgVersion0_outputsExpectedSamples() throws Exception { + Context applicationContext = ApplicationProvider.getApplicationContext(); + FakeClock clock = new FakeClock(/* isAutoAdvancing= */ true); + CapturingRenderersFactory capturingRenderersFactory = + new CapturingRenderersFactory(applicationContext, clock); + ExoPlayer player = + new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) + .setClock(clock) + .build(); + Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); + player.setVideoSurface(surface); + PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); + + player.setMediaItem( + MediaItem.fromUri("asset:///media/dash/multi-segment-with-emsg0/manifest.mpd")); + player.prepare(); + player.play(); + advance(player).untilState(Player.STATE_ENDED); + player.release(); + surface.release(); + + DumpFileAsserts.assertOutput( + applicationContext, playbackOutput, "playbackdumps/dash/multi-segment-with-emsg0.dump"); + } + + @Test + public void play_multiSegmentWithEmsgVersion1_outputsExpectedSamples() throws Exception { + Context applicationContext = ApplicationProvider.getApplicationContext(); + FakeClock clock = new FakeClock(/* isAutoAdvancing= */ true); + CapturingRenderersFactory capturingRenderersFactory = + new CapturingRenderersFactory(applicationContext, clock); + ExoPlayer player = + new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) + .setClock(clock) + .build(); + Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1)); + player.setVideoSurface(surface); + PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); + + player.setMediaItem( + MediaItem.fromUri("asset:///media/dash/multi-segment-with-emsg1/manifest.mpd")); + player.prepare(); + player.play(); + advance(player).untilState(Player.STATE_ENDED); + player.release(); + surface.release(); + + DumpFileAsserts.assertOutput( + applicationContext, playbackOutput, "playbackdumps/dash/multi-segment-with-emsg1.dump"); + } + private static ImmutableList mockWebServerResourcesFromAssetsDirectory(String assetDirectory) throws IOException { Context context = ApplicationProvider.getApplicationContext(); diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java index c4b423881f8..c9acc4a281f 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java @@ -39,6 +39,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import java.io.IOException; import java.io.StringReader; import java.nio.charset.StandardCharsets; @@ -92,6 +93,8 @@ public class DashManifestParserTest { private static final String SAMPLE_MPD_DASHIF_LICENSE_URL = "media/mpd/sample_mpd_dashif_license_url"; private static final String SAMPLE_MPD_DOLBY_VISION = "media/mpd/sample_mpd_dolby"; + private static final String SAMPLE_MPD_SUPPLEMENTAL_CODECS = + "media/mpd/sample_mpd_supplemental_codecs"; private static final String NEXT_TAG_NAME = "Next"; private static final String NEXT_TAG = "<" + NEXT_TAG_NAME + "/>"; @@ -1051,6 +1054,24 @@ public void contentProtection_withDashIfLicenseUrl() throws IOException { assertThat(schemeData1.licenseServerUrl).isEqualTo("https://testserver2.test/AcquireLicense"); } + @Test + public void supplementalCodecs() throws IOException { + DashManifestParser parser = new DashManifestParser(); + + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_SUPPLEMENTAL_CODECS)); + + assertThat(manifest.getPeriodCount()).isEqualTo(1); + Period period = manifest.getPeriod(0); + Representation representation = + Iterables.getOnlyElement(Iterables.getOnlyElement(period.adaptationSets).representations); + assertThat(representation.format.sampleMimeType).isEqualTo(MimeTypes.VIDEO_DOLBY_VISION); + assertThat(representation.format.codecs).isEqualTo("dvh1.08.03"); + } + private static List buildCea608AccessibilityDescriptors(String value) { return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null)); } diff --git a/libraries/exoplayer_hls/build.gradle b/libraries/exoplayer_hls/build.gradle index 92145c820ff..669245b714a 100644 --- a/libraries/exoplayer_hls/build.gradle +++ b/libraries/exoplayer_hls/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.exoplayer.hls' 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 9685492b079..d527f52c27b 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 @@ -381,7 +381,11 @@ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParame return CHUNK_PUBLICATION_STATE_PRELOAD; } Uri newUri = Uri.parse(UriUtil.resolve(mediaPlaylist.baseUri, newPart.url)); - return Objects.equals(newUri, mediaChunk.dataSpec.uri) + Uri mediaChunkUri = mediaChunk.dataSpec.uri; + if (cmcdConfiguration != null) { + mediaChunkUri = CmcdData.removeFromUri(mediaChunkUri); + } + return Objects.equals(newUri, mediaChunkUri) ? CHUNK_PUBLICATION_STATE_PUBLISHED : CHUNK_PUBLICATION_STATE_REMOVED; } @@ -458,6 +462,11 @@ public void getNextChunk( // Select the track. MediaChunkIterator[] mediaChunkIterators = createMediaChunkIterators(previous, loadPositionUs); + boolean oldTrackExcluded = + oldTrackIndex != C.INDEX_UNSET + // Check this before updating the track because of the realtime dependency of the logic. + && trackSelection.isTrackExcluded( + trackSelection.indexOf(oldTrackIndex), SystemClock.elapsedRealtime()); trackSelection.updateSelectedTrack( playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, mediaChunkIterators); int selectedTrackIndex = trackSelection.getSelectedIndexInTrackGroup(); @@ -488,6 +497,7 @@ public void getNextChunk( boolean shouldForceKeepCurrentTrackSelection = shouldForceKeepCurrentTrackSelection( switchingTrack, + oldTrackExcluded, playlist, chunkMediaSequence, partIndex, @@ -945,22 +955,9 @@ public FallbackOptions createFallbackOptions(Uri playlistUrl) { numberOfExcludedLocations++; } } - // The numberOfTracks is the number of tracks in the selection, which should also be equal to - // the number of redundant groups of this chunk source. - int numberOfTracks = trackSelection.length(); - int numberOfExcludedTracks = 0; - ExoTrackSelection trackSelection = getTrackSelection(); - for (int i = 0; i < redundantGroups.length; i++) { - if (trackSelection.isTrackExcluded(i, nowMs) - || playlistTracker.isExcluded(redundantGroups[i], nowMs)) { - // The numberOfExcludedTracks is the number of redundant groups in this chunk source - // whose corresponding track is excluded from the track selection, or the urls from all - // locations are excluded. - numberOfExcludedTracks++; - } - } + int numberOfTracks = getTrackSelection().length(); return new FallbackOptions( - numberOfLocations, numberOfExcludedLocations, numberOfTracks, numberOfExcludedTracks); + numberOfLocations, numberOfExcludedLocations, numberOfTracks, getExcludedTrackCount(nowMs)); } /** @@ -977,24 +974,37 @@ public FallbackOptions createFallbackOptions(Chunk chunk) { // Otherwise, this chunk is of another type eg. InitializationChunk that may be specific to a // track, then we do the track fallback. long nowMs = SystemClock.elapsedRealtime(); + int numberOfTracks = getTrackSelection().length(); + return new FallbackOptions( + /* numberOfLocations= */ 1, + /* numberOfExcludedLocations= */ 0, + numberOfTracks, + getExcludedTrackCount(nowMs)); + } + + // Private methods. + + /** + * Returns the number of tracks in the current {@link ExoTrackSelection} that are currently + * excluded. + * + * @param nowMs The current time in milliseconds, as measured by {@link + * SystemClock#elapsedRealtime()}. + */ + private int getExcludedTrackCount(long nowMs) { ExoTrackSelection trackSelection = getTrackSelection(); int numberOfTracks = trackSelection.length(); int numberOfExcludedTracks = 0; for (int i = 0; i < numberOfTracks; i++) { + int trackGroupIndex = trackSelection.getIndexInTrackGroup(i); if (trackSelection.isTrackExcluded(i, nowMs) - || playlistTracker.isExcluded(redundantGroups[i], nowMs)) { + || playlistTracker.isExcluded(redundantGroups[trackGroupIndex], nowMs)) { numberOfExcludedTracks++; } } - return new FallbackOptions( - /* numberOfLocations= */ 1, - /* numberOfExcludedLocations= */ 0, - numberOfTracks, - numberOfExcludedTracks); + return numberOfExcludedTracks; } - // Private methods. - /** * Returns the media sequence number and part index to load next in the {@code mediaPlaylist}. * @@ -1081,6 +1091,7 @@ private Pair getNextMediaSequenceAndPartIndex( private static boolean shouldForceKeepCurrentTrackSelection( boolean switchingTrack, + boolean oldTrackExcluded, HlsMediaPlaylist playlist, long mediaSequence, int part, @@ -1091,6 +1102,10 @@ private static boolean shouldForceKeepCurrentTrackSelection( // We are already keeping the current selection. return false; } + if (oldTrackExcluded) { + // The old track can't be kept because it was excluded. + return false; + } if (previousChunk == null) { // This is the first chunk, we can select any track. return false; 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 f90a78e4402..d4e27c8597f 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 @@ -40,6 +40,7 @@ import static com.google.common.base.Preconditions.checkState; import static java.lang.Math.abs; import static java.lang.Math.max; +import static java.lang.Math.min; import android.content.Context; import android.net.Uri; @@ -83,11 +84,13 @@ import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.Loader; import androidx.media3.exoplayer.upstream.ParsingLoadable; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -1367,6 +1370,7 @@ private AdPlaybackState mapInterstitialsForLive( } adPlaybackState = insertOrUpdateInterstitialInAdGroup( + mediaPlaylist, mediaItem, interstitial, adPlaybackState, @@ -1375,6 +1379,45 @@ private AdPlaybackState mapInterstitialsForLive( contentMediaSourceAdDataHolder.addInsertedInterstitialId(adsId, interstitial.id); } } + return maybeResolvePendingSnapInResolutions(adPlaybackState, mediaPlaylist); + } + + private AdPlaybackState maybeResolvePendingSnapInResolutions( + AdPlaybackState adPlaybackState, HlsMediaPlaylist mediaPlaylist) { + Object adsId = checkNotNull(adPlaybackState.adsId); + long endOfPlaylistUs = mediaPlaylist.startTimeUs + mediaPlaylist.durationUs; + List pendingSnapInResolutions = + contentMediaSourceAdDataHolder.getPendingSnapInResolutions(adsId); + int resolvedIndex = C.INDEX_UNSET; + for (int i = 0; i < pendingSnapInResolutions.size(); i++) { + PendingSnapInResolution pendingSnapInResolution = pendingSnapInResolutions.get(i); + if (pendingSnapInResolution.resumeTimeUs > endOfPlaylistUs) { + break; + } + Interstitial interstitial = pendingSnapInResolution.interstitial; + // Resolve the resume offset according to the snap position of the segement start + long resolvedResumeOffsetUs = resolveInterstitialResumeOffsetUs(interstitial, mediaPlaylist); + AdGroup adGroup = adPlaybackState.getAdGroup(pendingSnapInResolution.adGroupIndex); + long interstitialDurationUs = + resolveInterstitialDurationUs(interstitial, /* defaultDurationUs= */ C.TIME_UNSET); + // The content resume offset that was used before resolving the actuals offset. + long oldResumeOffsetIncrementUs = + interstitial.resumeOffsetUs != C.TIME_UNSET + ? interstitial.resumeOffsetUs + : (interstitialDurationUs != C.TIME_UNSET ? interstitialDurationUs : 0L); + // Recalculate the resume offset of the group in case the interstitial offset has changed. + long correctedAdGroupContentResumeOffsetUs = + adGroup.contentResumeOffsetUs - oldResumeOffsetIncrementUs + resolvedResumeOffsetUs; + adPlaybackState = + adPlaybackState.withContentResumeOffsetUs( + pendingSnapInResolution.adGroupIndex, correctedAdGroupContentResumeOffsetUs); + resolvedIndex++; + } + if (resolvedIndex != C.INDEX_UNSET) { + // Remove resolved interstitials from the list of pending resolutions. + contentMediaSourceAdDataHolder.removePendingSnapInResolutionUntilIndexInclusive( + adsId, resolvedIndex); + } return adPlaybackState; } @@ -1459,6 +1502,7 @@ private AdPlaybackState mapInterstitialsForVod( } adPlaybackState = insertOrUpdateInterstitialInAdGroup( + mediaPlaylist, mediaItem, interstitial, adPlaybackState, @@ -1472,6 +1516,7 @@ private AdPlaybackState mapInterstitialsForVod( } private AdPlaybackState insertOrUpdateInterstitialInAdGroup( + HlsMediaPlaylist mediaPlaylist, MediaItem mediaItem, Interstitial interstitial, AdPlaybackState adPlaybackState, @@ -1502,6 +1547,17 @@ private AdPlaybackState insertOrUpdateInterstitialInAdGroup( interstitial.resumeOffsetUs != C.TIME_UNSET ? interstitial.resumeOffsetUs : (interstitialDurationUs != C.TIME_UNSET ? interstitialDurationUs : 0L); + if (interstitial.snapTypes.contains(SNAP_TYPE_IN)) { + long resumeTimeUs = interstitial.startDateUnixUs + resumeOffsetIncrementUs; + if (resumeTimeUs < mediaPlaylist.startTimeUs + mediaPlaylist.durationUs) { + resumeOffsetIncrementUs = resolveInterstitialResumeOffsetUs(interstitial, mediaPlaylist); + } else { + // The segment at which to resume is not yet in the playlist. Deferring offset calculation. + contentMediaSourceAdDataHolder.putPendingSnapInResolution( + checkNotNull(adPlaybackState.adsId), + new PendingSnapInResolution(resumeTimeUs, adGroupIndex, interstitial)); + } + } long resumeOffsetUs = adGroup.contentResumeOffsetUs + resumeOffsetIncrementUs; adPlaybackState = adPlaybackState @@ -1585,16 +1641,27 @@ private static long resolveInterstitialStartTimeUs( return mediaPlaylist.startTimeUs + mediaPlaylist.durationUs; } else if (interstitial.snapTypes.contains(SNAP_TYPE_OUT)) { return getClosestSegmentBoundaryUs(interstitial.startDateUnixUs, mediaPlaylist); - } else if (interstitial.snapTypes.contains(SNAP_TYPE_IN)) { + } else { + return interstitial.startDateUnixUs; + } + } + + private static long resolveInterstitialResumeOffsetUs( + Interstitial interstitial, HlsMediaPlaylist mediaPlaylist) { + if (interstitial.snapTypes.contains(SNAP_TYPE_IN)) { long resumeOffsetUs = interstitial.resumeOffsetUs != C.TIME_UNSET ? interstitial.resumeOffsetUs : resolveInterstitialDurationUs(interstitial, /* defaultDurationUs= */ 0L); - return getClosestSegmentBoundaryUs( - interstitial.startDateUnixUs + resumeOffsetUs, mediaPlaylist) - - resumeOffsetUs; + long startTimeUs = + interstitial.snapTypes.contains(SNAP_TYPE_OUT) + ? getClosestSegmentBoundaryUs(interstitial.startDateUnixUs, mediaPlaylist) + : interstitial.startDateUnixUs; + return getClosestSegmentBoundaryUs(startTimeUs + resumeOffsetUs, mediaPlaylist) - startTimeUs; } else { - return interstitial.startDateUnixUs; + return interstitial.resumeOffsetUs != C.TIME_UNSET + ? interstitial.resumeOffsetUs + : resolveInterstitialDurationUs(interstitial, C.TIME_UNSET); } } @@ -1779,11 +1846,13 @@ private void markAdAsPlayedAndNotifyListeners( } } - private static final class ContentMediaSourceAdDataHolder { + @VisibleForTesting + /* package */ static final class ContentMediaSourceAdDataHolder { private final Map activeEventListeners; private final Map activeAdPlaybackStates; private final Map> insertedInterstitialIds; private final Map> unresolvedAssetLists; + private final Map> pendingSnapInResolutions; private final Set contentSourceAwaitingFirstAdToStart; private final Set unsupportedAdsIds; @@ -1793,6 +1862,7 @@ public ContentMediaSourceAdDataHolder() { activeAdPlaybackStates = new HashMap<>(); insertedInterstitialIds = new HashMap<>(); unresolvedAssetLists = new HashMap<>(); + pendingSnapInResolutions = new HashMap<>(); contentSourceAwaitingFirstAdToStart = new HashSet<>(); unsupportedAdsIds = new HashSet<>(); } @@ -1899,6 +1969,21 @@ public int getUnresolvedAssetListCount(Object adsId) { return assetListDataTreeMap != null ? assetListDataTreeMap.size() : 0; } + public void putPendingSnapInResolution( + Object adsId, PendingSnapInResolution pendingSnapInResolution) { + List pendingResolutions = this.pendingSnapInResolutions.get(adsId); + if (pendingResolutions == null) { + pendingResolutions = new ArrayList<>(); + pendingSnapInResolutions.put(adsId, pendingResolutions); + } + pendingResolutions.add(pendingSnapInResolution); + } + + public List getPendingSnapInResolutions(Object adsId) { + List pendingResolutions = this.pendingSnapInResolutions.get(adsId); + return pendingResolutions == null ? Collections.emptyList() : pendingResolutions; + } + /** * Stops calling the {@link EventListener} and clears all ad data for the given ads ID. * @@ -1912,8 +1997,19 @@ public AdPlaybackState stopContentSource(Object adsId) { unresolvedAssetLists.remove(adsId); unsupportedAdsIds.remove(adsId); contentSourceAwaitingFirstAdToStart.remove(adsId); + pendingSnapInResolutions.remove(adsId); return activeAdPlaybackStates.remove(adsId); } + + @VisibleForTesting + /* package */ void removePendingSnapInResolutionUntilIndexInclusive( + Object adsId, int resolvedIndex) { + Preconditions.checkArgument(resolvedIndex >= 0); + List pendingResolutions = + checkNotNull(getPendingSnapInResolutions(adsId)); + resolvedIndex = min(resolvedIndex, pendingResolutions.size() - 1); + pendingResolutions.subList(0, resolvedIndex + 1).clear(); + } } private class LoaderCallback @@ -2144,4 +2240,19 @@ public int hashCode() { return result; } } + + @VisibleForTesting + /* package */ static class PendingSnapInResolution { + + private final long resumeTimeUs; + private final int adGroupIndex; + private final Interstitial interstitial; + + /* package */ PendingSnapInResolution( + long resumeTimeUs, int adGroupIndex, Interstitial interstitial) { + this.resumeTimeUs = resumeTimeUs; + this.adGroupIndex = adGroupIndex; + this.interstitial = interstitial; + } + } } diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java index 529d4f73d38..43f0fa3e6ab 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java @@ -99,8 +99,9 @@ private @MonotonicNonNull TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; - // Maps sample stream wrappers to variant/rendition index by matching array positions. - private int[][] manifestUrlIndicesPerWrapper; + // Maps sample stream wrappers to variant/rendition redundant group index by matching array + // positions. + private int[][] redundantGroupIndicesPerWrapper; private int audioVideoSampleStreamWrapperCount; private SequenceableLoader compositeSequenceableLoader; private long endPositionUs; @@ -175,7 +176,7 @@ public HlsMediaPeriod( timestampAdjusterProvider = new TimestampAdjusterProvider(); sampleStreamWrappers = new HlsSampleStreamWrapper[0]; enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0]; - manifestUrlIndicesPerWrapper = new int[0][]; + redundantGroupIndicesPerWrapper = new int[0][]; endPositionUs = C.TIME_END_OF_SOURCE; } @@ -217,21 +218,25 @@ public List getStreamKeys(List trackSelections) { checkNotNull(playlistTracker.getMultivariantPlaylist()); List variantRedundantGroups = checkNotNull(playlistTracker.getRedundantGroups(HlsRedundantGroup.VARIANT)); + List audioRenditionRedundantGroups = + checkNotNull(playlistTracker.getRedundantGroups(HlsRedundantGroup.AUDIO_RENDITION)); + List subtitleRenditionRedundantGroups = + checkNotNull(playlistTracker.getRedundantGroups(HlsRedundantGroup.SUBTITLE_RENDITION)); boolean hasVariants = !multivariantPlaylist.variants.isEmpty(); int audioWrapperOffset = hasVariants ? 1 : 0; TrackGroupArray mainWrapperTrackGroups; - int mainWrapperPrimaryGroupIndex; - int[] mainWrapperVariantIndices; + int mainWrapperPrimaryTrackGroupIndex; + int[] mainWrapperRedundantGroupIndices; if (hasVariants) { HlsSampleStreamWrapper mainWrapper = sampleStreamWrappers[0]; - mainWrapperVariantIndices = manifestUrlIndicesPerWrapper[0]; + mainWrapperRedundantGroupIndices = redundantGroupIndicesPerWrapper[0]; mainWrapperTrackGroups = mainWrapper.getTrackGroups(); - mainWrapperPrimaryGroupIndex = mainWrapper.getPrimaryTrackGroupIndex(); + mainWrapperPrimaryTrackGroupIndex = mainWrapper.getPrimaryTrackGroupIndex(); } else { - mainWrapperVariantIndices = new int[0]; + mainWrapperRedundantGroupIndices = new int[0]; mainWrapperTrackGroups = TrackGroupArray.EMPTY; - mainWrapperPrimaryGroupIndex = 0; + mainWrapperPrimaryTrackGroupIndex = 0; } List streamKeys = new ArrayList<>(); @@ -241,13 +246,21 @@ public List getStreamKeys(List trackSelections) { TrackGroup trackSelectionGroup = trackSelection.getTrackGroup(); int mainWrapperTrackGroupIndex = mainWrapperTrackGroups.indexOf(trackSelectionGroup); if (mainWrapperTrackGroupIndex != C.INDEX_UNSET) { - if (mainWrapperTrackGroupIndex == mainWrapperPrimaryGroupIndex) { + if (mainWrapperTrackGroupIndex == mainWrapperPrimaryTrackGroupIndex) { // Primary group in main wrapper. hasPrimaryTrackGroupSelection = true; for (int i = 0; i < trackSelection.length(); i++) { - int variantIndex = mainWrapperVariantIndices[trackSelection.getIndexInTrackGroup(i)]; - streamKeys.add( - new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, variantIndex)); + int variantRedundantGroupIndex = + mainWrapperRedundantGroupIndices[trackSelection.getIndexInTrackGroup(i)]; + HlsRedundantGroup variantRedundantGroup = + variantRedundantGroups.get(variantRedundantGroupIndex); + for (int variantUrlIndexInMultivariantPlaylist : + variantRedundantGroup.getIndicesInMultivariantPlaylist()) { + streamKeys.add( + new StreamKey( + HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, + variantUrlIndexInMultivariantPlaylist)); + } } } else { // Embedded group in main wrapper. @@ -263,11 +276,22 @@ public List getStreamKeys(List trackSelections) { wrapperTrackGroups.get(selectedTrackGroupIndex).type == C.TRACK_TYPE_AUDIO ? HlsMultivariantPlaylist.GROUP_INDEX_AUDIO : HlsMultivariantPlaylist.GROUP_INDEX_SUBTITLE; - int[] selectedWrapperUrlIndices = manifestUrlIndicesPerWrapper[i]; + List renditionRedundantGroups = + (groupIndexType == HlsMultivariantPlaylist.GROUP_INDEX_AUDIO) + ? audioRenditionRedundantGroups + : subtitleRenditionRedundantGroups; + int[] selectedWrapperRedundantGroupIndices = redundantGroupIndicesPerWrapper[i]; for (int trackIndex = 0; trackIndex < trackSelection.length(); trackIndex++) { - int renditionIndex = - selectedWrapperUrlIndices[trackSelection.getIndexInTrackGroup(trackIndex)]; - streamKeys.add(new StreamKey(groupIndexType, renditionIndex)); + int renditionRedundantGroupIndex = + selectedWrapperRedundantGroupIndices[ + trackSelection.getIndexInTrackGroup(trackIndex)]; + HlsRedundantGroup renditionRedundantGroup = + renditionRedundantGroups.get(renditionRedundantGroupIndex); + for (int renditionUrlIndexInMultivariantPlaylist : + renditionRedundantGroup.getIndicesInMultivariantPlaylist()) { + streamKeys.add( + new StreamKey(groupIndexType, renditionUrlIndexInMultivariantPlaylist)); + } } break; } @@ -277,19 +301,26 @@ public List getStreamKeys(List trackSelections) { if (needsPrimaryTrackGroupSelection && !hasPrimaryTrackGroupSelection) { // A track selection includes a variant-embedded track, but no variant is added yet. We use // the valid variant with the lowest bitrate to reduce overhead. - int lowestBitrateIndex = mainWrapperVariantIndices[0]; + int lowestBitrateRedundantGroupIndex = mainWrapperRedundantGroupIndices[0]; int lowestBitrate = - variantRedundantGroups.get(mainWrapperVariantIndices[0]).groupKey.format.bitrate; - for (int i = 1; i < mainWrapperVariantIndices.length; i++) { + variantRedundantGroups.get(mainWrapperRedundantGroupIndices[0]).groupKey.format.bitrate; + for (int i = 1; i < mainWrapperRedundantGroupIndices.length; i++) { int variantBitrate = - variantRedundantGroups.get(mainWrapperVariantIndices[i]).groupKey.format.bitrate; + variantRedundantGroups.get(mainWrapperRedundantGroupIndices[i]).groupKey.format.bitrate; if (variantBitrate < lowestBitrate) { lowestBitrate = variantBitrate; - lowestBitrateIndex = mainWrapperVariantIndices[i]; + lowestBitrateRedundantGroupIndex = mainWrapperRedundantGroupIndices[i]; } } - streamKeys.add( - new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, lowestBitrateIndex)); + HlsRedundantGroup variantRedundantGroupWithLowestBitrate = + variantRedundantGroups.get(lowestBitrateRedundantGroupIndex); + for (int variantUrlIndexInMultivariantPlaylist : + variantRedundantGroupWithLowestBitrate.getIndicesInMultivariantPlaylist()) { + streamKeys.add( + new StreamKey( + HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, + variantUrlIndexInMultivariantPlaylist)); + } } return streamKeys; } @@ -520,7 +551,7 @@ private void buildAndPrepareSampleStreamWrappers(long positionUs) { pendingPrepareCount = 0; ArrayList sampleStreamWrappers = new ArrayList<>(); - ArrayList manifestUrlIndicesPerWrapper = new ArrayList<>(); + ArrayList redundantGroupIndicesPerWrapper = new ArrayList<>(); if (hasVariants) { buildAndPrepareMainSampleStreamWrapper( @@ -530,7 +561,7 @@ private void buildAndPrepareSampleStreamWrappers(long positionUs) { multivariantPlaylist.muxedCaptionFormats, positionUs, sampleStreamWrappers, - manifestUrlIndicesPerWrapper, + redundantGroupIndicesPerWrapper, overridingDrmInitData); } @@ -540,7 +571,7 @@ private void buildAndPrepareSampleStreamWrappers(long positionUs) { positionUs, audioRedundantGroups, sampleStreamWrappers, - manifestUrlIndicesPerWrapper, + redundantGroupIndicesPerWrapper, overridingDrmInitData); audioVideoSampleStreamWrapperCount = sampleStreamWrappers.size(); @@ -549,11 +580,11 @@ private void buildAndPrepareSampleStreamWrappers(long positionUs) { positionUs, subtitleRedundantGroups, sampleStreamWrappers, - manifestUrlIndicesPerWrapper, + redundantGroupIndicesPerWrapper, overridingDrmInitData); this.sampleStreamWrappers = sampleStreamWrappers.toArray(new HlsSampleStreamWrapper[0]); - this.manifestUrlIndicesPerWrapper = manifestUrlIndicesPerWrapper.toArray(new int[0][]); + this.redundantGroupIndicesPerWrapper = redundantGroupIndicesPerWrapper.toArray(new int[0][]); pendingPrepareCount = this.sampleStreamWrappers.length; // Set primary timestamp source and trigger preparation (if not already prepared) for (int i = 0; i < audioVideoSampleStreamWrapperCount; i++) { @@ -598,7 +629,8 @@ private void buildAndPrepareSampleStreamWrappers(long positionUs) { * @param positionUs If preparation requires any chunk downloads, the position in microseconds at * which downloading should start. Ignored otherwise. * @param sampleStreamWrappers List to which the built main sample stream wrapper should be added. - * @param manifestUrlIndicesPerWrapper List to which the selected variant indices should be added. + * @param redundantGroupIndicesPerWrapper List to which the selected variant redundant group + * indices should be added. * @param overridingDrmInitData Overriding {@link DrmInitData}, keyed by protection scheme type * (i.e. {@link DrmInitData#schemeType}). */ @@ -609,7 +641,7 @@ private void buildAndPrepareMainSampleStreamWrapper( @Nullable List muxedCaptionFormats, long positionUs, List sampleStreamWrappers, - List manifestUrlIndicesPerWrapper, + List redundantGroupIndicesPerWrapper, Map overridingDrmInitData) { int[] variantTypes = new int[variantRedundantGroups.size()]; int videoVariantCount = 0; @@ -643,7 +675,7 @@ private void buildAndPrepareMainSampleStreamWrapper( } HlsRedundantGroup[] selectedRedundantGroups = new HlsRedundantGroup[selectedVariantsCount]; Format[] selectedRedundantGroupFormats = new Format[selectedVariantsCount]; - int[] selectedVariantIndices = new int[selectedVariantsCount]; + int[] selectedVariantRedundantGroupIndices = new int[selectedVariantsCount]; int outIndex = 0; for (int i = 0; i < variantRedundantGroups.size(); i++) { if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO) @@ -651,7 +683,7 @@ private void buildAndPrepareMainSampleStreamWrapper( HlsRedundantGroup variantRedundantGroup = variantRedundantGroups.get(i); selectedRedundantGroups[outIndex] = variantRedundantGroup; selectedRedundantGroupFormats[outIndex] = variantRedundantGroup.groupKey.format; - selectedVariantIndices[outIndex++] = i; + selectedVariantRedundantGroupIndices[outIndex++] = i; } } String codecs = selectedRedundantGroupFormats[0].codecs; @@ -678,7 +710,7 @@ private void buildAndPrepareMainSampleStreamWrapper( overridingDrmInitData, positionUs); sampleStreamWrappers.add(sampleStreamWrapper); - manifestUrlIndicesPerWrapper.add(selectedVariantIndices); + redundantGroupIndicesPerWrapper.add(selectedVariantRedundantGroupIndices); if (allowChunklessPreparation && codecsStringAllowsChunklessPreparation) { List muxedTrackGroups = new ArrayList<>(); if (numberOfVideoCodecs > 0) { @@ -748,7 +780,7 @@ private void buildAndPrepareAudioSampleStreamWrappers( long positionUs, List audioRedundantGroups, List sampleStreamWrappers, - List manifestUrlsIndicesPerWrapper, + List redundantGroupIndicesPerWrapper, Map overridingDrmInitData) { ArrayList scratchRedundantGroups = new ArrayList<>(/* initialCapacity= */ audioRedundantGroups.size()); @@ -796,7 +828,7 @@ private void buildAndPrepareAudioSampleStreamWrappers( /* muxedCaptionFormats= */ Collections.emptyList(), overridingDrmInitData, positionUs); - manifestUrlsIndicesPerWrapper.add(Ints.toArray(scratchIndicesList)); + redundantGroupIndicesPerWrapper.add(Ints.toArray(scratchIndicesList)); sampleStreamWrappers.add(sampleStreamWrapper); if (allowChunklessPreparation && codecStringsAllowChunklessPreparation) { @@ -823,7 +855,7 @@ private void buildAndPrepareSubtitleSampleStreamWrappers( long positionUs, List subtitleRedundantGroups, List sampleStreamWrappers, - List manifestUrlsIndicesPerWrapper, + List redundantGroupIndicesPerWrapper, Map overridingDrmInitData) { ArrayList scratchRedundantGroups = new ArrayList<>(/* initialCapacity= */ subtitleRedundantGroups.size()); @@ -869,7 +901,7 @@ private void buildAndPrepareSubtitleSampleStreamWrappers( /* muxedCaptionFormats= */ ImmutableList.of(), overridingDrmInitData, positionUs); - manifestUrlsIndicesPerWrapper.add(Ints.toArray(scratchIndicesList)); + redundantGroupIndicesPerWrapper.add(Ints.toArray(scratchIndicesList)); sampleStreamWrappers.add(sampleStreamWrapper); Format[] outputTextFormats = new Format[originalSubtitleFormats.length]; diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java index 7b199ff59dc..001b93bbc1b 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMediaPlaylist.java @@ -1423,6 +1423,12 @@ public int hashCode() { */ public final ImmutableList interstitials; + /** + * The last seen initialization segment defined by {@code #EXT-X-MAP}, or {@code null} if no + * {@code #EXT-X-MAP} has been declared for the corresponding stream. + */ + @Nullable public final Segment lastSeenInitSegment; + /** * Constructs an instance. * @@ -1447,6 +1453,7 @@ public int hashCode() { * @param serverControl See {@link #serverControl} * @param renditionReports See {@link #renditionReports}. * @param interstitials See {@link #interstitials}. + * @param lastSeenInitSegment See {@link #lastSeenInitSegment}. */ public HlsMediaPlaylist( @PlaylistType int playlistType, @@ -1469,7 +1476,8 @@ public HlsMediaPlaylist( List trailingParts, ServerControl serverControl, Map renditionReports, - List interstitials) { + List interstitials, + @Nullable Segment lastSeenInitSegment) { super(baseUri, tags, hasIndependentSegments); this.playlistType = playlistType; this.startTimeUs = startTimeUs; @@ -1487,6 +1495,7 @@ public HlsMediaPlaylist( this.trailingParts = ImmutableList.copyOf(trailingParts); this.renditionReports = ImmutableMap.copyOf(renditionReports); this.interstitials = ImmutableList.copyOf(interstitials); + this.lastSeenInitSegment = lastSeenInitSegment; if (!trailingParts.isEmpty()) { Part lastPart = Iterables.getLast(trailingParts); durationUs = lastPart.relativeStartTimeUs + lastPart.durationUs; @@ -1574,7 +1583,8 @@ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { trailingParts, serverControl, renditionReports, - interstitials); + interstitials, + lastSeenInitSegment); } /** @@ -1606,6 +1616,7 @@ public HlsMediaPlaylist copyWithEndTag() { trailingParts, serverControl, renditionReports, - interstitials); + interstitials, + lastSeenInitSegment); } } diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMultivariantPlaylist.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMultivariantPlaylist.java index ba057574a3d..deb726ca052 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMultivariantPlaylist.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsMultivariantPlaylist.java @@ -341,7 +341,7 @@ private static List copyStreams( // trackIndex so as to avoid breaking stream keys that have been persisted for offline. All // duplicates should be copied if the first variant is copied, or discarded otherwise. // 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to - // avoid breaking stream keys that have been persisted for offline. All renitions with null + // avoid breaking stream keys that have been persisted for offline. All renditions with null // URLs should be copied. They may become unreachable if all variants that reference them are // removed, but this is OK. // 3. Renditions with URLs matching copied variants should always themselves be copied, even if diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java index ad68c2e9e64..8a6438762ed 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java @@ -827,7 +827,9 @@ private static HlsMediaPlaylist parseMediaPlaylist( long partTargetDurationUs = C.TIME_UNSET; boolean hasIndependentSegmentsTag = multivariantPlaylist.hasIndependentSegments; boolean hasEndTag = false; - @Nullable Segment initializationSegment = null; + @Nullable + Segment initializationSegment = + (previousMediaPlaylist != null) ? previousMediaPlaylist.lastSeenInitSegment : null; HashMap variableDefinitions = new HashMap<>(); HashMap urlToInferredInitSegment = new HashMap<>(); List segments = new ArrayList<>(); @@ -1514,7 +1516,8 @@ && parseOptionalStringAttr( trailingParts, serverControl, renditionReportMap, - interstitials); + interstitials, + initializationSegment); } private static DrmInitData getPlaylistProtectionSchemes( diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsRedundantGroup.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsRedundantGroup.java index 2913eebf134..e0a3a1bc490 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsRedundantGroup.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsRedundantGroup.java @@ -22,6 +22,7 @@ import android.net.Uri; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.ParserException; import androidx.media3.common.util.UnstableApi; @@ -149,6 +150,7 @@ public int hashCode() { public final GroupKey groupKey; private final HashMap pathwayIdToPlaylistUrl; + private final List indicesInMultivariantPlaylist; private String currentPathwayId; /** @@ -159,10 +161,30 @@ public int hashCode() { * @param playlistUrl The playlist url that is associated with the default current pathway. */ public HlsRedundantGroup(GroupKey groupKey, String pathwayId, Uri playlistUrl) { + this(groupKey, pathwayId, playlistUrl, C.INDEX_UNSET); + } + + /** + * Creates a {@link HlsRedundantGroup}. + * + * @param groupKey See {@link #groupKey}. + * @param pathwayId The ID of the default current pathway. + * @param playlistUrl The playlist url that is associated with the default current pathway. + * @param indexInMultivariantPlaylist The index of the {@code playlistUrl} in either {@link + * HlsMultivariantPlaylist#variants}, {@link HlsMultivariantPlaylist#videos}, {@link + * HlsMultivariantPlaylist#audios} or {@link HlsMultivariantPlaylist#subtitles}. May be {@link + * C#INDEX_UNSET} if the {@code playlistUrl} is not declared in the multivariant playlist. + */ + public HlsRedundantGroup( + GroupKey groupKey, String pathwayId, Uri playlistUrl, int indexInMultivariantPlaylist) { this.groupKey = groupKey; this.pathwayIdToPlaylistUrl = new HashMap<>(); this.pathwayIdToPlaylistUrl.put(pathwayId, playlistUrl); this.currentPathwayId = pathwayId; + this.indicesInMultivariantPlaylist = new ArrayList<>(); + if (indexInMultivariantPlaylist != C.INDEX_UNSET) { + indicesInMultivariantPlaylist.add(indexInMultivariantPlaylist); + } } /** @@ -170,7 +192,25 @@ public HlsRedundantGroup(GroupKey groupKey, String pathwayId, Uri playlistUrl) { * HlsRedundantGroup}. */ public void put(String pathwayId, Uri playlistUrl) { + put(pathwayId, playlistUrl, C.INDEX_UNSET); + } + + /** + * Puts a {@code playlistUrl} and its associated {@code pathwayId} to the {@link + * HlsRedundantGroup}. + * + * @param pathwayId The ID of the pathway to add. + * @param playlistUrl The playlist url to add. + * @param indexInMultivariantPlaylist The index of the {@code playlistUrl} in either {@link + * HlsMultivariantPlaylist#variants}, {@link HlsMultivariantPlaylist#videos}, {@link + * HlsMultivariantPlaylist#audios} or {@link HlsMultivariantPlaylist#subtitles}. May be {@link + * C#INDEX_UNSET} if the {@code playlistUrl} is not declared in the multivariant playlist. + */ + public void put(String pathwayId, Uri playlistUrl, int indexInMultivariantPlaylist) { pathwayIdToPlaylistUrl.put(pathwayId, playlistUrl); + if (indexInMultivariantPlaylist != C.INDEX_UNSET) { + indicesInMultivariantPlaylist.add(indexInMultivariantPlaylist); + } } /** Returns the size of the {@link HlsRedundantGroup}. */ @@ -213,6 +253,16 @@ public ImmutableSet getAllPlaylistUrls() { return ImmutableSet.copyOf(pathwayIdToPlaylistUrl.values()); } + /** + * Returns the indices of the {@linkplain Variant variants} or {@linkplain Rendition renditions} + * belonging to this {@link HlsRedundantGroup} in either {@link HlsMultivariantPlaylist#variants}, + * {@link HlsMultivariantPlaylist#videos}, {@link HlsMultivariantPlaylist#audios} or {@link + * HlsMultivariantPlaylist#subtitles}. + */ + public ImmutableList getIndicesInMultivariantPlaylist() { + return ImmutableList.copyOf(indicesInMultivariantPlaylist); + } + /** * Creates a list of {@linkplain HlsRedundantGroup redundant groups} for {@linkplain Variant * variants}. @@ -234,11 +284,13 @@ public static ImmutableList createVariantRedundantGroupList( HashMap redundantGroupIndices = new HashMap<>(); HashMap generatedPathwayIdCounts = new HashMap<>(); - for (Variant variant : variants) { + for (int i = 0; i < variants.size(); i++) { + Variant variant = variants.get(i); GroupKey groupKey = new GroupKey(variant.format, variant.stableVariantId); propagateRedundantGroupList( variant.url, variant.pathwayId, + /* indexInMultivariantPlaylist= */ i, redundantGroupList, groupKey, redundantGroupIndices, @@ -263,7 +315,8 @@ public static ImmutableList createRenditionRedundantGroupList ArrayList redundantGroupList = new ArrayList<>(); HashMap redundantGroupIndices = new HashMap<>(); HashMap generatedPathwayIdCounts = new HashMap<>(); - for (Rendition rendition : renditions) { + for (int i = 0; i < renditions.size(); i++) { + Rendition rendition = renditions.get(i); if (rendition.url == null) { continue; } @@ -273,6 +326,7 @@ public static ImmutableList createRenditionRedundantGroupList propagateRedundantGroupList( rendition.url, /* knownPathwayId= */ null, + /* indexInMultivariantPlaylist= */ i, redundantGroupList, groupKey, redundantGroupIndices, @@ -299,6 +353,10 @@ public static ImmutableList createRenditionRedundantGroupList * ID will be used. The first is '.', and the subsequent ones for the same group are '..', * '...', according to the order of the corresponding {@link Variant} or {@link Rendition} in * the {@link HlsMultivariantPlaylist}. + * @param indexInMultivariantPlaylist The index of the {@code playlistUrl} in either {@link + * HlsMultivariantPlaylist#variants}, {@link HlsMultivariantPlaylist#videos}, {@link + * HlsMultivariantPlaylist#audios} or {@link HlsMultivariantPlaylist#subtitles}. May be {@link + * C#INDEX_UNSET} if the {@code playlistUrl} is not declared in the multivariant playlist. * @param redundantGroupList The list of {@link HlsRedundantGroup} to be propagated. * @param groupKey The {@link GroupKey} to look up the existing {@link HlsRedundantGroup}. * @param redundantGroupIndices A map to look up an existing {@link HlsRedundantGroup} for a @@ -315,6 +373,7 @@ public static ImmutableList createRenditionRedundantGroupList private static void propagateRedundantGroupList( Uri url, @Nullable String knownPathwayId, + int indexInMultivariantPlaylist, List redundantGroupList, GroupKey groupKey, Map redundantGroupIndices, @@ -329,7 +388,8 @@ private static void propagateRedundantGroupList( pathwayId = "."; generatedPathwayIdCounts.put(groupKey, 1); } - HlsRedundantGroup newRedundantGroup = new HlsRedundantGroup(groupKey, pathwayId, url); + HlsRedundantGroup newRedundantGroup = + new HlsRedundantGroup(groupKey, pathwayId, url, indexInMultivariantPlaylist); redundantGroupIndices.put(groupKey, redundantGroupList.size()); redundantGroupList.add(newRedundantGroup); } else { @@ -349,7 +409,7 @@ private static void propagateRedundantGroupList( pathwayId), /* cause= */ null); } - redundantGroup.put(pathwayId, url); + redundantGroup.put(pathwayId, url, indexInMultivariantPlaylist); } } } 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 58d65b5fa30..4c4f0ba8588 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 @@ -49,6 +49,7 @@ import androidx.media3.exoplayer.hls.playlist.HlsRedundantGroup; import androidx.media3.exoplayer.hls.playlist.HlsRedundantGroup.GroupKey; import androidx.media3.exoplayer.source.MediaSourceEventListener; +import androidx.media3.exoplayer.source.chunk.Chunk; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; @@ -707,6 +708,38 @@ public void getNextChunk_reloadingCurrentPreloadPartAfterPublication_returnsShou assertThat(((HlsMediaChunk) output.chunk).shouldSpliceIn()).isTrue(); } + @Test + public void getChunkPublicationState_withCmcdQueryParameters_returnsPublished() throws Exception { + CmcdConfiguration.Factory cmcdConfigurationFactory = + mediaItem -> + new CmcdConfiguration( + /* sessionId= */ "sessionId", + /* contentId= */ mediaItem.mediaId, + new CmcdConfiguration.RequestConfig() {}, + CmcdConfiguration.MODE_QUERY_PARAMETER); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); + HlsChunkSource chunkSource = + createHlsChunkSource( + PLAYLIST_LIVE_LOW_LATENCY_SEGMENTS_AND_SINGLE_PRELOAD_PART, cmcdConfiguration); + HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder(); + chunkSource.getNextChunk( + new LoadingInfo.Builder().setPlaybackPositionUs(34_000_000).setPlaybackSpeed(1.0f).build(), + /* loadPositionUs= */ 34_000_000, + /* largestReadPositionUs= */ 0, + /* queue= */ ImmutableList.of(), + /* allowEndOfStream= */ true, + output); + HlsMediaChunk hlsMediaChunk = (HlsMediaChunk) output.chunk; + HlsChunkSource updatedChunkSource = + createHlsChunkSource(PLAYLIST_LIVE_LOW_LATENCY_SEGMENTS_AND_PARTS, cmcdConfiguration); + + int publicationState = updatedChunkSource.getChunkPublicationState(hlsMediaChunk); + + assertThat(publicationState).isEqualTo(HlsChunkSource.CHUNK_PUBLICATION_STATE_PUBLISHED); + } + @Test public void getNextChunk_changedTrackSelectionWithNonOverlappingSegments_returnsShouldSpliceInFalse() @@ -1003,7 +1036,8 @@ public void maybeThrowError_withBlockedNextChunkAndThenPlaylistError_doesThrow() } @Test - public void createFallbackOptionsForPlaylistError_returnsCorrectResult() throws IOException { + public void createFallbackOptionsForPlaylistError_withAllTracksSelected_returnsCorrectResult() + throws IOException { HlsRedundantGroup[] redundantGroups = createSampleRedundantGroups(); HlsPlaylistTracker mockHlsPlaylistTracker = mock(HlsPlaylistTracker.class); HlsChunkSource testChunkSource = createHlsChunkSource(redundantGroups, mockHlsPlaylistTracker); @@ -1029,7 +1063,8 @@ public void createFallbackOptionsForPlaylistError_returnsCorrectResult() throws } @Test - public void createFallbackOptionsForChunkError_returnsCorrectResult() throws IOException { + public void createFallbackOptionsForChunkError_withAllTracksSelected_returnsCorrectResult() + throws IOException { HlsRedundantGroup[] redundantGroups = createSampleRedundantGroups(); HlsPlaylistTracker mockPlaylistTracker = mock(HlsPlaylistTracker.class); HlsChunkSource testChunkSource = createHlsChunkSource(redundantGroups, mockPlaylistTracker); @@ -1059,13 +1094,56 @@ public void createFallbackOptionsForChunkError_returnsCorrectResult() throws IOE assertThat(fallbackOptions.numberOfExcludedTracks).isEqualTo(1); } + @Test + public void + createFallbackOptionsForPlaylistError_subsetTrackSelectionWithTrackExcluded_returnsCorrectResult() + throws IOException { + HlsRedundantGroup[] redundantGroups = createSampleRedundantGroups(); + HlsPlaylistTracker mockPlaylistTracker = mock(HlsPlaylistTracker.class); + HlsChunkSource testChunkSource = createHlsChunkSource(redundantGroups, mockPlaylistTracker); + FakeTrackSelection trackSelection = + new FakeTrackSelection( + testChunkSource.getTrackGroup(), new int[] {1, 3}, /* selectedIndex= */ 0); + trackSelection.enable(); + assertThat(trackSelection.excludeTrack(/* index= */ 1, /* exclusionDurationMs= */ 10000)) + .isTrue(); + testChunkSource.setTrackSelection(trackSelection); + Uri playlistUrl = Uri.parse("https://test/media-a/playlist1.m3u8"); + + FallbackOptions fallbackOptions = testChunkSource.createFallbackOptions(playlistUrl); + + assertThat(fallbackOptions.numberOfTracks).isEqualTo(2); + assertThat(fallbackOptions.numberOfExcludedTracks).isEqualTo(1); + } + + @Test + public void + createFallbackOptionsForChunkError_subsetTrackSelectionWithPlaylistExcluded_returnsCorrectResult() + throws IOException { + HlsRedundantGroup[] redundantGroups = createSampleRedundantGroups(); + HlsPlaylistTracker mockPlaylistTracker = mock(HlsPlaylistTracker.class); + HlsChunkSource testChunkSource = createHlsChunkSource(redundantGroups, mockPlaylistTracker); + FakeTrackSelection trackSelection = + new FakeTrackSelection( + testChunkSource.getTrackGroup(), new int[] {1, 3}, /* selectedIndex= */ 0); + trackSelection.enable(); + testChunkSource.setTrackSelection(trackSelection); + Chunk mockChunk = mock(Chunk.class); + when(mockPlaylistTracker.isExcluded(eq(redundantGroups[3]), anyLong())).thenReturn(true); + + FallbackOptions fallbackOptions = testChunkSource.createFallbackOptions(mockChunk); + + assertThat(fallbackOptions.numberOfTracks).isEqualTo(2); + assertThat(fallbackOptions.numberOfExcludedTracks).isEqualTo(1); + } + @Test public void onPlaylistError_fallbackSelectionIsNull_returnsFalse() throws IOException { HlsRedundantGroup[] redundantGroups = createSampleRedundantGroups(); HlsPlaylistTracker mockHlsPlaylistTracker = mock(HlsPlaylistTracker.class); HlsChunkSource testChunkSource = createHlsChunkSource(redundantGroups, mockHlsPlaylistTracker); TrackGroup trackGroup = testChunkSource.getTrackGroup(); - TestTrackSelection trackSelection = new TestTrackSelection(trackGroup, /* selectedIndex= */ 0); + FakeTrackSelection trackSelection = new FakeTrackSelection(trackGroup); trackSelection.enable(); testChunkSource.setTrackSelection(trackSelection); when(mockHlsPlaylistTracker.excludeMediaPlaylist(any(), anyLong())).thenReturn(true); @@ -1082,7 +1160,7 @@ public void onPlaylistError_fallbackSelectionIsTrackType_excludeTrackAndPlaylist HlsPlaylistTracker mockHlsPlaylistTracker = mock(HlsPlaylistTracker.class); HlsChunkSource testChunkSource = createHlsChunkSource(redundantGroups, mockHlsPlaylistTracker); TrackGroup trackGroup = testChunkSource.getTrackGroup(); - TestTrackSelection trackSelection = new TestTrackSelection(trackGroup, /* selectedIndex= */ 0); + FakeTrackSelection trackSelection = new FakeTrackSelection(trackGroup); trackSelection.enable(); testChunkSource.setTrackSelection(trackSelection); when(mockHlsPlaylistTracker.excludeMediaPlaylist(any(), anyLong())).thenReturn(true); @@ -1104,7 +1182,7 @@ public void onPlaylistError_fallbackSelectionIsLocationType_excludePlaylistOnly( HlsPlaylistTracker mockHlsPlaylistTracker = mock(HlsPlaylistTracker.class); HlsChunkSource testChunkSource = createHlsChunkSource(redundantGroups, mockHlsPlaylistTracker); TrackGroup trackGroup = testChunkSource.getTrackGroup(); - TestTrackSelection trackSelection = new TestTrackSelection(trackGroup, /* selectedIndex= */ 0); + FakeTrackSelection trackSelection = new FakeTrackSelection(trackGroup); trackSelection.enable(); testChunkSource.setTrackSelection(trackSelection); when(mockHlsPlaylistTracker.excludeMediaPlaylist(any(), anyLong())).thenReturn(true); @@ -1125,7 +1203,7 @@ public void onChunkError_fallbackSelectionIsNull_returnsFalse() throws IOExcepti HlsPlaylistTracker mockHlsPlaylistTracker = mock(HlsPlaylistTracker.class); HlsChunkSource testChunkSource = createHlsChunkSource(redundantGroups, mockHlsPlaylistTracker); TrackGroup trackGroup = testChunkSource.getTrackGroup(); - TestTrackSelection trackSelection = new TestTrackSelection(trackGroup, /* selectedIndex= */ 0); + FakeTrackSelection trackSelection = new FakeTrackSelection(trackGroup); trackSelection.enable(); testChunkSource.setTrackSelection(trackSelection); when(mockHlsPlaylistTracker.excludeMediaPlaylist(any(), anyLong())).thenReturn(true); @@ -1147,7 +1225,7 @@ public void onChunkError_fallbackSelectionIsTrackType_excludeTrackOnly() throws HlsPlaylistTracker mockHlsPlaylistTracker = mock(HlsPlaylistTracker.class); HlsChunkSource testChunkSource = createHlsChunkSource(redundantGroups, mockHlsPlaylistTracker); TrackGroup trackGroup = testChunkSource.getTrackGroup(); - TestTrackSelection trackSelection = new TestTrackSelection(trackGroup, /* selectedIndex= */ 0); + FakeTrackSelection trackSelection = new FakeTrackSelection(trackGroup); trackSelection.enable(); testChunkSource.setTrackSelection(trackSelection); when(mockHlsPlaylistTracker.excludeMediaPlaylist(any(), anyLong())).thenReturn(true); @@ -1175,7 +1253,7 @@ public void onChunkError_fallbackSelectionIsLocationType_excludePlaylistOnly() HlsPlaylistTracker mockHlsPlaylistTracker = mock(HlsPlaylistTracker.class); HlsChunkSource testChunkSource = createHlsChunkSource(redundantGroups, mockHlsPlaylistTracker); TrackGroup trackGroup = testChunkSource.getTrackGroup(); - TestTrackSelection trackSelection = new TestTrackSelection(trackGroup, /* selectedIndex= */ 0); + FakeTrackSelection trackSelection = new FakeTrackSelection(trackGroup); trackSelection.enable(); testChunkSource.setTrackSelection(trackSelection); when(mockHlsPlaylistTracker.excludeMediaPlaylist(any(), anyLong())).thenReturn(true); @@ -1437,25 +1515,4 @@ private static long playlistTimeToPeriodTimeUs(long playlistTimeUs) { private static long periodTimeToPlaylistTimeUs(long periodTimeUs) { return periodTimeUs - PLAYLIST_START_PERIOD_OFFSET_US; } - - private static final class TestTrackSelection extends FakeTrackSelection { - - private final long[] excludeUntilMs; - - private TestTrackSelection(TrackGroup rendererTrackGroup, int selectedIndex) { - super(rendererTrackGroup, selectedIndex); - excludeUntilMs = new long[rendererTrackGroup.length]; - } - - @Override - public boolean excludeTrack(int index, long exclusionDurationMs) { - excludeUntilMs[index] = exclusionDurationMs; - return true; - } - - @Override - public boolean isTrackExcluded(int index, long nowMs) { - return nowMs <= excludeUntilMs[index]; - } - } } 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 2f1427bcb8c..fc4f226b990 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 @@ -21,6 +21,8 @@ import static androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.getClosestSegmentBoundaryUs; +import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.SNAP_TYPE_IN; +import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.SNAP_TYPE_OUT; import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.truth.Truth.assertThat; @@ -63,8 +65,11 @@ import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.AdsResumptionState; import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.Asset; import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.AssetList; +import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.ContentMediaSourceAdDataHolder; import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.Listener; +import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.PendingSnapInResolution; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist; +import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial; import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.ads.AdsLoader; @@ -1527,45 +1532,35 @@ public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfRe + "#EXT-X-DATERANGE:" + "ID=\"ad0-0\"," + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:39.900\"," // snap to 12.222 - 12.222 -> 0L + + "START-DATE=\"2020-01-02T21:55:40.000\"," // + "X-SNAP=\"IN\"," - + "X-PLAYOUT-LIMIT=12.222," + + "X-RESUME-OFFSET=3.222," // Snaps to start of next content segment. + + "X-PLAYOUT-LIMIT=3.222," + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + "\n" + "#EXT-X-DATERANGE:" + "ID=\"ad1-0\"," + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:51.222\"," // aligned 12.222 - 3.222 -> 9_000L - + "X-SNAP=\"IN\"," - + "X-PLAYOUT-LIMIT=3.222," - + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" - + "\n" - + "#EXT-X-DATERANGE:" - + "ID=\"ad2-0\"," - + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:54.678\"," // snap to end of window - 4.333 + + "START-DATE=\"2020-01-02T21:55:52.222\"," + "X-SNAP=\"IN\"," + + "X-RESUME-OFFSET=4.333," // Snaps to start of next content segment. + "X-PLAYOUT-LIMIT=4.333," - + "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\"" + + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" + "\n", adsLoader, /* windowIndex= */ 0, /* windowPositionInPeriodUs= */ 0, /* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE)) .isEqualTo( - new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 9_000_000L, 14_000_000L) + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 12_222_000) .withAdCount(/* adGroupIndex= */ 0, 1) .withAdCount(/* adGroupIndex= */ 1, 1) - .withAdCount(/* adGroupIndex= */ 2, 1) - .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 12_222_000L) - .withContentResumeOffsetUs(/* adGroupIndex= */ 1, 3_222_000L) - .withContentResumeOffsetUs(/* adGroupIndex= */ 2, 4_333_000L) - .withAdDurationsUs(/* adGroupIndex= */ 0, 12_222_000L) - .withAdDurationsUs(/* adGroupIndex= */ 1, 3_222_000L) - .withAdDurationsUs(/* adGroupIndex= */ 2, 4_333_000L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 6_111_000L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 1, 6_111_000L) + .withAdDurationsUs(/* adGroupIndex= */ 0, 3_222_000L) + .withAdDurationsUs(/* adGroupIndex= */ 1, 4_333_000L) .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") .withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0") - .withAdId(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, "ad2-0") .withAvailableAdMediaItem( /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, @@ -1579,13 +1574,6 @@ public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfRe new MediaItem.Builder() .setUri("http://example.com/media-1-0.m3u8") .setMimeType(MimeTypes.APPLICATION_M3U8) - .build()) - .withAvailableAdMediaItem( - /* adGroupIndex= */ 2, - /* adIndexInAdGroup= */ 0, - new MediaItem.Builder() - .setUri("http://example.com/media-2-0.m3u8") - .setMimeType(MimeTypes.APPLICATION_M3U8) .build())); } @@ -1604,43 +1592,33 @@ public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfRe + "#EXT-X-DATERANGE:" + "ID=\"ad0-0\"," + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:39.900\"," // snap to (12.222 - 12.222) -> 0L + + "START-DATE=\"2020-01-02T21:55:40.111\"," + "X-SNAP=\"IN\"," - + "X-RESUME-OFFSET=12.222," + + "X-RESUME-OFFSET=3.222," // snap to end of first segment: 6.000 + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + "\n" + "#EXT-X-DATERANGE:" + "ID=\"ad1-0\"," + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:51.222\"," // snap to (12.222 - 3.22) -> 9_000L - + "X-SNAP=\"IN\"," - + "X-RESUME-OFFSET=3.222," - + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" - + "\n" - + "#EXT-X-DATERANGE:" - + "ID=\"ad2-0\"," - + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:54.678\"," // snap to (end of window - 4.333) + + "START-DATE=\"2020-01-02T21:55:52.332\"," + "X-SNAP=\"IN\"," + "DURATION=4.333," - + "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\"" + + "X-RESUME-OFFSET=4.333," // snap to end of first segment: 6.001 + + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" + "\n" + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + "#EXTINF:6.111,\nmain1.ts\n" + "#EXTINF:6.111,\nmain2.ts\n" // segment start at 6.111 + "#EXTINF:6.111,\nmain3.ts\n")) // segment start at 12.222 .containsExactly( - new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 123L, 9_000_123L, 14_000_123L) + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 111_123L, 12_332_123L) .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) .withAdCount(/* adGroupIndex= */ 0, 1) .withAdCount(/* adGroupIndex= */ 1, 1) - .withAdCount(/* adGroupIndex= */ 2, 1) - .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 12_222_000L) - .withContentResumeOffsetUs(/* adGroupIndex= */ 1, 3_222_000L) - .withContentResumeOffsetUs(/* adGroupIndex= */ 2, 4_333_000L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 6_000_000L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 1, 6_001_000L) .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") .withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0") - .withAdId(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, "ad2-0") .withAvailableAdMediaItem( /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, @@ -1654,20 +1632,12 @@ public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfRe new MediaItem.Builder() .setUri("http://example.com/media-1-0.m3u8") .setMimeType(MimeTypes.APPLICATION_M3U8) - .build()) - .withAvailableAdMediaItem( - /* adGroupIndex= */ 2, - /* adIndexInAdGroup= */ 0, - new MediaItem.Builder() - .setUri("http://example.com/media-2-0.m3u8") - .setMimeType(MimeTypes.APPLICATION_M3U8) .build())); } @Test - public void - handleContentTimelineChanged_snapInFarBeforeOrAfterWindow_snapToStartOfWindowAndPostRoll() - throws IOException { + public void handleContentTimelineChanged_snapInFarBeforeOrAfterWindow_ignored() + throws IOException { assertThat( callHandleContentTimelineChangedAndCaptureAdPlaybackState( "#EXTM3U\n" @@ -1679,58 +1649,29 @@ public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfRe + "#EXT-X-DATERANGE:" + "ID=\"ad0-0\"," // pre roll + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"1990-01-02T00:00:00.000\"," // snap to start of window. - + "X-PLAYOUT-LIMIT=1.0," - + "X-RESUME-OFFSET=0.0," // with no offset SNAP_IN => SNAP_OUT + + "START-DATE=\"2020-01-02T21:55:39.999\"," // Starts behind live window: ignore + "X-SNAP=\"IN\"," + + "X-PLAYOUT-LIMIT=1.0," + + "X-RESUME-OFFSET=1.0," + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + "\n" + "#EXT-X-DATERANGE:" + "ID=\"ad1-0\"," // ignored + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"1990-01-02T00:00:00.000\"," // snap end to start of window - + "DURATION=1.0," // translate start of ad back to 1s behind window + + "START-DATE=\"2020-01-02T21:55:46.112\"," // Before end of window. + "X-SNAP=\"IN\"," + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" - + "\n" - + "#EXT-X-DATERANGE:" - + "ID=\"ad2-0\"," // post roll - + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2050-01-02T21:55:00.900\"," // snap to end of window - + "X-SNAP=\"IN\"," - + "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\"" + "\n", adsLoader, /* windowIndex= */ 0, /* windowPositionInPeriodUs= */ 0, /* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE)) - .isEqualTo( - new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, C.TIME_END_OF_SOURCE) - .withAdCount(/* adGroupIndex= */ 0, 1) - .withAdCount(/* adGroupIndex= */ 1, 1) - .withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L) - .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") - .withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad2-0") - .withAvailableAdMediaItem( - /* adGroupIndex= */ 0, - /* adIndexInAdGroup= */ 0, - new MediaItem.Builder() - .setUri("http://example.com/media-0-0.m3u8") - .setMimeType(MimeTypes.APPLICATION_M3U8) - .build()) - .withAvailableAdMediaItem( - /* adGroupIndex= */ 1, - /* adIndexInAdGroup= */ 0, - new MediaItem.Builder() - .setUri("http://example.com/media-2-0.m3u8") - .setMimeType(MimeTypes.APPLICATION_M3U8) - .build())); + .isEqualTo(new AdPlaybackState("adsId")); } @Test - public void - handleContentTimelineChanged_snapInLiveFarBeforeOrAfterWindow_snapToStartAndEndOfWindow() - throws IOException { + public void handleContentTimelineChanged_snapInLiveBeforeOrAfterWindow_ignored() + throws IOException { assertThat( callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates( adsLoader, @@ -1742,53 +1683,84 @@ public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfRe + "#EXT-X-DATERANGE:" + "ID=\"ad0-0\"," // pre roll + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"1990-01-02T00:00:00.000\"," // snap to start of window + + "START-DATE=\"2020-01-02T21:55:39.999Z\"," // Starts behind start of window. + "X-PLAYOUT-LIMIT=1.0," - + "X-RESUME-OFFSET=0.0," // with no offset SNAP_IN => SNAP_OUT + + "X-RESUME-OFFSET=0.0," + "X-SNAP=\"IN\"," + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + "\n" + "#EXT-X-DATERANGE:" - + "ID=\"ad1-0\"," // ignore + + "ID=\"ad1-0\"," // midroll starts 3x target duration before window: ignored + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"1990-01-02T00:00:00.000\"," // snap to start of window - + "DURATION=1.0," // translate start of ad back to 1s behind window + + "START-DATE=\"2020-01-02T21:56:10.223\"," + "X-SNAP=\"IN\"," + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" + "\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6.111,\nmain1.0.ts\n" + + "#EXTINF:6.111,\nmain1.0.ts\n" // end of window: 12:222 - 21:55:52.222 + + "\n")) + .containsExactly( + new AdPlaybackState("adsId") + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false)); + } + + @Test + public void + handleContentTimelineChanged_snapInLiveEndsBeforeWindowEnds_deferredResumeOffsetCalculation() + throws IOException { + AdPlaybackState expectedAdPlaybackState = + new AdPlaybackState("adsId", 6_111_000L) + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 10_000_000L) + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-0-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()); + assertThat( + callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates( + adsLoader, + /* startAdsLoader= */ true, + /* windowOffsetInFirstPeriodUs= */ 0, + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," // pre roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:48.111Z\"," // snap OUT to start of segment + + "X-RESUME-OFFSET=10.000," // snap IN to end of playlist: 12_222 + + "X-SNAP=\"OUT,IN\"," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6.111,\nmain1.0.ts\n" + + "#EXTINF:6.111,\nmain2.0.ts\n" // end of window: 12:222 - 21:55:52.222 + + "\n", + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + "#EXT-X-DATERANGE:" - + "ID=\"ad2-0\"," // mid roll at end of window (but not C.TIME_END_OF_SOURCE) + + "ID=\"ad0-0\"," // pre roll + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2050-01-02T21:55:00.900\"," // snap end of window: 12_222_000 - + "X-SNAP=\"IN\"," // no duration or offset: SNAP_IN => SNAP_OUT - + "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\"" + + "START-DATE=\"2020-01-02T21:55:46.111Z\"," + + "X-RESUME-OFFSET=10.000," + + "X-SNAP=\"IN,OUT\"," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + "\n" + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + "#EXTINF:6.111,\nmain1.0.ts\n" - + "#EXTINF:6.111,\nmain1.0.ts\n" // end of window: 12:222 - 21:55:52.222 + + "#EXTINF:6.111,\nmain2.0.ts\n" // end of window: 12:222 - 21:55:52.222 + + "#EXTINF:6.111,\nmain3.0.ts\n" // new segment triggers deferred resolution + "\n")) .containsExactly( - new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 12_222_000L) - .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) - .withAdCount(/* adGroupIndex= */ 0, 1) - .withAdCount(/* adGroupIndex= */ 1, 1) - .withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L) - .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") - .withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad2-0") - .withAvailableAdMediaItem( - /* adGroupIndex= */ 0, - /* adIndexInAdGroup= */ 0, - new MediaItem.Builder() - .setUri("http://example.com/media-0-0.m3u8") - .setMimeType(MimeTypes.APPLICATION_M3U8) - .build()) - .withAvailableAdMediaItem( - /* adGroupIndex= */ 1, - /* adIndexInAdGroup= */ 0, - new MediaItem.Builder() - .setUri("http://example.com/media-2-0.m3u8") - .setMimeType(MimeTypes.APPLICATION_M3U8) - .build())); + expectedAdPlaybackState, + expectedAdPlaybackState.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 12_222_000L)); } @Test @@ -1808,16 +1780,16 @@ public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfRe + "#EXT-X-DATERANGE:" + "ID=\"ad0-0\"," // mid roll + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:45.000\"," // snap tp 6_111_000 + + "START-DATE=\"2020-01-02T21:55:45.000\"," // snaps OUT to 6_111_000 + "X-SNAP=\"OUT\"," + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + "\n" + "#EXT-X-DATERANGE:" + "ID=\"ad0-1\"," // mid roll + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:47.000\"," // snap to 6_111_000 - + "X-SNAP=\"IN\"," - + "X-PLAYOUT-LIMIT=6.111," // ends at 21:55:53.111 -> 21:55:52.222 - 6.111 + + "START-DATE=\"2020-01-02T21:55:47.111\"," // snaps OUT to 6_111_000 + + "X-SNAP=\"OUT,IN\"," + + "X-RESUME-OFFSET=3.111," // Changed by snap IN to 6.111 + "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\"" + "\n", adsLoader, @@ -1828,7 +1800,6 @@ public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfRe new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 6_111_000L) .withAdCount(/* adGroupIndex= */ 0, 2) .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 6_111_000L) - .withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, 6_111_000L) .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1") .withAvailableAdMediaItem( @@ -1862,17 +1833,16 @@ public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfRe + "#EXT-X-DATERANGE:" + "ID=\"ad0-0\"," + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:45.000\"," // -> 6_111_000L - + "X-SNAP=\"OUT\"," - + "X-PLAYOUT-LIMIT=2.222," + + "START-DATE=\"2020-01-02T21:55:45.000\"," // snaps OUT to 6_111_000L + + "X-SNAP=\"OUT,IN\"," + + "X-RESUME-OFFSET=4.222," // snaps IN to 6.111 + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + "\n" + "#EXT-X-DATERANGE:" + "ID=\"ad0-1\"," + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:47.000\"," // -> 6_111_000L - + "X-SNAP=\"IN\"," - + "X-PLAYOUT-LIMIT=6.111," + + "START-DATE=\"2020-01-02T21:55:47.000\"," // snaps OUT to 6_111_000L + + "X-SNAP=\"OUT\"," + "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\"" + "\n" + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" @@ -1884,8 +1854,7 @@ public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfRe new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 6_111_000L) .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) .withAdCount(/* adGroupIndex= */ 0, 2) - .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 8_333_000L) - .withAdDurationsUs(/* adGroupIndex= */ 0, 2_222_000L, 6_111_000L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 6_111_000L) .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1") .withAvailableAdMediaItem( @@ -6547,6 +6516,75 @@ public void getClosestSegmentBoundaryUs_positionBeforeAndAfterPlaylist_getsSanit .isEqualTo(contentMediaPlaylist.startTimeUs + 24_000_000L); } + @Test + public void removePendingSnapInResolutionUntilIncludingIndex_correctOperations() { + Interstitial interstitial = + new Interstitial( + /* id= */ "ad1", + /* assetUri= */ Uri.parse("http://example.com/ad1.m3u8"), + /* assetListUri= */ null, + /* startDateUnixUs= */ 0L, + /* endDateUnixUs= */ C.TIME_UNSET, + /* durationUs= */ C.TIME_UNSET, + /* plannedDurationUs= */ C.TIME_UNSET, + /* cue= */ ImmutableList.of(), + /* endOnNext= */ false, + /* resumeOffsetUs= */ 0, + /* playoutLimitUs= */ C.TIME_UNSET, + ImmutableList.of(SNAP_TYPE_IN, SNAP_TYPE_OUT), + ImmutableList.of(), + /* clientDefinedAttributes= */ ImmutableList.of(), + /* contentMayVary= */ false, + /* timelineOccupies= */ Interstitial.TIMELINE_OCCUPIES_POINT, + /* timelineStyle= */ Interstitial.TIMELINE_STYLE_PRIMARY, + /* skipControlOffsetUs= */ C.TIME_UNSET, + /* skipControlDurationUs= */ C.TIME_UNSET, + /* skipControlLabelId= */ null); + ContentMediaSourceAdDataHolder holder = new ContentMediaSourceAdDataHolder(); + for (int i = 0; i < 10; i++) { + holder.putPendingSnapInResolution( + "adsId", + new PendingSnapInResolution( + /* resumeTimeUs= */ i * 10_000L, + /* adGroupIndex= */ i, + /* interstitial= */ interstitial)); + holder.putPendingSnapInResolution( + "adsId_other", + new PendingSnapInResolution( + /* resumeTimeUs= */ i * 10_000L, + /* adGroupIndex= */ i, + /* interstitial= */ interstitial)); + } + assertThat(holder.getPendingSnapInResolutions("adsId")).hasSize(10); + assertThat(holder.getPendingSnapInResolutions("adsId_other")).hasSize(10); + + holder.removePendingSnapInResolutionUntilIndexInclusive("adsId", /* resolvedIndex= */ 4); + holder.removePendingSnapInResolutionUntilIndexInclusive("adsId_other", /* resolvedIndex= */ 4); + + assertThat(holder.getPendingSnapInResolutions("adsId")).hasSize(5); + assertThat(holder.getPendingSnapInResolutions("adsId_other")).hasSize(5); + + holder.removePendingSnapInResolutionUntilIndexInclusive("adsId", /* resolvedIndex= */ 4); + holder.removePendingSnapInResolutionUntilIndexInclusive( + "adsId_other", /* resolvedIndex= */ 1234); + + assertThat(holder.getPendingSnapInResolutions("adsId")).isEmpty(); + assertThat(holder.getPendingSnapInResolutions("adsId_other")).isEmpty(); + assertThrows( + IllegalArgumentException.class, + () -> + holder.removePendingSnapInResolutionUntilIndexInclusive( + "adsId", /* resolvedIndex= */ -1)); + } + + @Test + public void removePendingSnapInResolutionUntilIncludingIndex_whenEmptyYieldsCorrectOperation() { + ContentMediaSourceAdDataHolder holder = new ContentMediaSourceAdDataHolder(); + + // Must not throw an exception because the empty list is immutable. + holder.removePendingSnapInResolutionUntilIndexInclusive("adsid", 2); + } + private List callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates( HlsInterstitialsAdsLoader adsLoader, boolean startAdsLoader, diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsMediaPeriodTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsMediaPeriodTest.java index cd8bf190f3f..39fab226b03 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsMediaPeriodTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsMediaPeriodTest.java @@ -76,6 +76,8 @@ public void getSteamKeys_isCompatibleWithHlsMultivariantPlaylistFilter() { createAudioOnlyVariant(Uri.parse("https://variant1"), /* peakBitrate= */ 10000), createMuxedVideoAudioVariant( Uri.parse("https://variant2"), /* peakBitrate= */ 200000), + createMuxedVideoAudioVariant( + Uri.parse("https://backup/variant2"), /* peakBitrate= */ 200000), createAudioOnlyVariant(Uri.parse("https://variant3"), /* peakBitrate= */ 300000), createMuxedVideoAudioVariant( Uri.parse("https://variant4"), /* peakBitrate= */ 400000), @@ -83,10 +85,13 @@ public void getSteamKeys_isCompatibleWithHlsMultivariantPlaylistFilter() { Uri.parse("https://variant5"), /* peakBitrate= */ 600000)), /* audios= */ Arrays.asList( createAudioRendition(Uri.parse("https://audio1"), /* language= */ "spa"), + createAudioRendition(Uri.parse("https://backup/audio1"), /* language= */ "spa"), createAudioRendition(Uri.parse("https://audio2"), /* language= */ "ger"), createAudioRendition(Uri.parse("https://audio3"), /* language= */ "tur")), /* subtitles= */ Arrays.asList( createSubtitleRendition(Uri.parse("https://subtitle1"), /* language= */ "spa"), + createSubtitleRendition( + Uri.parse("https://backup/subtitle1"), /* language= */ "spa"), createSubtitleRendition(Uri.parse("https://subtitle2"), /* language= */ "ger"), createSubtitleRendition(Uri.parse("https://subtitle3"), /* language= */ "tur")), /* muxedAudioFormat= */ createAudioFormat("eng"), diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsPreloadMediaSourceTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsPreloadMediaSourceTest.java new file mode 100644 index 00000000000..f993cabf81a --- /dev/null +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsPreloadMediaSourceTest.java @@ -0,0 +1,327 @@ +/* + * Copyright 2026 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 org.junit.Assert.assertThrows; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +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.mockito.Mockito.when; + +import android.content.Context; +import android.net.Uri; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.SystemClock; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.DefaultLoadControl; +import androidx.media3.exoplayer.LoadControl; +import androidx.media3.exoplayer.Renderer; +import androidx.media3.exoplayer.RendererCapabilities; +import androidx.media3.exoplayer.RenderersFactory; +import androidx.media3.exoplayer.analytics.PlayerId; +import androidx.media3.exoplayer.audio.AudioRendererEventListener; +import androidx.media3.exoplayer.metadata.MetadataOutput; +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; +import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.exoplayer.source.preload.PreloadMediaSource; +import androidx.media3.exoplayer.text.TextOutput; +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; +import androidx.media3.exoplayer.trackselection.TrackSelector; +import androidx.media3.exoplayer.upstream.BandwidthMeter; +import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter; +import androidx.media3.exoplayer.video.VideoRendererEventListener; +import androidx.media3.test.utils.FakeAudioRenderer; +import androidx.media3.test.utils.FakeVideoRenderer; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit test for {@link PreloadMediaSource} with HLS stream. */ +@RunWith(AndroidJUnit4.class) +public final class HlsPreloadMediaSourceTest { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private static final int TARGET_PRELOAD_DURATION_US = 10000; + private static final int LARGE_TARGET_BUFFER_BYTES_FOR_PRELOAD = Integer.MAX_VALUE; + private static final int SMALL_TARGET_BUFFER_BYTES_FOR_PRELOAD = 1024; + + private MediaSource.Factory mediaSourceFactory; + private LoadControl loadControl; + private BandwidthMeter bandwidthMeter; + private RenderersFactory renderersFactory; + private MediaItem mediaItem; + @Mock private PreloadMediaSource.PreloadControl mockPreloadControl; + + @Before + public void setUp() { + mediaSourceFactory = + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); + loadControl = + mock( + LoadControl.class, + delegatesTo( + new DefaultLoadControl.Builder() + .setPlayerTargetBufferBytes( + PlayerId.PRELOAD.name, LARGE_TARGET_BUFFER_BYTES_FOR_PRELOAD) + .build())); + bandwidthMeter = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); + renderersFactory = + (handler, videoListener, audioListener, textOutput, metadataOutput) -> + new Renderer[] { + new FakeVideoRenderer( + SystemClock.DEFAULT.createHandler(handler.getLooper(), /* callback= */ null), + videoListener), + new FakeAudioRenderer( + SystemClock.DEFAULT.createHandler(handler.getLooper(), /* callback= */ null), + audioListener) + }; + mediaItem = + new MediaItem.Builder() + .setUri(Uri.parse("asset://android_asset/media/hls/multi-segment/playlist.m3u8")) + .build(); + when(mockPreloadControl.onSourcePrepared(any())).thenReturn(true); + when(mockPreloadControl.onTracksSelected(any())).thenReturn(true); + when(mockPreloadControl.onContinueLoadingRequested(any(), anyLong())).thenReturn(true); + } + + @Test + public void preload_loadPeriodToTargetPreloadPosition() throws Exception { + AtomicBoolean preloadTerminated = new AtomicBoolean(); + when(mockPreloadControl.onContinueLoadingRequested(any(), anyLong())) + .thenAnswer( + invocation -> { + long bufferedDurationUs = invocation.getArgument(1); + if (bufferedDurationUs >= TARGET_PRELOAD_DURATION_US) { + preloadTerminated.set(true); + return false; + } + return true; + }); + TrackSelector trackSelector = + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()); + trackSelector.init(() -> {}, bandwidthMeter); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + + preloadMediaSource.preload(/* startPositionUs= */ 0L); + runMainLooperUntil(preloadTerminated::get); + + verify(loadControl).onPrepared(PlayerId.PRELOAD); + verify(mockPreloadControl).onSourcePrepared(eq(preloadMediaSource)); + verify(mockPreloadControl).onTracksSelected(eq(preloadMediaSource)); + verify(mockPreloadControl, atLeastOnce()) + .onContinueLoadingRequested(eq(preloadMediaSource), anyLong()); + verify(mockPreloadControl, never()).onUsedByPlayer(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onLoadingUnableToContinue(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onPreloadError(any(), eq(preloadMediaSource)); + } + + @Test + public void preload_stopWhenTracksSelectedByPreloadControl() throws Exception { + AtomicBoolean preloadTerminated = new AtomicBoolean(); + when(mockPreloadControl.onTracksSelected(any())) + .thenAnswer( + invocation -> { + preloadTerminated.set(true); + return false; + }); + TrackSelector trackSelector = + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()); + trackSelector.init(() -> {}, bandwidthMeter); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + + preloadMediaSource.preload(/* startPositionUs= */ 0L); + runMainLooperUntil(preloadTerminated::get); + + verify(loadControl).onPrepared(PlayerId.PRELOAD); + verify(mockPreloadControl).onSourcePrepared(eq(preloadMediaSource)); + verify(mockPreloadControl).onTracksSelected(eq(preloadMediaSource)); + verify(mockPreloadControl, never()) + .onContinueLoadingRequested(eq(preloadMediaSource), anyLong()); + verify(mockPreloadControl, never()).onUsedByPlayer(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onLoadingUnableToContinue(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onPreloadError(any(), eq(preloadMediaSource)); + } + + @Test + public void preload_stopWhenSourcePreparedByPreloadControl() throws Exception { + AtomicBoolean preloadTerminated = new AtomicBoolean(); + when(mockPreloadControl.onSourcePrepared(any())) + .thenAnswer( + invocation -> { + preloadTerminated.set(true); + return false; + }); + TrackSelector trackSelector = + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()); + trackSelector.init(() -> {}, bandwidthMeter); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + + preloadMediaSource.preload(/* startPositionUs= */ 0L); + runMainLooperUntil(preloadTerminated::get); + + verify(loadControl).onPrepared(PlayerId.PRELOAD); + verify(mockPreloadControl).onSourcePrepared(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onTracksSelected(eq(preloadMediaSource)); + verify(mockPreloadControl, never()) + .onContinueLoadingRequested(eq(preloadMediaSource), anyLong()); + verify(mockPreloadControl, never()).onUsedByPlayer(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onLoadingUnableToContinue(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onPreloadError(any(), eq(preloadMediaSource)); + } + + @Test + public void preload_loadToTheEndOfSource() throws Exception { + AtomicBoolean preloadTerminated = new AtomicBoolean(); + doAnswer( + invocation -> { + preloadTerminated.set(true); + return null; + }) + .when(mockPreloadControl) + .onLoadedToTheEndOfSource(any()); + TrackSelector trackSelector = + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()); + trackSelector.init(() -> {}, bandwidthMeter); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + + preloadMediaSource.preload(/* startPositionUs= */ 0L); + runMainLooperUntil(preloadTerminated::get); + + verify(loadControl).onPrepared(PlayerId.PRELOAD); + verify(mockPreloadControl).onSourcePrepared(eq(preloadMediaSource)); + verify(mockPreloadControl).onTracksSelected(eq(preloadMediaSource)); + // We expect onContinueLoadingRequested() to be called at least once before reaching the end of + // the source. + verify(mockPreloadControl, atLeastOnce()) + .onContinueLoadingRequested(eq(preloadMediaSource), anyLong()); + verify(mockPreloadControl).onLoadedToTheEndOfSource(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onUsedByPlayer(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onLoadingUnableToContinue(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onPreloadError(any(), eq(preloadMediaSource)); + } + + @Test + public void preload_withSmallTargetBufferBytesThreshold_onLoadingUnableToContinueCalled() { + AtomicBoolean preloadTerminated = new AtomicBoolean(); + doAnswer( + invocation -> { + preloadTerminated.set(true); + return null; + }) + .when(mockPreloadControl) + .onLoadedToTheEndOfSource(any()); + TrackSelector trackSelector = + new DefaultTrackSelector(ApplicationProvider.getApplicationContext()); + trackSelector.init(() -> {}, bandwidthMeter); + LoadControl loadControl = + mock( + LoadControl.class, + delegatesTo( + new DefaultLoadControl.Builder() + .setPlayerTargetBufferBytes( + PlayerId.PRELOAD.name, SMALL_TARGET_BUFFER_BYTES_FOR_PRELOAD) + .build())); + PreloadMediaSource.Factory preloadMediaSourceFactory = + new PreloadMediaSource.Factory( + mediaSourceFactory, + mockPreloadControl, + trackSelector, + bandwidthMeter, + getRendererCapabilities(renderersFactory), + loadControl, + Util.getCurrentOrMainLooper()); + PreloadMediaSource preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem); + + preloadMediaSource.preload(/* startPositionUs= */ 0L); + + assertThrows(TimeoutException.class, () -> runMainLooperUntil(preloadTerminated::get)); + verify(loadControl).onPrepared(PlayerId.PRELOAD); + verify(mockPreloadControl).onSourcePrepared(eq(preloadMediaSource)); + verify(mockPreloadControl).onTracksSelected(eq(preloadMediaSource)); + verify(mockPreloadControl, atLeastOnce()) + .onContinueLoadingRequested(eq(preloadMediaSource), anyLong()); + verify(mockPreloadControl, never()).onUsedByPlayer(eq(preloadMediaSource)); + verify(mockPreloadControl, atLeastOnce()).onLoadingUnableToContinue(eq(preloadMediaSource)); + verify(mockPreloadControl, never()).onPreloadError(any(), eq(preloadMediaSource)); + } + + private static RendererCapabilities[] getRendererCapabilities(RenderersFactory renderersFactory) { + Renderer[] renderers = + renderersFactory.createRenderers( + Util.createHandlerForCurrentLooper(), + mock(VideoRendererEventListener.class), + mock(AudioRendererEventListener.class), + mock(TextOutput.class), + mock(MetadataOutput.class)); + RendererCapabilities[] rendererCapabilities = new RendererCapabilities[renderers.length]; + for (int i = 0; i < renderers.length; i++) { + rendererCapabilities[i] = renderers[i].getCapabilities(); + } + return rendererCapabilities; + } +} diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java index 6f3248ac31c..a1729e413f3 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java @@ -696,6 +696,54 @@ public void start_httpBadRequest_forcesFullNonBlockingPlaylistRequest() assertRequestUrlsCalled(httpUrls); } + @Test + public void start_playlistUpdateWithoutInitSegment_persistsInitSegmentFromPreviousPlaylist() + throws Exception { + String playlist = + "#EXTM3U\n" + + "#EXT-X-VERSION:6\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-MEDIA-SEQUENCE:1\n" + + "#EXT-X-MAP:URI=\"init0.mp4\"\n" + + "#EXTINF:10.0,\n" + + "file0.mp4\n" + + "#EXT-X-DISCONTINUITY\n" + + "#EXT-X-MAP:URI=\"init1.mp4\"\n" + + "#EXTINF:10.0,\n" + + "file1.mp4\n"; + String playlistUpdate = + "#EXTM3U\n" + + "#EXT-X-VERSION:6\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-MEDIA-SEQUENCE:2\n" + + "#EXTINF:10.0,\n" + + "file2.mp4\n"; + + List httpUrls = + enqueueWebServerResponses( + new String[] {"/multivariant.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8"}, + getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT), + new MockResponse().setResponseCode(200).setBody(playlist), + new MockResponse().setResponseCode(200).setBody(playlistUpdate)); + + List mediaPlaylists = + runPlaylistTrackerAndCollectMediaPlaylists( + new DefaultHttpDataSource.Factory(), + Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()), + /* awaitedMediaPlaylistCount= */ 2); + + assertRequestUrlsCalled(httpUrls); + HlsMediaPlaylist.Segment segment0 = mediaPlaylists.get(0).segments.get(0); + HlsMediaPlaylist.Segment segment1 = mediaPlaylists.get(0).segments.get(1); + HlsMediaPlaylist.Segment segment2 = mediaPlaylists.get(1).segments.get(0); + assertThat(segment0.url).isEqualTo("file0.mp4"); + assertThat(segment0.initializationSegment.url).isEqualTo("init0.mp4"); + assertThat(segment1.url).isEqualTo("file1.mp4"); + assertThat(segment1.initializationSegment.url).isEqualTo("init1.mp4"); + assertThat(segment2.url).isEqualTo("file2.mp4"); + assertThat(segment2.initializationSegment.url).isEqualTo("init1.mp4"); + } + private List enqueueWebServerResponses(String[] paths, MockResponse... mockResponses) { assertThat(paths).hasLength(mockResponses.length); for (MockResponse mockResponse : mockResponses) { diff --git a/libraries/exoplayer_ima/build.gradle b/libraries/exoplayer_ima/build.gradle index 1aa74a6eb04..51fcb1fc216 100644 --- a/libraries/exoplayer_ima/build.gradle +++ b/libraries/exoplayer_ima/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.exoplayer.ima' diff --git a/libraries/exoplayer_rtsp/build.gradle b/libraries/exoplayer_rtsp/build.gradle index c66317e12a0..2adc4bd6b17 100644 --- a/libraries/exoplayer_rtsp/build.gradle +++ b/libraries/exoplayer_rtsp/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.exoplayer.rtsp' diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH264Reader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH264Reader.java index d39a617386a..3a792d374d1 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH264Reader.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH264Reader.java @@ -63,6 +63,7 @@ private long firstReceivedTimestamp; private int previousSequenceNumber; + private long previousTimestamp; /** The combined size of a sample that is fragmented into multiple RTP packets. */ private int fragmentedSampleSizeBytes; @@ -73,7 +74,9 @@ * If a Fragmentation Unit is lost then following units corresponding to the same NAL unit should * be discarded (RFC6184 Section 5.8). */ - boolean discardPacketsUntilNextNalUnit; + private boolean isCurrentAccessUnitCorrupted; + + private boolean isProcessingFragmentationUnit; /** Creates an instance. */ public RtpH264Reader(RtpPayloadFormat payloadFormat) { @@ -81,6 +84,9 @@ public RtpH264Reader(RtpPayloadFormat payloadFormat) { fuScratchBuffer = new ParsableByteArray(); firstReceivedTimestamp = C.TIME_UNSET; previousSequenceNumber = C.INDEX_UNSET; + previousTimestamp = C.TIME_UNSET; + isCurrentAccessUnitCorrupted = false; + isProcessingFragmentationUnit = false; } @Override @@ -96,52 +102,72 @@ public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {} @Override public void consume(ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) throws ParserException { - + checkNotNull(trackOutput); int rtpH264PacketMode; - try { - // RFC6184 Section 5.6, 5.7 and 5.8. - rtpH264PacketMode = data.getData()[0] & 0x1F; - } catch (IndexOutOfBoundsException e) { - throw ParserException.createForMalformedManifest(/* message= */ null, e); + + // 1. Detect AU Boundary by timestamp change. + if (previousTimestamp != C.TIME_UNSET && timestamp != previousTimestamp) { + resetReaderStateForNewAccessUnit(); } - checkNotNull(trackOutput); + // 2. Handle Packet Processing. + if (!isCurrentAccessUnitCorrupted) { + try { + // RFC6184 Section 5.6, 5.7 and 5.8. + rtpH264PacketMode = data.getData()[0] & 0x1F; + } catch (IndexOutOfBoundsException e) { + throw ParserException.createForMalformedManifest(/* message= */ null, e); + } - if (rtpH264PacketMode != RTP_PACKET_TYPE_FU_A) { - resetReaderStateForNewNalUnit(); - } + if (isProcessingFragmentationUnit && rtpH264PacketMode != RTP_PACKET_TYPE_FU_A) { + isCurrentAccessUnitCorrupted = true; + } - if (rtpH264PacketMode > 0 && rtpH264PacketMode < 24) { - processSingleNalUnitPacket(data); - } else if (rtpH264PacketMode == RTP_PACKET_TYPE_STAP_A) { - processSingleTimeAggregationPacket(data); - } else if (rtpH264PacketMode == RTP_PACKET_TYPE_FU_A) { - processFragmentationUnitPacket(data, sequenceNumber); - } else { - throw ParserException.createForMalformedManifest( - String.format("RTP H264 packetization mode [%d] not supported.", rtpH264PacketMode), - /* cause= */ null); + if (!isCurrentAccessUnitCorrupted) { + if (rtpH264PacketMode > 0 && rtpH264PacketMode < 24) { + processSingleNalUnitPacket(data); + } else if (rtpH264PacketMode == RTP_PACKET_TYPE_STAP_A) { + processSingleTimeAggregationPacket(data); + } else if (rtpH264PacketMode == RTP_PACKET_TYPE_FU_A) { + processFragmentationUnitPacket(data, sequenceNumber); + } else { + throw ParserException.createForMalformedManifest( + Util.formatInvariant( + "RTP H264 packetization mode [%d] not supported.", rtpH264PacketMode), + /* cause= */ null); + } + } } + // 3. Metadata and State Update. if (firstReceivedTimestamp == C.TIME_UNSET) { firstReceivedTimestamp = timestamp; } - if (rtpMarker && !discardPacketsUntilNextNalUnit) { - long timeUs = - toSampleTimeUs( - startTimeOffsetUs, timestamp, firstReceivedTimestamp, MEDIA_CLOCK_FREQUENCY); - trackOutput.sampleMetadata( - timeUs, bufferFlags, fragmentedSampleSizeBytes, /* offset= */ 0, /* cryptoData= */ null); + if (rtpMarker) { + if (!isCurrentAccessUnitCorrupted) { + long timeUs = + toSampleTimeUs( + startTimeOffsetUs, timestamp, firstReceivedTimestamp, MEDIA_CLOCK_FREQUENCY); + trackOutput.sampleMetadata( + timeUs, + bufferFlags, + fragmentedSampleSizeBytes, + /* offset= */ 0, + /* cryptoData= */ null); + } + resetReaderStateForNewAccessUnit(); } + previousTimestamp = timestamp; previousSequenceNumber = sequenceNumber; } @Override public void seek(long nextRtpTimestamp, long timeUs) { firstReceivedTimestamp = nextRtpTimestamp; - fragmentedSampleSizeBytes = 0; + previousTimestamp = C.TIME_UNSET; + resetReaderStateForNewAccessUnit(); startTimeOffsetUs = timeUs; } @@ -259,7 +285,12 @@ private void processFragmentationUnitPacket(ParsableByteArray data, int packetSe boolean isLastFuPacket = (fuHeader & 0x40) > 0; if (isFirstFuPacket) { - resetReaderStateForNewNalUnit(); + if (isProcessingFragmentationUnit) { + // Interruption: A new FU started before the previous one finished. + isCurrentAccessUnitCorrupted = true; + fragmentedSampleSizeBytes = 0; + } + isProcessingFragmentationUnit = true; // Prepends starter code. fragmentedSampleSizeBytes += writeStartCode(); @@ -269,13 +300,13 @@ private void processFragmentationUnitPacket(ParsableByteArray data, int packetSe fuScratchBuffer.reset(data.getData()); fuScratchBuffer.setPosition(1); } else { - if (discardPacketsUntilNextNalUnit) { + if (isCurrentAccessUnitCorrupted) { return; } // Check that this packet is in the sequence of the previous packet. int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); if (packetSequenceNumber != expectedSequenceNumber) { - discardPacketsUntilNextNalUnit = true; + isCurrentAccessUnitCorrupted = true; Log.w( TAG, Util.formatInvariant( @@ -295,10 +326,18 @@ private void processFragmentationUnitPacket(ParsableByteArray data, int packetSe fragmentedSampleSizeBytes += fragmentSize; if (isLastFuPacket) { + isProcessingFragmentationUnit = false; bufferFlags = getBufferFlagsFromNalType(nalHeader & 0x1F); } } + private void resetReaderStateForNewAccessUnit() { + isCurrentAccessUnitCorrupted = false; + isProcessingFragmentationUnit = false; + fragmentedSampleSizeBytes = 0; + bufferFlags = 0; + } + private int writeStartCode() { nalStartCodeArray.setPosition(/* position= */ 0); int bytesWritten = nalStartCodeArray.bytesLeft(); @@ -306,11 +345,6 @@ private int writeStartCode() { return bytesWritten; } - private void resetReaderStateForNewNalUnit() { - discardPacketsUntilNextNalUnit = false; - fragmentedSampleSizeBytes = 0; - } - private static @C.BufferFlags int getBufferFlagsFromNalType(int nalType) { return nalType == NAL_UNIT_TYPE_IDR ? C.BUFFER_FLAG_KEY_FRAME : 0; } 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 58254081f94..bef5e5cb0ef 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 @@ -64,6 +64,7 @@ private @C.BufferFlags int bufferFlags; private long firstReceivedTimestamp; private int previousSequenceNumber; + private long previousTimestamp; /** The combined size of a sample that is fragmented into multiple RTP packets. */ private int fragmentedSampleSizeBytes; @@ -74,7 +75,9 @@ * If a Fragmentation Unit is lost then following units corresponding to the same NAL unit should * be discarded (RFC7798 Section 4.4.3). */ - boolean discardPacketsUntilNextNalUnit; + private boolean isCurrentAccessUnitCorrupted; + + private boolean isProcessingFragmentationUnit; /** Creates an instance. */ public RtpH265Reader(RtpPayloadFormat payloadFormat) { @@ -83,6 +86,9 @@ public RtpH265Reader(RtpPayloadFormat payloadFormat) { this.payloadFormat = payloadFormat; firstReceivedTimestamp = C.TIME_UNSET; previousSequenceNumber = C.INDEX_UNSET; + previousTimestamp = C.TIME_UNSET; + isCurrentAccessUnitCorrupted = false; + isProcessingFragmentationUnit = false; } @Override @@ -100,46 +106,67 @@ public void consume(ParsableByteArray data, long timestamp, int sequenceNumber, if (data.getData().length == 0) { throw ParserException.createForMalformedManifest("Empty RTP data packet.", /* cause= */ null); } - // NAL Unit Header.type (RFC7798 Section 1.1.4). - int payloadType = (data.getData()[0] >> 1) & 0x3F; - checkNotNull(trackOutput); - if (payloadType != RTP_PACKET_TYPE_FU) { - resetReaderStateForNewNalUnit(); + // 1. Detect AU Boundary by timestamp change. + if (previousTimestamp != C.TIME_UNSET && timestamp != previousTimestamp) { + resetReaderStateForNewAccessUnit(); } - if (payloadType >= 0 && payloadType < RTP_PACKET_TYPE_AP) { - processSingleNalUnitPacket(data); - } else if (payloadType == RTP_PACKET_TYPE_AP) { - processAggregationPacket(data); - } else if (payloadType == RTP_PACKET_TYPE_FU) { - processFragmentationUnitPacket(data, sequenceNumber); - } else { - throw ParserException.createForMalformedManifest( - String.format("RTP H265 payload type [%d] not supported.", payloadType), - /* cause= */ null); + // 2. Handle Packet Processing. + if (!isCurrentAccessUnitCorrupted) { + // NAL Unit Header.type (RFC7798 Section 1.1.4). + int payloadType = (data.getData()[0] >> 1) & 0x3F; + + // If we were processing an FU and get a non-FU packet, the FU was interrupted/corrupted. + if (isProcessingFragmentationUnit && payloadType != RTP_PACKET_TYPE_FU) { + isCurrentAccessUnitCorrupted = true; + } + + if (!isCurrentAccessUnitCorrupted) { + if (payloadType >= 0 && payloadType < RTP_PACKET_TYPE_AP) { + processSingleNalUnitPacket(data); + } else if (payloadType == RTP_PACKET_TYPE_AP) { + processAggregationPacket(data); + } else if (payloadType == RTP_PACKET_TYPE_FU) { + processFragmentationUnitPacket(data, sequenceNumber); + } else { + throw ParserException.createForMalformedManifest( + Util.formatInvariant("RTP H265 payload type [%d] not supported.", payloadType), + /* cause= */ null); + } + } } + // 3. Metadata and State Update. if (firstReceivedTimestamp == C.TIME_UNSET) { firstReceivedTimestamp = timestamp; } - if (rtpMarker && !discardPacketsUntilNextNalUnit) { - long timeUs = - toSampleTimeUs( - startTimeOffsetUs, timestamp, firstReceivedTimestamp, MEDIA_CLOCK_FREQUENCY); - trackOutput.sampleMetadata( - timeUs, bufferFlags, fragmentedSampleSizeBytes, /* offset= */ 0, /* cryptoData= */ null); + if (rtpMarker) { + if (!isCurrentAccessUnitCorrupted) { + long timeUs = + toSampleTimeUs( + startTimeOffsetUs, timestamp, firstReceivedTimestamp, MEDIA_CLOCK_FREQUENCY); + trackOutput.sampleMetadata( + timeUs, + bufferFlags, + fragmentedSampleSizeBytes, + /* offset= */ 0, + /* cryptoData= */ null); + } + resetReaderStateForNewAccessUnit(); } + previousTimestamp = timestamp; previousSequenceNumber = sequenceNumber; } @Override public void seek(long nextRtpTimestamp, long timeUs) { firstReceivedTimestamp = nextRtpTimestamp; - fragmentedSampleSizeBytes = 0; + previousTimestamp = C.TIME_UNSET; + resetReaderStateForNewAccessUnit(); startTimeOffsetUs = timeUs; } @@ -286,7 +313,12 @@ private void processFragmentationUnitPacket(ParsableByteArray data, int packetSe boolean isLastFuPacket = (fuHeader & 0x40) > 0; if (isFirstFuPacket) { - resetReaderStateForNewNalUnit(); + if (isProcessingFragmentationUnit) { + // Interruption: A new FU started before the previous one finished. + isCurrentAccessUnitCorrupted = true; + fragmentedSampleSizeBytes = 0; + } + isProcessingFragmentationUnit = true; // Prepends starter code. fragmentedSampleSizeBytes += writeStartCode(); @@ -300,13 +332,13 @@ private void processFragmentationUnitPacket(ParsableByteArray data, int packetSe fuScratchBuffer.getData()[2] = (byte) tid; fuScratchBuffer.setPosition(1); } else { - if (discardPacketsUntilNextNalUnit) { + if (isCurrentAccessUnitCorrupted) { return; } // Check that this packet is in the sequence of the previous packet. int expectedSequenceNumber = (previousSequenceNumber + 1) % RtpPacket.MAX_SEQUENCE_NUMBER; if (packetSequenceNumber != expectedSequenceNumber) { - discardPacketsUntilNextNalUnit = true; + isCurrentAccessUnitCorrupted = true; Log.w( TAG, Util.formatInvariant( @@ -326,10 +358,18 @@ private void processFragmentationUnitPacket(ParsableByteArray data, int packetSe fragmentedSampleSizeBytes += fragmentSize; if (isLastFuPacket) { + isProcessingFragmentationUnit = false; bufferFlags = getBufferFlagsFromNalType(nalUnitType); } } + private void resetReaderStateForNewAccessUnit() { + isCurrentAccessUnitCorrupted = false; + isProcessingFragmentationUnit = false; + fragmentedSampleSizeBytes = 0; + bufferFlags = 0; + } + private int writeStartCode() { nalStartCodeArray.setPosition(/* position= */ 0); int bytesWritten = nalStartCodeArray.bytesLeft(); @@ -337,11 +377,6 @@ private int writeStartCode() { return bytesWritten; } - private void resetReaderStateForNewNalUnit() { - discardPacketsUntilNextNalUnit = false; - fragmentedSampleSizeBytes = 0; - } - private static @C.BufferFlags int getBufferFlagsFromNalType(int nalType) { return (nalType == NAL_IDR_W_RADL || nalType == NAL_IDR_N_LP) ? C.BUFFER_FLAG_KEY_FRAME : 0; } diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH264ReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH264ReaderTest.java new file mode 100644 index 00000000000..1ac2f6093a6 --- /dev/null +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH264ReaderTest.java @@ -0,0 +1,332 @@ +/* + * Copyright 2026 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 com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.ParserException; +import androidx.media3.common.util.ParsableByteArray; +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.media3.test.utils.ImmutableByteArray; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link RtpH264Reader}. */ +@RunWith(AndroidJUnit4.class) +public class RtpH264ReaderTest { + + private static final long MEDIA_CLOCK_FREQUENCY = 90_000; + private static final int PACKET_SEQUENCE_NUMBER = 12345; + private static final long STAP_A_PACKET_RTP_TIMESTAMP = 9_000_000; + private static final ImmutableByteArray NALU_START_CODE = + ImmutableByteArray.copyOf(NalUnitUtil.NAL_START_CODE); + + // H264 STAP-A Header: F=0, NRI=3, Type=24 (0x78) + private static final ImmutableByteArray STAP_A_HEADER = ImmutableByteArray.ofHexString("78"); + // NALU 1: Header(1) + Payload(10) = 11 bytes (0x0b) + private static final ImmutableByteArray NALU_1_LENGTH = ImmutableByteArray.ofHexString("000b"); + private static final ImmutableByteArray NALU_1_HEADER = ImmutableByteArray.ofHexString("41"); + private static final ImmutableByteArray NALU_1_PAYLOAD = + ImmutableByteArray.ofHexString("0102030405060708090a"); + private static final ImmutableByteArray NALU_1_START_DELIMITED = + ImmutableByteArray.concat(NALU_START_CODE, NALU_1_HEADER, NALU_1_PAYLOAD); + + // NALU 2: Header(1) + Payload(12) = 13 bytes (0x0d) + private static final ImmutableByteArray NALU_2_LENGTH = ImmutableByteArray.ofHexString("000d"); + private static final ImmutableByteArray NALU_2_HEADER = ImmutableByteArray.ofHexString("65"); + private static final ImmutableByteArray NALU_2_PAYLOAD = + ImmutableByteArray.ofHexString("1112131415161718191a1b1c"); + private static final ImmutableByteArray NALU_2_START_DELIMITED = + ImmutableByteArray.concat(NALU_START_CODE, NALU_2_HEADER, NALU_2_PAYLOAD); + + private static final RtpPacket VALID_STAP_A_PACKET = + createAggregationPacket( + PACKET_SEQUENCE_NUMBER, + STAP_A_PACKET_RTP_TIMESTAMP, + NALU_1_LENGTH, + NALU_1_HEADER, + NALU_1_PAYLOAD, + NALU_2_LENGTH, + NALU_2_HEADER, + NALU_2_PAYLOAD); + + private static final RtpPayloadFormat H264_FORMAT = + new RtpPayloadFormat( + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setWidth(1920) + .setHeight(1080) + .build(), + /* rtpPayloadType= */ 96, + /* clockRate= */ (int) MEDIA_CLOCK_FREQUENCY, + /* fmtpParameters= */ ImmutableMap.of("packetization-mode", "1"), + RtpPayloadFormat.RTP_MEDIA_H264); + + private FakeExtractorOutput extractorOutput; + private RtpH264Reader rtpH264Reader; + + @Before + public void setUp() { + extractorOutput = new FakeExtractorOutput(); + rtpH264Reader = new RtpH264Reader(H264_FORMAT); + } + + @Test + public void consume_unsupportedPacketizationMode_throwsParserException() { + // Packetization mode 31 (0x1F) is reserved/unsupported in RFC6184. + RtpPacket packet = + new RtpPacket.Builder() + .setTimestamp(9000) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER) + .setPayloadData(new byte[] {(byte) 0x1F}) + .build(); + rtpH264Reader.createTracks(extractorOutput, /* trackId= */ 0); + + assertThrows(ParserException.class, () -> consume(rtpH264Reader, packet)); + } + + @Test + public void consume_validPackets() throws ParserException { + rtpH264Reader.createTracks(extractorOutput, /* trackId= */ 0); + rtpH264Reader.onReceivingFirstPacket( + VALID_STAP_A_PACKET.timestamp, VALID_STAP_A_PACKET.sequenceNumber); + + consume(rtpH264Reader, VALID_STAP_A_PACKET); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(1); + assertThat(trackOutput.getSampleData(0)) + .isEqualTo( + ImmutableByteArray.concat(NALU_1_START_DELIMITED, NALU_2_START_DELIMITED).toArray()); + assertThat(trackOutput.getSampleData(0).length).isEqualTo(32); + } + + @Test + public void consume_fragmentationUnitContinuationAtAccessUnitStart_discardsWholeSample() + throws ParserException { + long auTimestamp = 9_000_000; + // Packet 1: FU-A Continuation (NOT a start packet, Header 0x05 -> S=0, E=0). + RtpPacket fuContinuation = + createFragmentedPacket( + PACKET_SEQUENCE_NUMBER, + auTimestamp, + /* marker= */ false, + ImmutableByteArray.ofHexString("05"), + ImmutableByteArray.ofHexString("010203")); + // Packet 2: Valid Single NALU in the same Access Unit. + RtpPacket singleNalu = + new RtpPacket.Builder() + .setTimestamp(auTimestamp) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER + 1) + .setMarker(true) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + rtpH264Reader.createTracks(extractorOutput, /* trackId= */ 0); + + consume(rtpH264Reader, fuContinuation); + consume(rtpH264Reader, singleNalu); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(0); + } + + @Test + public void consume_multipleSingleNalUnitsInSingleAccessUnit_reportsCorrectCombinedSize() + throws ParserException { + long auTimestamp = 9_000_000; + // Packet 1: Single NAL unit, marker = false. + RtpPacket packet1 = + new RtpPacket.Builder() + .setTimestamp(auTimestamp) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER) + .setMarker(false) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + // Packet 2: Single NAL unit, marker = true. + RtpPacket packet2 = + new RtpPacket.Builder() + .setTimestamp(auTimestamp) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER + 1) + .setMarker(true) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_2_HEADER, NALU_2_PAYLOAD)) + .build(); + rtpH264Reader.createTracks(extractorOutput, /* trackId= */ 0); + + consume(rtpH264Reader, packet1); + consume(rtpH264Reader, packet2); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(1); + int expectedSize = NALU_1_START_DELIMITED.length() + NALU_2_START_DELIMITED.length(); + assertThat(trackOutput.getSampleData(0).length).isEqualTo(expectedSize); + } + + @Test + public void consume_interruptedFragmentationUnitByNewFragmentationUnit_discardsSample() + throws ParserException { + long auTimestamp = 9_000_000; + // Packet 1: FU-A Start for NALU A. + RtpPacket fuStartA = + createFragmentedPacket( + /* sequenceNumber= */ 100, + auTimestamp, + /* marker= */ false, + ImmutableByteArray.ofHexString("85"), // S=1, E=0, NALU type 5 + ImmutableByteArray.ofHexString("0102")); + // Packet 2: Another FU-A Start for NALU B (Interruption). + RtpPacket fuStartB = + createFragmentedPacket( + /* sequenceNumber= */ 101, + auTimestamp, + /* marker= */ true, + ImmutableByteArray.ofHexString("85"), + ImmutableByteArray.ofHexString("0304")); + rtpH264Reader.createTracks(extractorOutput, /* trackId= */ 0); + + consume(rtpH264Reader, fuStartA); + consume(rtpH264Reader, fuStartB); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + // Should discard the entire AU because it's corrupted. + assertThat(trackOutput.getSampleCount()).isEqualTo(0); + } + + @Test + public void consume_newAccessUnit_resetsStateCorrectly() throws ParserException { + RtpPacket packet1 = + new RtpPacket.Builder() + .setTimestamp(9000) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER) + .setMarker(false) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + RtpPacket packet2 = + new RtpPacket.Builder() + .setTimestamp(9000) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER + 1) + .setMarker(true) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + RtpPacket fuContinuation = + createFragmentedPacket( + PACKET_SEQUENCE_NUMBER + 3, + 18000, + /* marker= */ false, + ImmutableByteArray.ofHexString("05"), + ImmutableByteArray.ofHexString("010203")); + RtpPacket packet3 = + new RtpPacket.Builder() + .setTimestamp(27000) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER + 4) + .setMarker(true) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + rtpH264Reader.createTracks(extractorOutput, /* trackId= */ 0); + + // Access Unit 1: Two packets. + consume(rtpH264Reader, packet1); + consume(rtpH264Reader, packet2); + // Access Unit 2: Missing packets. + consume(rtpH264Reader, fuContinuation); + // Access Unit 3: One packet with different timestamp. + consume(rtpH264Reader, packet3); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + // Verify sizes are independent (not accumulated) + assertThat(trackOutput.getSampleData(1).length).isEqualTo(NALU_1_START_DELIMITED.length()); + } + + @Test + public void seek_resetsStateCorrectly() throws ParserException { + long seekTimeUs = 1_000_000; + RtpPacket fuContinuation = + createFragmentedPacket( + PACKET_SEQUENCE_NUMBER, + 9000, + /* marker= */ false, + ImmutableByteArray.ofHexString("05"), // S=0, E=0 + ImmutableByteArray.ofHexString("010203")); + RtpPacket singleNalu = + new RtpPacket.Builder() + .setTimestamp(18000) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER + 1) + .setMarker(true) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + rtpH264Reader.createTracks(extractorOutput, /* trackId= */ 0); + consume(rtpH264Reader, fuContinuation); + + rtpH264Reader.seek(/* nextRtpTimestamp= */ 18000, /* timeUs= */ seekTimeUs); + consume(rtpH264Reader, singleNalu); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(1); + assertThat(trackOutput.getSampleData(0).length).isEqualTo(NALU_1_START_DELIMITED.length()); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(seekTimeUs); + } + + private static RtpPacket createAggregationPacket( + int sequenceNumber, long timeStamp, ImmutableByteArray... nalUnits) { + ImmutableByteArray.Builder payloadData = new ImmutableByteArray.Builder().addAll(STAP_A_HEADER); + for (int i = 0; i < nalUnits.length; i += 2) { + payloadData.addAll(nalUnits[i]); // Length + payloadData.addAll(nalUnits[i + 1]); // Header + Payload + } + return new RtpPacket.Builder() + .setTimestamp(timeStamp) + .setSequenceNumber(sequenceNumber) + .setMarker(true) + .setPayloadData(payloadData.build().toArray()) + .build(); + } + + private static RtpPacket createFragmentedPacket( + int sequenceNumber, + long timeStamp, + boolean marker, + ImmutableByteArray fuHeader, + ImmutableByteArray payload) { + return new RtpPacket.Builder() + .setTimestamp(timeStamp) + .setSequenceNumber(sequenceNumber) + .setMarker(marker) + .setPayloadData( + // FU-A Indicator: 0x7c + ImmutableByteArray.concat(ImmutableByteArray.ofHexString("7c"), fuHeader, payload) + .toArray()) + .build(); + } + + private static void consume(RtpH264Reader h264Reader, RtpPacket rtpPacket) + throws ParserException { + h264Reader.consume( + new ParsableByteArray(rtpPacket.payloadData), + rtpPacket.timestamp, + rtpPacket.sequenceNumber, + rtpPacket.marker); + } +} 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 index 7418873b61a..d7d3fe8227a 100644 --- 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 @@ -218,6 +218,21 @@ public void setUp() { rtpH265Reader = new RtpH265Reader(H265_FORMAT); } + @Test + public void consume_unsupportedPayloadType_throwsParserException() { + // Payload type 63 (0x3F) is reserved/unsupported in RFC7798. + // The type is in bits 1-6 of the first byte: (0x3F << 1) = 0x7E. + RtpPacket packet = + new RtpPacket.Builder() + .setTimestamp(9000) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER) + .setPayloadData(new byte[] {(byte) 0x7E, 0x01}) + .build(); + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + + assertThrows(ParserException.class, () -> consume(rtpH265Reader, packet)); + } + @Test public void consume_validPackets() throws ParserException { rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); @@ -389,6 +404,207 @@ public void consume_mixedWithMissingEndFragmentationPacketAndValidSingleNALU() assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(naluPacket2PresentationTimestampUs); } + @Test + public void consume_multipleSingleNalUnitsInSingleAccessUnit_reportsCorrectCombinedSize() + throws ParserException { + // Both packets share the same timestamp, forming a single Access Unit. + long accessUnitTimestamp = 9_000_000; + // Packet 1: Single NAL unit, marker = false (more NAL units follow in this Access Unit). + RtpPacket packet1 = + new RtpPacket.Builder() + .setTimestamp(accessUnitTimestamp) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER) + .setMarker(false) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + // Packet 2: Single NAL unit, marker = true (last packet in the Access Unit). + RtpPacket packet2 = + new RtpPacket.Builder() + .setTimestamp(accessUnitTimestamp) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER + 1) + .setMarker(true) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_2_HEADER, NALU_2_PAYLOAD)) + .build(); + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + rtpH265Reader.onReceivingFirstPacket(packet1.timestamp, packet1.sequenceNumber); + + consume(rtpH265Reader, packet1); + consume(rtpH265Reader, packet2); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(1); + int expectedSize = NALU_1_START_DELIMITED.length() + NALU_2_START_DELIMITED.length(); + assertThat(trackOutput.getSampleData(0).length).isEqualTo(expectedSize); + assertThat(trackOutput.getSampleData(0)) + .isEqualTo( + ImmutableByteArray.concat(NALU_1_START_DELIMITED, NALU_2_START_DELIMITED).toArray()); + } + + @Test + public void consume_multipleMixedNalUnitsInSingleAccessUnit_aggregatesCorrectly() + throws ParserException { + long accessUnitTimestamp = 9_000_000; + RtpPacket fu1Packet1 = + createFragmentedPacket( + /* sequenceNumber= */ 100, + accessUnitTimestamp, + /* marker= */ false, + FU_1_PACKET_1_FU_HEADER, + FU_1_PACKET_1_NALU_PAYLOAD); + RtpPacket fu1Packet2 = + createFragmentedPacket( + /* sequenceNumber= */ 101, + accessUnitTimestamp, + /* marker= */ false, + FU_1_PACKET_2_FU_HEADER, + FU_1_PACKET_2_NALU_PAYLOAD); + RtpPacket fu1Packet3 = + createFragmentedPacket( + /* sequenceNumber= */ 102, + accessUnitTimestamp, + /* marker= */ false, + FU_1_PACKET_3_FU_HEADER, + FU_1_PACKET_3_NALU_PAYLOAD); + RtpPacket singleNaluPacket = + new RtpPacket.Builder() + .setTimestamp(accessUnitTimestamp) + .setSequenceNumber(103) + .setMarker(true) // Only the final packet in the AU has the marker + .setPayloadData(ImmutableByteArray.concatToArray(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + + consume(rtpH265Reader, fu1Packet1); + consume(rtpH265Reader, fu1Packet2); + consume(rtpH265Reader, fu1Packet3); + consume(rtpH265Reader, singleNaluPacket); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(1); + int expectedSize = NALU_1_START_DELIMITED.length() + NALU_1_START_DELIMITED.length(); + assertThat(trackOutput.getSampleData(0).length).isEqualTo(expectedSize); + } + + @Test + public void + consume_corruptedFragmentationUnitFollowedByValidSingleNALUInSameAccessUnit_discardsWholeSample() + throws ParserException { + long accessUnitTimestamp = 9_000_000; + // Packet 1: FU-A Start (TS 100). + RtpPacket fuStart = + createFragmentedPacket( + PACKET_SEQUENCE_NUMBER, + accessUnitTimestamp, + /* marker= */ false, + FU_1_PACKET_1_FU_HEADER, + FU_1_PACKET_1_NALU_PAYLOAD); + // Packet 2: MISSING (Simulated loss) + // Packet 3: Valid Single NALU (TS 100, Marker True). + RtpPacket singleNalu = + new RtpPacket.Builder() + .setTimestamp(accessUnitTimestamp) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER + 2) // Gap of 1 + .setMarker(true) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + rtpH265Reader.onReceivingFirstPacket(fuStart.timestamp, fuStart.sequenceNumber); + + consume(rtpH265Reader, fuStart); + consume(rtpH265Reader, singleNalu); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(0); + } + + @Test + public void consume_fragmentationUnitContinuationAtAccessUnitStart_discardsWholeSample() + throws ParserException { + long auTimestamp = 9_000_000; + // Packet 1: Fragmentation Unit Continuation (NOT a start packet) at the very beginning of the + // Access Unit. + RtpPacket fuContinuation = + createFragmentedPacket( + PACKET_SEQUENCE_NUMBER, + auTimestamp, + /* marker= */ false, + FU_1_PACKET_2_FU_HEADER, // '0x20' -> S=0, E=0 + FU_1_PACKET_2_NALU_PAYLOAD); + + // Packet 2: Valid Single NALU in the same AU (Marker = True) + RtpPacket singleNalu = + new RtpPacket.Builder() + .setTimestamp(auTimestamp) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER + 1) + .setMarker(true) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + consume(rtpH265Reader, fuContinuation); + consume(rtpH265Reader, singleNalu); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(0); + } + + @Test + public void consume_timestampRollover_resetsCorrectly() throws ParserException { + long maxTimestamp = 0xFFFFFFFFL; + long nextTimestamp = 0x00000000L; + RtpPacket p1 = + createFragmentedPacket( + /* sequenceNumber= */ 100, + maxTimestamp, + /* marker= */ true, + FU_1_PACKET_1_FU_HEADER, + FU_1_PACKET_1_NALU_PAYLOAD); + RtpPacket p2 = + createFragmentedPacket( + /* sequenceNumber= */ 101, + nextTimestamp, + /* marker= */ true, + FU_1_PACKET_1_FU_HEADER, + FU_1_PACKET_1_NALU_PAYLOAD); + + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + consume(rtpH265Reader, p1); + consume(rtpH265Reader, p2); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleTimeUs(1)).isGreaterThan(trackOutput.getSampleTimeUs(0)); + } + + @Test + public void seek_resetsStateCorrectly() throws ParserException { + long seekTimeUs = 1_000_000; + RtpPacket fuContinuation = + createFragmentedPacket( + PACKET_SEQUENCE_NUMBER, + 9000, + /* marker= */ false, + FU_1_PACKET_2_FU_HEADER, // S=0, E=0 + FU_1_PACKET_2_NALU_PAYLOAD); + RtpPacket singleNalu = + new RtpPacket.Builder() + .setTimestamp(18000) + .setSequenceNumber(PACKET_SEQUENCE_NUMBER + 1) + .setMarker(true) + .setPayloadData(ImmutableByteArray.concatToArray(NALU_1_HEADER, NALU_1_PAYLOAD)) + .build(); + rtpH265Reader.createTracks(extractorOutput, /* trackId= */ 0); + consume(rtpH265Reader, fuContinuation); + + rtpH265Reader.seek(/* nextRtpTimestamp= */ 18000, /* timeUs= */ seekTimeUs); + consume(rtpH265Reader, singleNalu); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(1); + assertThat(trackOutput.getSampleData(0).length).isEqualTo(NALU_1_START_DELIMITED.length()); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(seekTimeUs); + } + private static RtpPacket createAggregationPacket( int sequenceNumber, long timeStamp, ImmutableByteArray... nalUnits) { ImmutableByteArray.Builder payloadData = diff --git a/libraries/exoplayer_smoothstreaming/build.gradle b/libraries/exoplayer_smoothstreaming/build.gradle index fe58a2bfb92..3f21447a29a 100644 --- a/libraries/exoplayer_smoothstreaming/build.gradle +++ b/libraries/exoplayer_smoothstreaming/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.exoplayer.smoothstreaming' diff --git a/libraries/exoplayer_workmanager/build.gradle b/libraries/exoplayer_workmanager/build.gradle index 4984b3e6f72..aa1033a9495 100644 --- a/libraries/exoplayer_workmanager/build.gradle +++ b/libraries/exoplayer_workmanager/build.gradle @@ -13,7 +13,8 @@ * 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.exoplayer.workmanager' diff --git a/libraries/extractor/build.gradle b/libraries/extractor/build.gradle index 67aed348c3d..d0295613367 100644 --- a/libraries/extractor/build.gradle +++ b/libraries/extractor/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.extractor' diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Extractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Extractor.java index 5c3a910d12f..57424dc9c97 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Extractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/Mp3Extractor.java @@ -27,7 +27,6 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; -import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -595,24 +594,13 @@ private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException { } xingMetadata = xingFrame.getMetadata(); long startPosition = input.getPosition(); - if (input.getLength() != C.LENGTH_UNSET - && xingFrame.dataSize != C.LENGTH_UNSET - && input.getLength() != startPosition + xingFrame.dataSize) { - Log.i( - TAG, - "Data size mismatch between stream (" - + input.getLength() - + ") and Xing frame (" - + (startPosition + xingFrame.dataSize) - + "), using Xing value."); - } input.skipFully(synchronizedHeader.frameSize); // An Xing frame indicates the file is VBR (so we have to use the seek header for seeking) // while an Info header indicates the file is CBR, in which case ConstantBitrateSeeker will // give more accurate seeking than the low-resolution seek table in the Info header. We can // still use the length from the Info frame if we don't know the stream length directly. if (seekHeader == SEEK_HEADER_XING) { - seeker = XingSeeker.create(xingFrame, startPosition); + seeker = XingSeeker.create(xingFrame, startPosition, input.getLength()); } else { // seekHeader == SEEK_HEADER_INFO seeker = getConstantBitrateSeeker(startPosition, xingFrame, input.getLength()); } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java index 612e9b240f7..3fe97490def 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp3/XingSeeker.java @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import androidx.media3.common.C; +import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import androidx.media3.extractor.SeekPoint; @@ -34,21 +35,38 @@ * * @param xingFrame The parsed Xing data from this audio frame. * @param position The position of the start of this frame in the stream. + * @param streamLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ @Nullable - public static XingSeeker create(XingFrame xingFrame, long position) { + public static XingSeeker create(XingFrame xingFrame, long position, long streamLength) { long durationUs = xingFrame.computeDurationUs(); if (durationUs == C.TIME_UNSET) { return null; } + long dataSize; + if (xingFrame.dataSize != C.LENGTH_UNSET + && streamLength != C.LENGTH_UNSET + && position + xingFrame.dataSize != streamLength) { + long dataSizeFromStreamLength = streamLength - position; + Log.i( + TAG, + "Data size mismatch between stream (" + + dataSizeFromStreamLength + + ") and Xing frame (" + + xingFrame.dataSize + + "), using smaller value."); + dataSize = Math.min(xingFrame.dataSize, dataSizeFromStreamLength); + } else { + dataSize = xingFrame.dataSize; + } return new XingSeeker( position, xingFrame.header.frameSize, durationUs, xingFrame.header.bitrate, - xingFrame.dataSize, + dataSize, xingFrame.tableOfContents); } 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 0f750556e1a..8ace3db723b 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 @@ -2139,7 +2139,7 @@ private static void parseAudioSampleEntry( } else if (bitsPerSample == 32) { pcmEncoding = isBigEndian ? C.ENCODING_PCM_32BIT_BIG_ENDIAN : C.ENCODING_PCM_32BIT; } - } else if (bitsPerSample == 32) { + } else if (!isBigEndian && bitsPerSample == 32) { pcmEncoding = C.ENCODING_PCM_FLOAT; } parent.skipBytes(8); // constBytesPerAudioPacket, constLPCMFramesPerAudioPacket 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 fdc5075a0b8..283d7ff53ed 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 @@ -254,9 +254,9 @@ public static ExtractorsFactory newFactory(SubtitleParser.Factory subtitleParser // Whether extractorOutput.seekMap has been called. private boolean haveOutputSeekMap; - // Whether we've encountered and merged multiple sidx boxes with different start times and - // extractorOutput.seekMap has been called. - private boolean haveOutputSeekMapFromMultipleSidx; + // Whether the upfront forward scan for sidx boxes (triggered by FLAG_MERGE_FRAGMENTED_SIDX) has + // successfully completed and output the merged seek map. + private boolean upfrontSidxScanComplete; private long seekPositionBeforeSidxProcessing; @@ -540,7 +540,7 @@ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOExce seekPosition.position = seekPositionBeforeSidxProcessing; seekPositionBeforeSidxProcessing = C.INDEX_UNSET; extractorOutput.seekMap(chunkIndexMerger.merge()); - haveOutputSeekMapFromMultipleSidx = true; + upfrontSidxScanComplete = true; return Extractor.RESULT_SEEK; } else { reorderingBufferQueue.flush(); @@ -722,12 +722,14 @@ private void onLeafAtomRead(LeafBox leaf, ExtractorInput input) throws IOExcepti } else if (leaf.type == Mp4Box.TYPE_sidx) { Pair result = parseSidx(leaf.data, input.getPosition()); chunkIndexMerger.add(result.second); - if (!haveOutputSeekMap) { - segmentIndexEarliestPresentationTimeUs = result.first; - extractorOutput.seekMap(result.second); + segmentIndexEarliestPresentationTimeUs = result.first; + if (!upfrontSidxScanComplete) { + extractorOutput.seekMap( + chunkIndexMerger.size() == 1 ? result.second : chunkIndexMerger.merge()); haveOutputSeekMap = true; - } else if ((flags & FLAG_MERGE_FRAGMENTED_SIDX) != 0 - && !haveOutputSeekMapFromMultipleSidx + } + if ((flags & FLAG_MERGE_FRAGMENTED_SIDX) != 0 + && !upfrontSidxScanComplete && chunkIndexMerger.size() > 1) { seekPositionBeforeSidxProcessing = input.getPosition(); } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/MetadataUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/MetadataUtil.java index e620e22bb71..9fe818f1802 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/MetadataUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/MetadataUtil.java @@ -242,7 +242,12 @@ public static MdtaMetadataEntry parseMdtaMetadataEntryFromIlst( int dataSize = atomSize - 16; byte[] value = new byte[dataSize]; ilst.readBytes(value, 0, dataSize); - return new MdtaMetadataEntry(key, value, localeIndicator, typeIndicator); + try { + return new MdtaMetadataEntry(key, value, localeIndicator, typeIndicator); + } catch (Exception e) { + Log.w(TAG, "Failed to parse metadata entry with key: " + key); + break; + } } ilst.setPosition(atomPosition + atomSize); } 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 907173bc491..ef7b3a67d96 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 @@ -745,7 +745,8 @@ private void processMoovAtom(ContainerBox moov) throws ParserException { private static long findBestThumbnailPresentationTimeUs( TrackSampleTable sampleTable, long durationUs) { - if (!MimeTypes.isVideo(sampleTable.track.format.sampleMimeType)) { + if (!MimeTypes.isVideo(sampleTable.track.format.sampleMimeType) + || !sampleTable.hasSampleTableData()) { return C.TIME_UNSET; } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/TrackSampleTable.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/TrackSampleTable.java index c5f584e69f5..c58a0b9c440 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/TrackSampleTable.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/TrackSampleTable.java @@ -31,19 +31,22 @@ public final class TrackSampleTable { /** Number of samples. */ public final int sampleCount; - /** Sample offsets in bytes. */ + /** Sample offsets in bytes. Populated only if {@link #hasSampleTableData()} is {@code true}. */ public final long[] offsets; - /** Sample sizes in bytes. */ + /** Sample sizes in bytes. Populated only if {@link #hasSampleTableData()} is {@code true}. */ public final int[] sizes; /** Maximum sample size in {@link #sizes}. */ public final int maximumSize; - /** Sample timestamps in microseconds. */ + /** + * Sample timestamps in microseconds. Populated only if {@link #hasSampleTableData()} is {@code + * true}. + */ public final long[] timestampsUs; - /** Sample flags. */ + /** Sample flags. Populated only if {@link #hasSampleTableData()} is {@code true}. */ public final int[] flags; /** @@ -88,6 +91,17 @@ public TrackSampleTable( } } + /** + * Returns whether the per-sample mapping arrays (such as {@link #offsets}, {@link #sizes}, {@link + * #timestampsUs}, and {@link #flags}) are populated. + * + *

When {@link Mp4Extractor#FLAG_OMIT_TRACK_SAMPLE_TABLE} is set, this returns {@code false} + * while {@link #sampleCount} may still be greater than {@code 0}. + */ + public boolean hasSampleTableData() { + return timestampsUs.length > 0; + } + /** * Returns the sample index of the closest synchronization sample at or before the given * timestamp, if one is available. @@ -96,6 +110,9 @@ public TrackSampleTable( * @return Index of the synchronization sample, or {@link C#INDEX_UNSET} if none. */ public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) { + if (!hasSampleTableData()) { + return C.INDEX_UNSET; + } if (hasOnlySyncSamples) { return Util.binarySearchFloor( timestampsUs, timeUs, /* inclusive= */ true, /* stayInBounds= */ false); @@ -140,6 +157,9 @@ public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) { * @return index Index of the synchronization sample, or {@link C#INDEX_UNSET} if none. */ public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) { + if (!hasSampleTableData()) { + return C.INDEX_UNSET; + } if (hasOnlySyncSamples) { return Util.binarySearchCeil( timestampsUs, timeUs, /* inclusive= */ true, /* stayInBounds= */ false); diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/XingSeekerTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/XingSeekerTest.java index 3195428b7e8..c71614bc891 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/XingSeekerTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp3/XingSeekerTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; +import androidx.media3.common.C; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.Util; import androidx.media3.extractor.MpegAudioUtil; @@ -38,55 +39,66 @@ public final class XingSeekerTest { "00000007000008dd000e7919000205080a0d0f1214171a1c1e212426292c2e303336383b3d404245484a4c4f5254" + "575a5c5e616466696b6e707376787a7d808285878a8c8f929496999c9ea1a4a6a8abaeb0b3b5b8babdc0c2c4c7" + "cacccfd2d4d6d9dcdee1e3e6e8ebeef0f2f5f8fafd"); - private static final int XING_FRAME_POSITION = 157; + private static final XingFrame XING_FRAME = + createXingFrame(XING_FRAME_HEADER_DATA, XING_FRAME_PAYLOAD); - /** Data size, as encoded in {@link #XING_FRAME_PAYLOAD}. */ - private static final int DATA_SIZE_BYTES = 948505; + private static final int XING_FRAME_POSITION = 157; /** * Duration of the audio stream in microseconds, encoded as a frame count in {@link * #XING_FRAME_PAYLOAD}. */ - private static final int STREAM_DURATION_US = 59271814; + private static final int XING_STREAM_DURATION_US = 59271814; + + /** + * The position of the start of the audio data in the stream, as encoded by {@link #XING_FRAME}. + */ + private static final long XING_AUDIO_START_POSITION = + XING_FRAME_POSITION + XING_FRAME.header.frameSize; - /** The length of the stream in bytes. */ - private static final int STREAM_LENGTH = XING_FRAME_POSITION + DATA_SIZE_BYTES; + /** The position of the end of the audio data in the stream, as encoded by {@link #XING_FRAME}. */ + private static final long XING_AUDIO_END_POSITION = XING_FRAME_POSITION + XING_FRAME.dataSize; private XingSeeker seeker; - private XingSeeker seekerWithInputLength; - private int xingFrameSize; @Before public void setUp() throws Exception { - MpegAudioUtil.Header xingFrameHeader = new MpegAudioUtil.Header(); - xingFrameHeader.setForHeaderData(XING_FRAME_HEADER_DATA); seeker = XingSeeker.create( - XingFrame.parse(xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD)), - XING_FRAME_POSITION); - seekerWithInputLength = - XingSeeker.create( - XingFrame.parse(xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD)), - XING_FRAME_POSITION); - xingFrameSize = xingFrameHeader.frameSize; + XING_FRAME, + XING_FRAME_POSITION, + // Simulate 1000 bytes of non-MP3 junk at the end of the stream. + /* streamLength= */ XING_AUDIO_END_POSITION + 1000); } @Test public void getTimeUsBeforeFirstAudioFrame() { - assertThat(seeker.getTimeUs(-1)).isEqualTo(0); - assertThat(seekerWithInputLength.getTimeUs(-1)).isEqualTo(0); + assertThat(seeker.getTimeUs(XING_AUDIO_START_POSITION - 1)).isEqualTo(0); } @Test public void getTimeUsAtFirstAudioFrame() { - assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize)).isEqualTo(0); - assertThat(seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize)).isEqualTo(0); + assertThat(seeker.getTimeUs(XING_AUDIO_START_POSITION)).isEqualTo(0); } @Test public void getTimeUsAtEndOfStream() { - assertThat(seeker.getTimeUs(STREAM_LENGTH)).isEqualTo(STREAM_DURATION_US); - assertThat(seekerWithInputLength.getTimeUs(STREAM_LENGTH)).isEqualTo(STREAM_DURATION_US); + assertThat(seeker.getTimeUs(XING_AUDIO_END_POSITION)).isEqualTo(XING_STREAM_DURATION_US); + } + + // https://github.com/androidx/media/issues/3117#issuecomment-4046538506 + @Test + public void getTimeUsAtEndOfStream_xingLengthLongerThanStream() { + long streamLength = XING_AUDIO_END_POSITION - 1000; + XingSeeker seeker = XingSeeker.create(XING_FRAME, XING_FRAME_POSITION, streamLength); + assertThat(seeker.getTimeUs(streamLength)).isEqualTo(XING_STREAM_DURATION_US); + } + + @Test + public void getTimeUsAtEndOfStream_streamLengthNotKnown() { + XingSeeker seeker = + XingSeeker.create(XING_FRAME, XING_FRAME_POSITION, /* streamLength= */ C.LENGTH_UNSET); + assertThat(seeker.getTimeUs(XING_AUDIO_END_POSITION)).isEqualTo(XING_STREAM_DURATION_US); } @Test @@ -95,34 +107,86 @@ public void getSeekPointsAtStartOfStream() { SeekPoint seekPoint = seekPoints.first; assertThat(seekPoint).isEqualTo(seekPoints.second); assertThat(seekPoint.timeUs).isEqualTo(0); - assertThat(seekPoint.position).isEqualTo(XING_FRAME_POSITION + xingFrameSize); + assertThat(seekPoint.position).isEqualTo(XING_AUDIO_START_POSITION); } @Test public void getSeekPointsAtEndOfStream() { - SeekPoints seekPoints = seeker.getSeekPoints(STREAM_DURATION_US); + SeekPoints seekPoints = seeker.getSeekPoints(XING_STREAM_DURATION_US); + SeekPoint seekPoint = seekPoints.first; + assertThat(seekPoint).isEqualTo(seekPoints.second); + assertThat(seekPoint.timeUs).isEqualTo(XING_STREAM_DURATION_US); + assertThat(seekPoint.position).isEqualTo(XING_AUDIO_END_POSITION - 1); + } + + // https://github.com/androidx/media/issues/3117#issuecomment-4046538506 + @Test + public void getSeekPointsAtEndOfStream_xingLengthLongerThanStream() { + long streamLength = XING_AUDIO_END_POSITION - 1000; + XingSeeker seeker = XingSeeker.create(XING_FRAME, XING_FRAME_POSITION, streamLength); + SeekPoints seekPoints = seeker.getSeekPoints(XING_STREAM_DURATION_US); + SeekPoint seekPoint = seekPoints.first; + assertThat(seekPoint).isEqualTo(seekPoints.second); + assertThat(seekPoint.timeUs).isEqualTo(XING_STREAM_DURATION_US); + assertThat(seekPoint.position).isEqualTo(streamLength - 1); + } + + @Test + public void getSeekPointsAtEndOfStream_streamLengthNotKnown() { + XingSeeker seeker = + XingSeeker.create(XING_FRAME, XING_FRAME_POSITION, /* streamLength= */ C.LENGTH_UNSET); + SeekPoints seekPoints = seeker.getSeekPoints(XING_STREAM_DURATION_US); SeekPoint seekPoint = seekPoints.first; assertThat(seekPoint).isEqualTo(seekPoints.second); - assertThat(seekPoint.timeUs).isEqualTo(STREAM_DURATION_US); - assertThat(seekPoint.position).isEqualTo(STREAM_LENGTH - 1); + assertThat(seekPoint.timeUs).isEqualTo(XING_STREAM_DURATION_US); + assertThat(seekPoint.position).isEqualTo(XING_AUDIO_END_POSITION - 1); } @Test public void getTimeForAllPositions() { - for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) { - int position = XING_FRAME_POSITION + offset; - // Test seeker. + for (long position = XING_AUDIO_START_POSITION; + position < XING_AUDIO_END_POSITION; + position++) { long timeUs = seeker.getTimeUs(position); SeekPoints seekPoints = seeker.getSeekPoints(timeUs); SeekPoint seekPoint = seekPoints.first; assertThat(seekPoint).isEqualTo(seekPoints.second); assertThat(seekPoint.position).isEqualTo(position); - // Test seekerWithInputLength. - timeUs = seekerWithInputLength.getTimeUs(position); - seekPoints = seekerWithInputLength.getSeekPoints(timeUs); - seekPoint = seekPoints.first; + } + } + + // https://github.com/androidx/media/issues/3117#issuecomment-4046538506 + @Test + public void getTimeForAllPositions_xingLengthLongerThanStream() { + long streamLength = XING_AUDIO_END_POSITION - 1000; + XingSeeker seeker = XingSeeker.create(XING_FRAME, XING_FRAME_POSITION, streamLength); + for (long position = XING_AUDIO_START_POSITION; position < streamLength; position++) { + long timeUs = seeker.getTimeUs(position); + SeekPoints seekPoints = seeker.getSeekPoints(timeUs); + SeekPoint seekPoint = seekPoints.first; assertThat(seekPoint).isEqualTo(seekPoints.second); assertThat(seekPoint.position).isEqualTo(position); } } + + @Test + public void getTimeForAllPositions_streamLengthNotKnown() { + XingSeeker seeker = + XingSeeker.create(XING_FRAME, XING_FRAME_POSITION, /* streamLength= */ C.LENGTH_UNSET); + for (long position = XING_AUDIO_START_POSITION; + position < XING_AUDIO_END_POSITION; + position++) { + long timeUs = seeker.getTimeUs(position); + SeekPoints seekPoints = seeker.getSeekPoints(timeUs); + SeekPoint seekPoint = seekPoints.first; + assertThat(seekPoint).isEqualTo(seekPoints.second); + assertThat(seekPoint.position).isEqualTo(position); + } + } + + private static XingFrame createXingFrame(int header, byte[] payload) { + MpegAudioUtil.Header xingFrameHeader = new MpegAudioUtil.Header(); + xingFrameHeader.setForHeaderData(header); + return XingFrame.parse(xingFrameHeader, new ParsableByteArray(payload)); + } } diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorParameterizedTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorParameterizedTest.java index e420cfc9e6a..5102f87c329 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorParameterizedTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorParameterizedTest.java @@ -106,10 +106,7 @@ public void sampleSeekable_multipleSidx_noMerging() throws Exception { /* additionalEmsgTrackOutput= */ null), file, /* peekLimit= */ 700, - new ExtractorAsserts.AssertionConfig.Builder() - .setDumpFilesPrefix( - file.replaceFirst("media", "extractordumps") + ".no-merge-fragmented-sidx") - .build(), + new ExtractorAsserts.AssertionConfig.Builder().build(), simulationConfig); } @@ -127,10 +124,7 @@ public void sampleSeekable_multipleSidx_mergingEnabled() throws Exception { /* additionalEmsgTrackOutput= */ null), file, /* peekLimit= */ 700, - new ExtractorAsserts.AssertionConfig.Builder() - .setDumpFilesPrefix( - file.replaceFirst("media", "extractordumps") + ".merge-fragmented-sidx") - .build(), + new ExtractorAsserts.AssertionConfig.Builder().build(), simulationConfig); } diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorNonParameterizedTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorNonParameterizedTest.java index 60d3dd400e7..2ef4fa7695a 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorNonParameterizedTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorNonParameterizedTest.java @@ -214,6 +214,32 @@ public void extract_withOmitTrackSampleTableFlag_extractsCorrectMetadata() throw assertThat(output.trackOutputs.get(1).getSampleCount()).isEqualTo(0); } + @Test + public void extract_withOmitTrackSampleTableFlagAndNoStssBox_extractsCorrectMetadata() + throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + String inputFilePath = "media/mp4/sample_without_stss.mp4"; + Mp4Extractor mp4Extractor = + new Mp4Extractor( + new DefaultSubtitleParserFactory(), + /* flags= */ Mp4Extractor.FLAG_OMIT_TRACK_SAMPLE_TABLE); + + FakeExtractorOutput output = + TestUtil.extractAllSamplesFromFile(mp4Extractor, context, inputFilePath); + + assertThat(output.seekMap).isNotNull(); + assertThat(output.seekMap.getDurationUs()).isEqualTo(1001000); + assertThat(output.seekMap.isSeekable()).isTrue(); + assertThat(output.numberOfTracks).isEqualTo(1); + // Check Video Track Format + Format videoFormat = output.trackOutputs.get(0).lastFormat; + assertThat(videoFormat.sampleMimeType).isEqualTo("video/avc"); + assertThat(videoFormat.width).isEqualTo(1080); + assertThat(videoFormat.height).isEqualTo(720); + // Importantly, check that no actual sample data was output + assertThat(output.trackOutputs.get(0).getSampleCount()).isEqualTo(0); + } + private static String getDumpFilePath(String inputFilePath, String suffix) { return inputFilePath.replaceFirst("media", "extractordumps") + suffix; } diff --git a/libraries/inspector/build.gradle b/libraries/inspector/build.gradle index 24f4e03649e..b67ec924dc9 100644 --- a/libraries/inspector/build.gradle +++ b/libraries/inspector/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.inspector' @@ -38,6 +39,7 @@ dependencies { api project(modulePrefix + 'lib-exoplayer') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.concurrent:concurrent-futures:' + androidxConcurrentFuturesVersion + compileOnly 'androidx.exifinterface:exifinterface:' + androidxExifInterfaceVersion compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion diff --git a/libraries/inspector/src/main/java/androidx/media3/inspector/MetadataRetriever.java b/libraries/inspector/src/main/java/androidx/media3/inspector/MetadataRetriever.java index ed3acf078d5..34d6c46788c 100644 --- a/libraries/inspector/src/main/java/androidx/media3/inspector/MetadataRetriever.java +++ b/libraries/inspector/src/main/java/androidx/media3/inspector/MetadataRetriever.java @@ -28,6 +28,7 @@ import androidx.media3.common.util.Clock; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.MetadataRetrieverInternal; +import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.TrackGroupArray; @@ -42,6 +43,11 @@ /** * Retrieves information from a {@link MediaItem} without playback. * + *

This class is intended for extracting metadata from media formats supported by Media3 players + * (such as {@link ExoPlayer}) for playback. It is not designed as a general-purpose metadata + * extractor for non-playback formats, such as standalone images. To extract properties like height, + * width, and rotation from images, use {@link androidx.exifinterface.media.ExifInterface}. + * *

An instance is created for a single {@link MediaItem} via a {@link Builder}. It provides * methods to asynchronously retrieve metadata. The instance must be {@link #close() closed} after * use to release resources. diff --git a/libraries/inspector/src/test/java/androidx/media3/inspector/MetadataRetrieverTest.java b/libraries/inspector/src/test/java/androidx/media3/inspector/MetadataRetrieverTest.java index 6863fa81575..28bb7556878 100644 --- a/libraries/inspector/src/test/java/androidx/media3/inspector/MetadataRetrieverTest.java +++ b/libraries/inspector/src/test/java/androidx/media3/inspector/MetadataRetrieverTest.java @@ -562,4 +562,28 @@ public void setMaximumParallelRetrievals_toOne_processesAllRetrievalsSequentiall assertThat(trackGroups2.length).isEqualTo(1); } } + + @Test + public void retrieveMetadata_mediaSourceThrowsRuntimeException_failsFuture() { + MediaSource.Factory mockFactory = mock(MediaSource.Factory.class); + RuntimeException runtimeException = new RuntimeException(); + when(mockFactory.createMediaSource(any(MediaItem.class))).thenThrow(runtimeException); + MediaItem mediaItem = + MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")); + + try (MetadataRetriever retriever = + new MetadataRetriever.Builder(context, mediaItem) + .setClock(clock) + .setMediaSourceFactory(mockFactory) + .build()) { + + ListenableFuture trackGroupsFuture = retriever.retrieveTrackGroups(); + + ExecutionException thrown = + assertThrows( + ExecutionException.class, + () -> trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS)); + assertThat(thrown).hasCauseThat().isSameInstanceAs(runtimeException); + } + } } diff --git a/libraries/inspector_frame/build.gradle b/libraries/inspector_frame/build.gradle index 69b5deb8aae..e03cdb77d89 100644 --- a/libraries/inspector_frame/build.gradle +++ b/libraries/inspector_frame/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.inspector.frame' diff --git a/libraries/inspector_frame/src/androidTest/java/androidx/media3/inspector/frame/FrameExtractorTest.java b/libraries/inspector_frame/src/androidTest/java/androidx/media3/inspector/frame/FrameExtractorTest.java index 3336af3a349..86f5d0fccb2 100644 --- a/libraries/inspector_frame/src/androidTest/java/androidx/media3/inspector/frame/FrameExtractorTest.java +++ b/libraries/inspector_frame/src/androidTest/java/androidx/media3/inspector/frame/FrameExtractorTest.java @@ -17,6 +17,7 @@ import static androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND; import static androidx.media3.exoplayer.SeekParameters.CLOSEST_SYNC; +import static androidx.media3.test.utils.AssetInfo.MP3_ASSET; import static androidx.media3.test.utils.AssetInfo.MP4_ASSET; import static androidx.media3.test.utils.AssetInfo.MP4_ONLY_PREROLL_SYNC_SAMPLE_EDIT_LIST; import static androidx.media3.test.utils.AssetInfo.MP4_TRIM_OPTIMIZATION_270; @@ -35,6 +36,7 @@ import androidx.media3.common.GlObjectsProvider; import androidx.media3.common.GlTextureInfo; import androidx.media3.common.MediaItem; +import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.NullableType; import androidx.media3.effect.DefaultGlObjectsProvider; @@ -685,4 +687,121 @@ public void extractThumbnail_whenThumbnailMetadataIsMissing_returnsCorrectFrame( .isAtMost(1); } } + + @Test + public void extractFrame_withFailingEffect_reportsErrorViaFuture() { + ExecutionException thrown = + assertThrows( + ExecutionException.class, + () -> { + try (FrameExtractor frameExtractor = + new FrameExtractor.Builder(context, MediaItem.fromUri(FILE_PATH)) + .setEffects( + ImmutableList.of( + (GlEffect) + (context, useHdr) -> + new PassthroughShaderProgram() { + @Override + public void queueInputFrame( + GlObjectsProvider glObjectsProvider, + GlTextureInfo inputTexture, + long presentationTimeUs) { + onError( + new VideoFrameProcessingException( + "Simulated processing failure")); + } + })) + .build()) { + frameExtractor.getFrame(/* positionMs= */ 0).get(TIMEOUT_SECONDS, SECONDS); + } + }); + assertThat(thrown).hasCauseThat().isInstanceOf(ExoPlaybackException.class); + } + + @Test + public void extractFrame_audioOnlyMedia_reportsErrorViaFuture() { + try (FrameExtractor frameExtractor = + new FrameExtractor.Builder(context, MediaItem.fromUri(MP3_ASSET.uri)).build()) { + ExecutionException thrown = + assertThrows( + ExecutionException.class, + () -> frameExtractor.getFrame(/* positionMs= */ 0).get(TIMEOUT_SECONDS, SECONDS)); + + assertThat(thrown).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(thrown) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("Media item does not contain any video tracks."); + } + } + + @Test + public void extractFrame_audioOnlyMedia_consecutiveCallsReportErrorViaFuture() { + try (FrameExtractor frameExtractor = + new FrameExtractor.Builder(context, MediaItem.fromUri(MP3_ASSET.uri)).build()) { + ExecutionException thrown1 = + assertThrows( + ExecutionException.class, + () -> frameExtractor.getFrame(/* positionMs= */ 0).get(TIMEOUT_SECONDS, SECONDS)); + + assertThat(thrown1).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(thrown1) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("Media item does not contain any video tracks."); + + ExecutionException thrown2 = + assertThrows( + ExecutionException.class, + () -> frameExtractor.getFrame(/* positionMs= */ 1000).get(TIMEOUT_SECONDS, SECONDS)); + + assertThat(thrown2).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(thrown2) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("Media item does not contain any video tracks."); + } + } + + @Test + public void extractFrame_zeroDurationVideo_reportsErrorViaFuture() { + // Simulate an empty video track by clipping to zero duration, triggering STATE_ENDED. + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(FILE_PATH) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setEndPositionMs(0).build()) + .build(); + try (FrameExtractor frameExtractor = new FrameExtractor.Builder(context, mediaItem).build()) { + ExecutionException thrown = + assertThrows( + ExecutionException.class, + () -> frameExtractor.getFrame(/* positionMs= */ 0).get(TIMEOUT_SECONDS, SECONDS)); + + assertThat(thrown).hasCauseThat().isInstanceOf(IllegalStateException.class); + assertThat(thrown) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("Reached end of stream without extracting a frame."); + } + } + + @Test + public void extractFrame_drmProtectedMedia_reportsErrorViaFuture() { + try (FrameExtractor frameExtractor = + new FrameExtractor.Builder( + context, MediaItem.fromUri("asset:///media/drm/sample_fragmented_widevine.mp4")) + .build()) { + ExecutionException thrown = + assertThrows( + ExecutionException.class, + () -> frameExtractor.getFrame(/* positionMs= */ 0).get(TIMEOUT_SECONDS, SECONDS)); + + assertThat(thrown).hasCauseThat().isInstanceOf(UnsupportedOperationException.class); + assertThat(thrown) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("Frame extraction from DRM-protected media is not supported."); + } + } } diff --git a/libraries/inspector_frame/src/main/java/androidx/media3/inspector/frame/FrameExtractorInternal.java b/libraries/inspector_frame/src/main/java/androidx/media3/inspector/frame/FrameExtractorInternal.java index 015a07c58dd..cf0f3965934 100644 --- a/libraries/inspector_frame/src/main/java/androidx/media3/inspector/frame/FrameExtractorInternal.java +++ b/libraries/inspector_frame/src/main/java/androidx/media3/inspector/frame/FrameExtractorInternal.java @@ -51,6 +51,7 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; +import androidx.media3.common.Tracks; import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.VideoGraph; import androidx.media3.common.util.GlProgram; @@ -158,6 +159,13 @@ private static final Object LOCK = new Object(); + private static final MatrixTransformation MIRROR_Y_TRANSFORMATION = + presentationTimeUs -> { + Matrix mirrorY = new Matrix(); + mirrorY.setScale(/* sx= */ 1, /* sy= */ -1); + return mirrorY; + }; + @SuppressWarnings("NonFinalStaticField") // Required for lazy initialization. @GuardedBy("LOCK") @Nullable @@ -276,6 +284,9 @@ private FrameExtractorInternal() { }, playerHandler::post); } else { + Tracks tracks = checkNotNull(player).getCurrentTracks(); + assertTracksSupported(tracks); + long timestampToExtractMs = isThumbnailRequest ? getThumbnailPresentationTimeMs() : request.positionMs; return processTask( @@ -302,6 +313,33 @@ private FrameExtractorInternal() { }); } + private static void assertTracksSupported(Tracks tracks) { + if (!tracks.containsType(C.TRACK_TYPE_VIDEO)) { + throw new IllegalArgumentException("Media item does not contain any video tracks."); + } + + boolean hasSelectedClearVideoTrack = false; + boolean hasDrmVideoTrack = false; + + for (Tracks.Group group : tracks.getGroups()) { + if (group.getType() == C.TRACK_TYPE_VIDEO) { + for (int i = 0; i < group.length; i++) { + Format format = group.getTrackFormat(i); + if (format.drmInitData != null || format.cryptoType != C.CRYPTO_TYPE_NONE) { + hasDrmVideoTrack = true; + } else if (group.isTrackSelected(i)) { + hasSelectedClearVideoTrack = true; + } + } + } + } + + if (hasDrmVideoTrack && !hasSelectedClearVideoTrack) { + throw new UnsupportedOperationException( + "Frame extraction from DRM-protected media is not supported."); + } + } + private long getThumbnailPresentationTimeMs() { return thumbnailPresentationTimeMs != C.TIME_UNSET ? thumbnailPresentationTimeMs : 0; } @@ -390,13 +428,7 @@ private static ImmutableList buildVideoEffects( List effects, FrameReader frameReader) { ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); listBuilder.addAll(effects); - listBuilder.add( - (MatrixTransformation) - presentationTimeUs -> { - Matrix mirrorY = new Matrix(); - mirrorY.setScale(/* sx= */ 1, /* sy= */ -1); - return mirrorY; - }); + listBuilder.add(MIRROR_Y_TRANSFORMATION); listBuilder.add(frameReader); return listBuilder.build(); } @@ -411,8 +443,10 @@ private PlayerListener(FrameExtractorInternal internal) { @Override public void onPlayerError(EventTime eventTime, PlaybackException error) { CallbackToFutureAdapter.Completer frameBeingExtractedCompleter = - checkNotNull(internal.activeTaskCompleter.getAndSet(null)); - frameBeingExtractedCompleter.setException(error); + internal.activeTaskCompleter.getAndSet(null); + if (frameBeingExtractedCompleter != null) { + frameBeingExtractedCompleter.setException(error); + } } @Override @@ -426,8 +460,39 @@ public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) // and extractedFrameNeedsRendering remains false. No frames are rendered. Repeat the // previously returned frame. CallbackToFutureAdapter.Completer frameBeingExtractedCompleter = - checkNotNull(internal.activeTaskCompleter.getAndSet(null)); - frameBeingExtractedCompleter.set(checkNotNull(internal.lastSeekDedupeFrame)); + internal.activeTaskCompleter.getAndSet(null); + if (frameBeingExtractedCompleter != null) { + if (internal.lastSeekDedupeFrame != null) { + frameBeingExtractedCompleter.set(internal.lastSeekDedupeFrame); + } else { + frameBeingExtractedCompleter.setException( + new IllegalStateException( + "Expected to deduplicate frame, but no previous frame was found.")); + } + } + } + } else if (state == Player.STATE_ENDED) { + CallbackToFutureAdapter.Completer frameBeingExtractedCompleter = + internal.activeTaskCompleter.getAndSet(null); + if (frameBeingExtractedCompleter != null) { + frameBeingExtractedCompleter.setException( + new IllegalStateException("Reached end of stream without extracting a frame.")); + } + } + } + + @Override + public void onTracksChanged(EventTime eventTime, Tracks tracks) { + if (tracks.isEmpty()) { + return; + } + try { + assertTracksSupported(tracks); + } catch (RuntimeException e) { + CallbackToFutureAdapter.Completer frameBeingExtractedCompleter = + internal.activeTaskCompleter.getAndSet(null); + if (frameBeingExtractedCompleter != null) { + frameBeingExtractedCompleter.setException(e); } } } @@ -445,6 +510,23 @@ public GlShaderProgram toGlShaderProgram(Context context, boolean useHdr) throws VideoFrameProcessingException { return new FrameReadingGlShaderProgram(context, useHdr, internal); } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + FrameReader that = (FrameReader) obj; + return internal.equals(that.internal); + } + + @Override + public int hashCode() { + return internal.hashCode(); + } } private static final class FrameReadingGlShaderProgram extends PassthroughShaderProgram { @@ -575,10 +657,12 @@ public void queueInputFrame( bitmap.copyPixelsFromBuffer(byteBuffer); CallbackToFutureAdapter.Completer frameBeingExtractedCompleter = - checkNotNull(internal.activeTaskCompleter.getAndSet(null)); - FrameExtractor.Frame frame = new FrameExtractor.Frame(usToMs(presentationTimeUs), bitmap); - internal.lastSeekDedupeFrame = frame; - frameBeingExtractedCompleter.set(frame); + internal.activeTaskCompleter.getAndSet(null); + if (frameBeingExtractedCompleter != null) { + FrameExtractor.Frame frame = new FrameExtractor.Frame(usToMs(presentationTimeUs), bitmap); + internal.lastSeekDedupeFrame = frame; + frameBeingExtractedCompleter.set(frame); + } // Drop frame: do not call outputListener.onOutputFrameAvailable(). // Block effects pipeline: do not call inputListener.onReadyToAcceptInputFrame(). // The effects pipeline will unblock and receive new frames when flushed after a seek. @@ -754,13 +838,6 @@ public boolean isReady() { return frameRenderedSinceLastPositionReset; } - @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (!frameRenderedSinceLastPositionReset) { - super.render(positionUs, elapsedRealtimeUs); - } - } - @Override protected boolean processOutputBuffer( long positionUs, diff --git a/libraries/muxer/build.gradle b/libraries/muxer/build.gradle index 4507e8f46f5..006a536f306 100644 --- a/libraries/muxer/build.gradle +++ b/libraries/muxer/build.gradle @@ -12,7 +12,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.muxer' diff --git a/libraries/session/build.gradle b/libraries/session/build.gradle index fae66140d33..3f41ff14865 100644 --- a/libraries/session/build.gradle +++ b/libraries/session/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.session' diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index 0708b8aa306..027c3d7cb44 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -25,6 +25,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static java.lang.Integer.max; import android.app.Notification; import android.app.NotificationChannel; @@ -646,11 +647,23 @@ private static long getPlaybackStartTimeEpochMs(Player player) { private static int getMaxNotificationIconSize() { Resources res = Resources.getSystem(); try { - int id = res.getIdentifier("notification_right_icon_size", "dimen", "android"); - return res.getDimensionPixelSize(id); + int rightIconSizeId = res.getIdentifier("notification_right_icon_size", "dimen", "android"); + int iconSize = res.getDimensionPixelSize(rightIconSizeId); + if (SDK_INT < 31) { + int mediaImageMaxHeightId = + res.getIdentifier("notification_media_image_max_height", "dimen", "android"); + int mediaImageMaxHeight = res.getDimensionPixelSize(mediaImageMaxHeightId); + int mediaImageMaxWidthId = + res.getIdentifier("notification_media_image_max_width", "dimen", "android"); + int mediaImageMaxWidth = res.getDimensionPixelSize(mediaImageMaxWidthId); + iconSize = max(max(iconSize, mediaImageMaxWidth), mediaImageMaxHeight); + } + return iconSize; } catch (Resources.NotFoundException e) { - // Fallback to assumed icon size of 48dp if the system property is missing. - return (int) (48 * res.getDisplayMetrics().density); + // Fallback to default AOSP icon size if the system property is missing (48dp for right icon + // size and 280dp for media image max width used before API 31). + int assumedSizeDp = SDK_INT < 31 ? 280 : 48; + return (int) (assumedSizeDp * res.getDisplayMetrics().density); } } 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 7273342a9e6..53817e47a57 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java +++ b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java @@ -1369,18 +1369,17 @@ public static Player.Commands convertToPlayerCommands( || hasAction(actions, PlaybackStateCompat.ACTION_PLAY_PAUSE)) { playerCommandsBuilder.add(COMMAND_PLAY_PAUSE); } - if (hasAction(actions, PlaybackStateCompat.ACTION_PREPARE)) { + if (hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID) + || hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH) + || hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { + playerCommandsBuilder.add(COMMAND_SET_MEDIA_ITEM); + } + if (hasAction(actions, PlaybackStateCompat.ACTION_PREPARE) + || hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID) + || hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH) + || hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { playerCommandsBuilder.add(COMMAND_PREPARE); } - if ((hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID) - && hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) - || (hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH) - && hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) - || (hasAction(actions, PlaybackStateCompat.ACTION_PREPARE_FROM_URI) - && hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_URI))) { - // Require both PREPARE and PLAY actions as we have no logic to handle having just one action. - playerCommandsBuilder.addAll(COMMAND_SET_MEDIA_ITEM, COMMAND_PREPARE); - } if (hasAction(actions, PlaybackStateCompat.ACTION_REWIND)) { playerCommandsBuilder.add(COMMAND_SEEK_BACK); } @@ -1422,9 +1421,9 @@ && hasAction(actions, PlaybackStateCompat.ACTION_PLAY_FROM_URI))) { COMMAND_RELEASE); if ((sessionFlags & FLAG_HANDLES_QUEUE_COMMANDS) != 0) { playerCommandsBuilder.add(COMMAND_CHANGE_MEDIA_ITEMS); - if (hasAction(actions, PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) { - playerCommandsBuilder.add(Player.COMMAND_SEEK_TO_MEDIA_ITEM); - } + } + if (hasAction(actions, PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) { + playerCommandsBuilder.add(Player.COMMAND_SEEK_TO_MEDIA_ITEM); } if (isSessionReady) { if (hasAction(actions, PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 04edd996328..c22d6ec7eb0 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -870,7 +870,7 @@ public final void seekTo(int mediaItemIndex, long positionMs) { * {@inheritDoc} * *

Interoperability: When connected to {@code - * android.support.v4.media.session.MediaSessionCompat}, it returns {code 0}. + * android.support.v4.media.session.MediaSessionCompat}, it returns {@code 0}. */ @Override public final long getSeekBackIncrement() { @@ -899,7 +899,7 @@ public final void seekBack() { * {@inheritDoc} * *

Interoperability: When connected to {@code - * android.support.v4.media.session.MediaSessionCompat}, it returns {code 0}. + * android.support.v4.media.session.MediaSessionCompat}, it returns {@code 0}. */ @Override public final long getSeekForwardIncrement() { 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 4c293488c78..f1361dedec9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -300,7 +300,7 @@ public void release() { clearSurfacesAndCallbacks(); flushCommandQueueHandler.release(); this.iSession = null; - if (iSession != null) { + if (iSession != null && iSession.asBinder().isBinderAlive()) { int seq = sequencedFutureManager.obtainNextSequenceNumber(); try { iSession.asBinder().unlinkToDeath(deathRecipient, 0); @@ -3891,10 +3891,12 @@ private boolean handleMessage(Message msg) { } private void flushCommandQueue() { - try { - iSession.flushCommandQueue(controllerStub); - } catch (RemoteException e) { - Log.w(TAG, "Error in sending flushCommandQueue"); + if (iSession != null && iSession.asBinder().isBinderAlive()) { + try { + iSession.flushCommandQueue(controllerStub); + } catch (RemoteException e) { + Log.w(TAG, "Error in sending flushCommandQueue", e); + } } } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index 5c8dd9c6198..9fd805a69f5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -18,7 +18,6 @@ import static androidx.media3.session.MediaUtils.calculateBufferedPercentage; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.String.format; @@ -114,6 +113,7 @@ private ControllerInfo controllerInfo; private long currentPositionMs; private long lastSetPlayWhenReadyCalledTimeMs; + private boolean hasInitializedLegacyPlaylist; public MediaControllerImplLegacy( Context context, @@ -213,6 +213,7 @@ public void stop() { /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); + hasInitializedLegacyPlaylist = false; controllerCompat.getTransportControls().stop(); } @@ -279,7 +280,7 @@ public void prepare() { /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - if (hasMedia()) { + if (canInitializeLegacyPlaylist(controllerInfo)) { initializeLegacyPlaylist(); } } @@ -699,7 +700,7 @@ public void setMediaItems(List mediaItems, int startIndex, long start maskedControllerInfo, /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - if (isPrepared()) { + if (canInitializeLegacyPlaylist(controllerInfo)) { initializeLegacyPlaylist(); } } @@ -765,7 +766,7 @@ public void addMediaItems(int index, List mediaItems) { /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - if (isPrepared()) { + if (hasInitializedLegacyPlaylist) { addQueueItems(mediaItems, index); } } @@ -819,7 +820,7 @@ public void removeMediaItems(int fromIndex, int toIndex) { /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - if (isPrepared()) { + if (hasInitializedLegacyPlaylist) { for (int i = fromIndex; i < toIndex && i < legacyPlayerInfo.queue.size(); i++) { controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(i).getDescription()); } @@ -887,7 +888,7 @@ public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - if (isPrepared()) { + if (hasInitializedLegacyPlaylist) { ArrayList moveItems = new ArrayList<>(); for (int i = 0; i < (toIndex - fromIndex); i++) { moveItems.add(legacyPlayerInfo.queue.get(fromIndex)); @@ -1310,12 +1311,14 @@ public void setPlayWhenReady(boolean playWhenReady) { /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - if (isPrepared() && hasMedia()) { + if (hasInitializedLegacyPlaylist) { if (playWhenReady) { controllerCompat.getTransportControls().play(); } else { controllerCompat.getTransportControls().pause(); } + } else if (canInitializeLegacyPlaylist(controllerInfo)) { + initializeLegacyPlaylist(); } } @@ -1453,17 +1456,16 @@ private void connectToService() { }); } - private boolean isPrepared() { - return controllerInfo.playerInfo.playbackState != Player.STATE_IDLE; - } - - private boolean hasMedia() { - return !controllerInfo.playerInfo.timeline.isEmpty(); + private static boolean canInitializeLegacyPlaylist(ControllerInfo controllerInfo) { + return controllerInfo.playerInfo.playbackState != Player.STATE_IDLE + && !controllerInfo.playerInfo.timeline.isEmpty() + && (controllerInfo.availablePlayerCommands.contains(Player.COMMAND_PREPARE) + || controllerInfo.playerInfo.playWhenReady); } private void initializeLegacyPlaylist() { + hasInitializedLegacyPlaylist = true; Window window = new Window(); - checkState(isPrepared() && hasMedia()); QueueTimeline queueTimeline = (QueueTimeline) controllerInfo.playerInfo.timeline; // Set the current item first as these calls are expected to replace the current playlist. int currentIndex = controllerInfo.playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex; @@ -1568,6 +1570,11 @@ private void handleBitmapFuturesAllCompletedAndAddQueueItems( List<@NullableType ListenableFuture> bitmapFutures, List mediaItems, int startIndex) { + if (released + || (controllerCompat.getFlags() & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) { + // Stale command, session released or queue support changed. + return; + } for (int i = 0; i < bitmapFutures.size(); i++) { @Nullable ListenableFuture future = bitmapFutures.get(i); @Nullable Bitmap bitmap = null; @@ -1613,7 +1620,7 @@ private void handleNewLegacyParameters( updateControllerInfo( notifyConnected, newLegacyPlayerInfo, - /* resetPendingLegacyPlayerInfo= */ true, + /* hasNewLegacyPlayerInfo= */ true, newControllerInfo, /* discontinuityReason= */ reasons.first, /* mediaItemTransitionReason= */ reasons.second); @@ -1637,7 +1644,7 @@ private void updateStateMaskedControllerInfo( updateControllerInfo( /* notifyConnected= */ false, legacyPlayerInfo, - /* resetPendingLegacyPlayerInfo= */ false, + /* hasNewLegacyPlayerInfo= */ false, newControllerInfo, discontinuityReason, mediaItemTransitionReason); @@ -1648,17 +1655,19 @@ private void updateStateMaskedControllerInfo( private void updateControllerInfo( boolean notifyConnected, LegacyPlayerInfo newLegacyPlayerInfo, - boolean resetPendingLegacyPlayerInfo, + boolean hasNewLegacyPlayerInfo, ControllerInfo newControllerInfo, @Nullable @Player.DiscontinuityReason Integer discontinuityReason, @Nullable @Player.MediaItemTransitionReason Integer mediaItemTransitionReason) { LegacyPlayerInfo oldLegacyPlayerInfo = legacyPlayerInfo; ControllerInfo oldControllerInfo = controllerInfo; - if (legacyPlayerInfo != newLegacyPlayerInfo) { - legacyPlayerInfo = new LegacyPlayerInfo(newLegacyPlayerInfo); - } - if (resetPendingLegacyPlayerInfo) { + if (hasNewLegacyPlayerInfo) { + legacyPlayerInfo = newLegacyPlayerInfo; pendingLegacyPlayerInfo = legacyPlayerInfo; + if (canInitializeLegacyPlaylist(newControllerInfo)) { + // New platform state already has initialized playlist. + hasInitializedLegacyPlaylist = true; + } } controllerInfo = newControllerInfo; @@ -2569,17 +2578,6 @@ public LegacyPlayerInfo( this.sessionExtras = sessionExtras != null ? sessionExtras : Bundle.EMPTY; } - public LegacyPlayerInfo(LegacyPlayerInfo other) { - playbackInfoCompat = other.playbackInfoCompat; - playbackStateCompat = other.playbackStateCompat; - mediaMetadataCompat = other.mediaMetadataCompat; - queue = other.queue; - queueTitle = other.queueTitle; - repeatMode = other.repeatMode; - shuffleMode = other.shuffleMode; - sessionExtras = other.sessionExtras; - } - @CheckResult public LegacyPlayerInfo copyWithExtraBinderGetters( @Nullable PlaybackStateCompat playbackStateCompat, diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java index c4020583b1f..e1118378bfa 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java @@ -22,6 +22,7 @@ import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.LOCAL_VARIABLE; import static java.lang.annotation.ElementType.METHOD; @@ -431,6 +432,7 @@ default ListenableFuture>> onGetSearchRes public static final class Builder extends BuilderBase { private @LibraryErrorReplicationMode int libraryErrorReplicationMode; + private boolean buildCalled; /** * Creates a builder for {@link MediaLibrarySession}. @@ -689,6 +691,8 @@ public Builder setCommandButtonsForMediaItems(List commandButtons */ @Override public MediaLibrarySession build() { + checkState(!buildCalled); + buildCalled = true; ensureBitmapLoaderIsSizeLimited(); return new MediaLibrarySession( context, 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 3b5ccc28dd4..2f08d8774ec 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -421,10 +421,13 @@ private void postOrRunOnApplicationHandler(Runnable runnable) { private ListenableFuture>> getRecentMediaItemAtDeviceBootTime( ControllerInfo controller, @Nullable LibraryParams params) { - controller = - isMediaNotificationControllerConnected() - ? checkNotNull(getMediaNotificationControllerInfo()) - : controller; + if (isMediaNotificationControllerConnected()) { + // The media notification controller may have disconnected during session shutdown. + ControllerInfo mediaNotificationController = getMediaNotificationControllerInfo(); + if (mediaNotificationController != null) { + controller = mediaNotificationController; + } + } ListenableFuture future = callback.onPlaybackResumption(instance, controller, /* isForPlayback= */ false); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java index 5a73849849f..cd22b31c629 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java @@ -33,10 +33,12 @@ import android.os.Message; import android.util.Pair; import androidx.annotation.Nullable; +import androidx.concurrent.futures.CallbackToFutureAdapter; import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; import androidx.media3.common.Player; import androidx.media3.common.util.Log; +import androidx.media3.common.util.NullableType; import androidx.media3.common.util.Util; import androidx.media3.session.MediaNotification.Provider.NotificationChannelInfo; import androidx.media3.session.MediaSessionService.ShowNotificationForIdlePlayerMode; @@ -187,34 +189,54 @@ public void setMediaNotificationProvider(MediaNotification.Provider mediaNotific * @param session A session that needs notification update. * @param startInForegroundRequired Whether the service is required to start in the foreground. */ - public void updateNotification(MediaSession session, boolean startInForegroundRequired) { - if (!mediaSessionService.isSessionAdded(session) || !shouldShowNotification(session)) { - removeNotification(); - return; - } - - int notificationSequence = ++totalNotificationCount; - ImmutableList mediaButtonPreferences = - checkNotNull(getConnectedControllerForSession(session)).getMediaButtonPreferences(); - MediaNotification.Provider.Callback callback = - notification -> - mainExecutor.execute( - () -> onNotificationUpdated(notificationSequence, session, notification)); - Util.postOrRun( - new Handler(session.getPlayer().getApplicationLooper()), - () -> { - MediaNotification mediaNotification = - this.mediaNotificationProvider.createNotification( - session, mediaButtonPreferences, actionFactory, callback); - checkState( - /* expression= */ mediaNotification.notificationId != SHUTDOWN_NOTIFICATION_ID, - /* errorMessage= */ "notification ID " - + SHUTDOWN_NOTIFICATION_ID - + " is already used internally."); - mainExecutor.execute( - () -> - updateNotificationInternal( - session, mediaNotification, startInForegroundRequired)); + public ListenableFuture<@NullableType Void> updateNotification( + MediaSession session, boolean startInForegroundRequired) { + return CallbackToFutureAdapter.getFuture( + completer -> { + if (!mediaSessionService.isSessionAdded(session) || !shouldShowNotification(session)) { + removeNotification(); + completer.set(null); + return "notificationRemoved"; + } + int notificationSequence = ++totalNotificationCount; + ImmutableList mediaButtonPreferences = + checkNotNull(getConnectedControllerForSession(session)).getMediaButtonPreferences(); + MediaNotification.Provider.Callback callback = + notification -> + mainExecutor.execute( + () -> onNotificationUpdated(notificationSequence, session, notification)); + + Util.postOrRun( + new Handler(session.getPlayer().getApplicationLooper()), + () -> { + try { + MediaNotification mediaNotification = + this.mediaNotificationProvider.createNotification( + session, mediaButtonPreferences, actionFactory, callback); + checkState( + /* expression= */ mediaNotification.notificationId + != SHUTDOWN_NOTIFICATION_ID, + /* errorMessage= */ "notification ID " + + SHUTDOWN_NOTIFICATION_ID + + " is already used internally."); + mainExecutor.execute( + () -> { + try { + updateNotificationInternal( + session, mediaNotification, startInForegroundRequired); + completer.set(null); + } catch (IllegalStateException e) { + // Re-throw exception thrown in the main thread caused by not being + // allowed to start a service into the foreground from the background. + completer.setException(e); + } + }); + } catch (RuntimeException e) { + // Re-throw exception thrown in the app thread caused by programming errors. + completer.setException(e); + } + }); + return "notificationUpdated"; }); } 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 80e08cd8493..3fd48a54786 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -22,17 +22,21 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static java.lang.Math.max; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; +import android.view.Display; import android.view.KeyEvent; import android.view.Surface; +import android.view.WindowManager; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -57,6 +61,7 @@ import androidx.media3.common.VideoSize; import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.ExperimentalApi; +import androidx.media3.common.util.NullableType; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSourceBitmapLoader; @@ -65,6 +70,7 @@ import androidx.media3.session.legacy.MediaControllerCompat; import androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo; import com.google.common.collect.ImmutableList; +import com.google.common.primitives.ImmutableIntArray; import com.google.common.primitives.Longs; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -73,6 +79,7 @@ import java.util.HashMap; import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -254,6 +261,7 @@ public class MediaSession { public static final class Builder extends BuilderBase { private boolean useLegacySurfaceHandling; + private boolean buildCalled; /** * Creates a builder for {@link MediaSession}. @@ -509,6 +517,8 @@ public Builder setExperimentalSetUseLegacySurfaceHandling(boolean useLegacySurfa */ @Override public MediaSession build() { + checkState(!buildCalled); + buildCalled = true; ensureBitmapLoaderIsSizeLimited(); return new MediaSession( context, @@ -2479,6 +2489,9 @@ default void onError(int seq, SessionError sessionError) throws RemoteException BuilderT extends BuilderBase, CallbackT extends Callback> { + private static final AtomicReference<@NullableType ImmutableIntArray> bitmapSizesToAvoidApi29 = + new AtomicReference<>(/* initialValue= */ null); + /* package */ final Context context; /* package */ final Player player; /* package */ String id; @@ -2494,7 +2507,7 @@ default void onError(int seq, SessionError sessionError) throws RemoteException /* package */ boolean isPeriodicPositionUpdateEnabled; public BuilderBase(Context context, Player player, CallbackT callback) { - this.context = checkNotNull(context); + this.context = checkNotNull(context.getApplicationContext()); this.player = checkNotNull(player); checkArgument(player.canAdvertiseSession()); id = DEFAULT_SESSION_ID; @@ -2597,15 +2610,50 @@ protected final void ensureBitmapLoaderIsSizeLimited() { int dimensionLimit = MediaSession.getBitmapDimensionLimit(context); if (bitmapLoader == null) { bitmapLoader = - new CacheBitmapLoader( - new DataSourceBitmapLoader.Builder(context) - .setMaximumOutputDimension(dimensionLimit) - .setMakeShared(true) - .build()); + new DataSourceBitmapLoader.Builder(context) + .setMaximumOutputDimension(dimensionLimit) + .setMakeShared(true) + .build(); } else { bitmapLoader = new SizeLimitedBitmapLoader(bitmapLoader, dimensionLimit, /* makeShared= */ true); } + if (SDK_INT == 29) { + bitmapLoader = + new SizeAvoidingBitmapLoader(bitmapLoader, getBitmapSizesToAvoidApi29(context)); + } + // Always apply at least a basic level of caching to avoid repeated work. + bitmapLoader = new CacheBitmapLoader(bitmapLoader); + } + + private static ImmutableIntArray getBitmapSizesToAvoidApi29(Context context) { + // Workaround bug on API 29 where Bitmap size of exactly display size / 6 causes crashes. + // See https://github.com/androidx/media/issues/3118. + ImmutableIntArray sizesToAvoid = bitmapSizesToAvoidApi29.get(); + if (sizesToAvoid != null) { + return sizesToAvoid; + } + // Use same method to obtain display size and downscaling by 6 as in + // https://android.googlesource.com/platform/frameworks/base/+/android-platform-12.0.0_r29/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt#57 + Display display = context.getSystemService(WindowManager.class).getDefaultDisplay(); + Point currentDisplaySize = new Point(); + display.getSize(currentDisplaySize); + Point realDisplaySize = new Point(); + display.getRealSize(realDisplaySize); + // We also have to exclude the size when the display is rotated (landscape/portrait), but we + // can't query it upfront. It's usually either the same as the current dimension, or the + // device applies the same offset between the reported and real size (to account for the + // system navigation bar usually). + Point rotatedDisplaySize = + new Point( + realDisplaySize.y - (realDisplaySize.x - currentDisplaySize.x), + realDisplaySize.x - (realDisplaySize.y - currentDisplaySize.y)); + sizesToAvoid = + ImmutableIntArray.of( + max(currentDisplaySize.x / 6, currentDisplaySize.y / 6), + max(rotatedDisplaySize.x / 6, rotatedDisplaySize.y / 6)); + bitmapSizesToAvoidApi29.set(sizesToAvoid); + return sizesToAvoid; } } 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 fef58350f33..fa4f1f6dd7d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -1035,9 +1035,14 @@ protected void setSessionActivity( } protected ControllerInfo resolveControllerInfoForCallback(ControllerInfo controller) { - return isMediaNotificationControllerConnected && isSystemUiController(controller) - ? checkNotNull(getMediaNotificationControllerInfo()) - : controller; + if (isMediaNotificationControllerConnected && isSystemUiController(controller)) { + // The media notification controller may have disconnected between the boolean flag check + // and this lookup (race condition during session shutdown). Fall back to the original + // controller if that happens. + ControllerInfo mediaNotificationController = getMediaNotificationControllerInfo(); + return mediaNotificationController != null ? mediaNotificationController : controller; + } + return controller; } /** @@ -1532,7 +1537,11 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma private boolean applyMediaButtonKeyEvent( KeyEvent keyEvent, boolean doubleTapCompleted, boolean isDismissNotificationEvent) { - ControllerInfo controllerInfo = checkNotNull(instance.getMediaNotificationControllerInfo()); + // The media notification controller may have disconnected during session shutdown. + @Nullable ControllerInfo controllerInfo = instance.getMediaNotificationControllerInfo(); + if (controllerInfo == null) { + return false; + } Runnable command; int keyCode = keyEvent.getKeyCode(); if ((keyCode == KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KEYCODE_HEADSETHOOK) 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 5c67c4ef85c..857c7656d58 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -23,6 +23,7 @@ import static androidx.media3.session.SessionUtil.checkPackageValidity; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static java.lang.annotation.ElementType.TYPE_USE; import android.annotation.SuppressLint; @@ -49,11 +50,14 @@ import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.Player; import androidx.media3.common.util.Log; +import androidx.media3.common.util.NullableType; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.legacy.MediaBrowserServiceCompat; import androidx.media3.session.legacy.MediaSessionManager; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -802,7 +806,22 @@ public void onUpdateNotification(MediaSession session) { public void onUpdateNotification(MediaSession session, boolean startInForegroundRequired) { onUpdateNotification(session); if (defaultMethodCalled) { - getMediaNotificationManager().updateNotification(session, startInForegroundRequired); + ListenableFuture<@NullableType Void> result = + getMediaNotificationManager().updateNotification(session, startInForegroundRequired); + ListenableFuture<@NullableType Void> ignored = + Futures.catching( + result, + RuntimeException.class, + e -> { + if (SDK_INT >= 31 && e instanceof ForegroundServiceStartNotAllowedException) { + Log.e(TAG, "Failed to start service into the foreground", e); + onForegroundServiceStartNotAllowedException(); + } else { + Log.e(TAG, "Calling updateNotification() failed with a runtime exception", e); + } + return null; + }, + directExecutor()); } } @@ -848,18 +867,9 @@ protected final void setMediaNotificationProvider( */ /* package */ boolean onUpdateNotificationInternal( MediaSession session, boolean startInForegroundWhenPaused) { - try { - boolean startInForegroundRequired = - getMediaNotificationManager().shouldRunInForeground(startInForegroundWhenPaused); - onUpdateNotification(session, startInForegroundRequired); - } catch (/* ForegroundServiceStartNotAllowedException */ IllegalStateException e) { - if ((SDK_INT >= 31) && Api31.instanceOfForegroundServiceStartNotAllowedException(e)) { - Log.e(TAG, "Failed to start foreground", e); - onForegroundServiceStartNotAllowedException(); - return false; - } - throw e; - } + boolean startInForegroundRequired = + getMediaNotificationManager().shouldRunInForeground(startInForegroundWhenPaused); + onUpdateNotification(session, startInForegroundRequired); return true; } @@ -1049,12 +1059,4 @@ public void release() { pendingControllers.clear(); } } - - @RequiresApi(31) - private static final class Api31 { - public static boolean instanceOfForegroundServiceStartNotAllowedException( - IllegalStateException e) { - return e instanceof ForegroundServiceStartNotAllowedException; - } - } } diff --git a/libraries/session/src/main/java/androidx/media3/session/SizeAvoidingBitmapLoader.java b/libraries/session/src/main/java/androidx/media3/session/SizeAvoidingBitmapLoader.java new file mode 100644 index 00000000000..6433a984c5a --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/SizeAvoidingBitmapLoader.java @@ -0,0 +1,92 @@ +/* + * 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.session; + +import static androidx.media3.datasource.BitmapUtil.makeShared; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static java.lang.Math.max; + +import android.graphics.Bitmap; +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.util.BitmapLoader; +import com.google.common.primitives.ImmutableIntArray; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +/** + * A {@link BitmapLoader} that avoids producing bitmaps whose larger dimension equals the specific + * values. See https://github.com/androidx/media/issues/3118. + */ +/* package */ final class SizeAvoidingBitmapLoader implements BitmapLoader { + + private final BitmapLoader bitmapLoader; + private final ImmutableIntArray avoidSizes; + + /** + * Creates an instance that loads bitmaps using {@link BitmapLoader} avoiding the specified sizes. + * + * @param bitmapLoader The {@link BitmapLoader}. + * @param avoidSizes The sizes to avoid as the larger dimension. + */ + public SizeAvoidingBitmapLoader(BitmapLoader bitmapLoader, ImmutableIntArray avoidSizes) { + this.bitmapLoader = bitmapLoader; + this.avoidSizes = avoidSizes; + } + + @Override + public boolean supportsMimeType(String mimeType) { + return bitmapLoader.supportsMimeType(mimeType); + } + + @Override + public ListenableFuture decodeBitmap(byte[] data) { + ListenableFuture future = bitmapLoader.decodeBitmap(data); + return Futures.transform(future, this::scaleIfNecessary, directExecutor()); + } + + @Override + public ListenableFuture loadBitmap(Uri uri) { + ListenableFuture future = bitmapLoader.loadBitmap(uri); + return Futures.transform(future, this::scaleIfNecessary, directExecutor()); + } + + @Nullable + @Override + public ListenableFuture loadBitmapFromMetadata(MediaMetadata metadata) { + ListenableFuture future = bitmapLoader.loadBitmapFromMetadata(metadata); + return future == null + ? null + : Futures.transform(future, this::scaleIfNecessary, directExecutor()); + } + + private Bitmap scaleIfNecessary(Bitmap bitmap) { + int height = bitmap.getHeight(); + int width = bitmap.getWidth(); + if (avoidSizes.contains(max(height, width))) { + if (avoidSizes.contains(height)) { + height--; + } + if (avoidSizes.contains(width)) { + width--; + } + bitmap = Bitmap.createScaledBitmap(bitmap, width, height, /* filter= */ true); + bitmap = makeShared(bitmap); + } + return bitmap; + } +} 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 8fe2a1437c0..7ebf82e2c43 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 @@ -1482,6 +1482,9 @@ void sendCustomAction( private void sendRequest(int what, @Nullable Bundle data, Messenger cbMessenger) throws RemoteException { + if (!messenger.getBinder().isBinderAlive()) { + return; + } Message msg = Message.obtain(); msg.what = what; msg.arg1 = CLIENT_VERSION_CURRENT; diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaControllerCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaControllerCompat.java index d15a5ee58d5..2ac982e364a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaControllerCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaControllerCompat.java @@ -573,7 +573,7 @@ public Object getMediaController() { */ public abstract static class Callback implements IBinder.DeathRecipient { @Nullable final MediaController.Callback callbackFwk; - @Nullable MessageHandler handler; + @Nullable private MessageHandler handler; @Nullable IMediaControllerCallback iControllerCallback; // Sharing this in constructor @@ -1301,14 +1301,16 @@ public final void unregisterCallback(Callback callback) { synchronized (lock) { IMediaSession extraBinder = sessionToken.getExtraBinder(); if (extraBinder != null) { - try { - Callback.CallbackStub callbackStub = callbackMap.remove(callback); - if (callbackStub != null) { - callback.iControllerCallback = null; - extraBinder.unregisterCallbackListener(callbackStub); + Callback.CallbackStub callbackStub = callbackMap.remove(callback); + if (callbackStub != null) { + callback.iControllerCallback = null; + if (extraBinder.asBinder().isBinderAlive()) { + try { + extraBinder.unregisterCallbackListener(callbackStub); + } catch (RemoteException | SecurityException e) { + Log.w(TAG, "Dead object in unregisterCallbackListener.", e); + } } - } catch (RemoteException | SecurityException e) { - Log.e(TAG, "Dead object in unregisterCallback.", e); } } else { pendingCallbacks.remove(callback); @@ -1366,8 +1368,7 @@ public List getQueue() { public void addQueueItem(MediaDescriptionCompat description) { long flags = getFlags(); if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) { - throw new UnsupportedOperationException( - "This session doesn't support queue management operations"); + return; } Bundle params = new Bundle(); params.putParcelable( @@ -1381,8 +1382,7 @@ public void addQueueItem(MediaDescriptionCompat description) { public void addQueueItem(MediaDescriptionCompat description, int index) { long flags = getFlags(); if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) { - throw new UnsupportedOperationException( - "This session doesn't support queue management operations"); + return; } Bundle params = new Bundle(); params.putParcelable( @@ -1397,8 +1397,7 @@ public void addQueueItem(MediaDescriptionCompat description, int index) { public void removeQueueItem(MediaDescriptionCompat description) { long flags = getFlags(); if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) { - throw new UnsupportedOperationException( - "This session doesn't support queue management operations"); + return; } Bundle params = new Bundle(); params.putParcelable( diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionManager.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionManager.java index e028c24d3b3..1b98d57c0b5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionManager.java @@ -15,6 +15,7 @@ */ package androidx.media3.session.legacy; +import static android.os.Build.VERSION.SDK_INT; import static androidx.annotation.RestrictTo.Scope.LIBRARY; import android.content.ComponentName; @@ -22,7 +23,6 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.os.Build; import android.os.Process; import android.provider.Settings; import android.text.TextUtils; @@ -131,7 +131,7 @@ public RemoteUserInfo(@Nullable String packageName, int pid, int uid) { } else if (TextUtils.isEmpty(packageName)) { throw new IllegalArgumentException("packageName should be nonempty"); } - if (Build.VERSION.SDK_INT >= 28) { + if (SDK_INT >= 28) { impl = new RemoteUserInfoImplApi28(packageName, pid, uid); } else { // Note: We need to include IBinder to distinguish controllers in a process. @@ -242,23 +242,32 @@ public boolean isTrustedForMediaControl(MediaSessionManager.RemoteUserInfoImpl u // In that case, this check will always fail. // Alternative way is to use Context#getOpPackageName() for sending the package name, // but it's hidden so we cannot use it. - if (hasMediaControlPermission(userInfo)) { - return true; - } + int remoteUserUid = userInfo.getUid(); try { + if (SDK_INT == 23 && userInfo.getPackageName().equals(RemoteUserInfo.LEGACY_CONTROLLER)) { + // On API 23, there is no package name or caller propagation to allow caller checks for + // framework controllers, and the best we can do is to simply trust all controllers. + return true; + } ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(userInfo.getPackageName(), 0); if (applicationInfo == null) { return false; } + if (SDK_INT < 28) { + // The UID isn't propagated via the framework before API 28, so we need to look it up + // based on the package name. + remoteUserUid = applicationInfo.uid; + } } catch (PackageManager.NameNotFoundException e) { Log.d(TAG, "Package " + userInfo.getPackageName() + " doesn't exist"); return false; } - return isPermissionGranted(userInfo, PERMISSION_STATUS_BAR_SERVICE) + return remoteUserUid == Process.SYSTEM_UID + || remoteUserUid == Process.myUid() + || hasMediaControlPermission(userInfo) + || isPermissionGranted(userInfo, PERMISSION_STATUS_BAR_SERVICE) || isPermissionGranted(userInfo, PERMISSION_MEDIA_CONTENT_CONTROL) - || userInfo.getUid() == Process.SYSTEM_UID - || userInfo.getUid() == Process.myUid() || isEnabledNotificationListener(userInfo); } 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 c3f8e75bb6e..b036541b036 100644 --- a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java @@ -1001,7 +1001,7 @@ public void convertToPlayerCommands_withSkipToPreviousAction_seekToPreviousComma @Test public void - convertToPlayerCommands_withPlayFromActionsWithoutPrepareFromAction_setMediaItemCommandNotAvailable() { + convertToPlayerCommands_withPlayFromActionsWithoutPrepareFromAction_setMediaItemCommandAvailableWithoutPrepare() { PlaybackStateCompat playbackStateCompat = new PlaybackStateCompat.Builder() .setActions( @@ -1017,8 +1017,8 @@ public void convertToPlayerCommands_withSkipToPreviousAction_seekToPreviousComma /* sessionFlags= */ 0, /* isSessionReady= */ true); - assertThat(getCommandsAsList(playerCommands)) - .containsNoneOf(Player.COMMAND_SET_MEDIA_ITEM, Player.COMMAND_PREPARE); + assertThat(getCommandsAsList(playerCommands)).doesNotContain(Player.COMMAND_PREPARE); + assertThat(getCommandsAsList(playerCommands)).contains(Player.COMMAND_SET_MEDIA_ITEM); } @Test @@ -1039,8 +1039,7 @@ public void convertToPlayerCommands_withSkipToPreviousAction_seekToPreviousComma /* sessionFlags= */ 0, /* isSessionReady= */ true); - assertThat(getCommandsAsList(playerCommands)) - .containsNoneOf(Player.COMMAND_SET_MEDIA_ITEM, Player.COMMAND_PREPARE); + assertThat(getCommandsAsList(playerCommands)).doesNotContain(Player.COMMAND_SET_MEDIA_ITEM); } @Test diff --git a/libraries/session/src/test/java/androidx/media3/session/SizeAvoidingBitmapLoaderTest.java b/libraries/session/src/test/java/androidx/media3/session/SizeAvoidingBitmapLoaderTest.java new file mode 100644 index 00000000000..9e62740b22f --- /dev/null +++ b/libraries/session/src/test/java/androidx/media3/session/SizeAvoidingBitmapLoaderTest.java @@ -0,0 +1,228 @@ +/* + * 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.session; + +import static androidx.media3.test.utils.TestUtil.assertSubclassOverridesAllMethods; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.SECONDS; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.util.BitmapLoader; +import androidx.media3.datasource.DataSourceBitmapLoader; +import androidx.media3.test.utils.TestUtil; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.primitives.ImmutableIntArray; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okio.Buffer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +/** Tests for {@link SizeAvoidingBitmapLoader}. */ +@RunWith(AndroidJUnit4.class) +public class SizeAvoidingBitmapLoaderTest { + + private static final String TEST_IMAGE_PATH = "media/jpeg/non-motion-photo-shortened.jpg"; + + @Rule public final TemporaryFolder tempFolder = new TemporaryFolder(); + + private Context context; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + } + + @Test + public void overridesAllMethods() throws NoSuchMethodException { + assertSubclassOverridesAllMethods(BitmapLoader.class, SizeAvoidingBitmapLoader.class); + } + + @Test + public void decodeBitmap_sizeBelowLimit_keepsSize() throws Exception { + SizeAvoidingBitmapLoader sizeAvoidingBitmapLoader = + new SizeAvoidingBitmapLoader( + new SizeLimitedBitmapLoader( + new DataSourceBitmapLoader.Builder(context).build(), + /* maxBitmapSize= */ 199, + /* makeShared= */ true), + /* avoidSizes= */ ImmutableIntArray.of(200)); + byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH); + + Bitmap bitmap = sizeAvoidingBitmapLoader.decodeBitmap(imageData).get(10, SECONDS); + + assertThat(bitmap.getHeight()).isEqualTo(199); + } + + @Test + public void decodeBitmap_sizeAboveLimit_keepsSize() throws Exception { + SizeAvoidingBitmapLoader sizeAvoidingBitmapLoader = + new SizeAvoidingBitmapLoader( + new SizeLimitedBitmapLoader( + new DataSourceBitmapLoader.Builder(context).build(), + /* maxBitmapSize= */ 201, + /* makeShared= */ true), + /* avoidSizes= */ ImmutableIntArray.of(200)); + byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH); + + Bitmap bitmap = sizeAvoidingBitmapLoader.decodeBitmap(imageData).get(10, SECONDS); + + assertThat(bitmap.getHeight()).isEqualTo(201); + } + + @Test + public void decodeBitmap_sizeAtLimit_changesSize() throws Exception { + SizeAvoidingBitmapLoader sizeAvoidingBitmapLoader = + new SizeAvoidingBitmapLoader( + new SizeLimitedBitmapLoader( + new DataSourceBitmapLoader.Builder(context).build(), + /* maxBitmapSize= */ 200, + /* makeShared= */ true), + /* avoidSizes= */ ImmutableIntArray.of(200)); + byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH); + + Bitmap bitmap = sizeAvoidingBitmapLoader.decodeBitmap(imageData).get(10, SECONDS); + + assertThat(bitmap.getHeight()).isNotEqualTo(200); + } + + @Test + public void loadBitmap_sizeBelowLimit_keepsSize() throws Exception { + SizeAvoidingBitmapLoader sizeAvoidingBitmapLoader = + new SizeAvoidingBitmapLoader( + new SizeLimitedBitmapLoader( + new DataSourceBitmapLoader.Builder(context).build(), + /* maxBitmapSize= */ 199, + /* makeShared= */ true), + /* avoidSizes= */ ImmutableIntArray.of(200)); + byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH); + Buffer responseBody = new Buffer().write(imageData); + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody)); + Uri uri = Uri.parse(mockWebServer.url("test_path").toString()); + + Bitmap bitmap = sizeAvoidingBitmapLoader.loadBitmap(uri).get(10, SECONDS); + + assertThat(bitmap.getHeight()).isEqualTo(199); + } + + @Test + public void loadBitmap_sizeAboveLimit_keepsSize() throws Exception { + SizeAvoidingBitmapLoader sizeAvoidingBitmapLoader = + new SizeAvoidingBitmapLoader( + new SizeLimitedBitmapLoader( + new DataSourceBitmapLoader.Builder(context).build(), + /* maxBitmapSize= */ 201, + /* makeShared= */ true), + /* avoidSizes= */ ImmutableIntArray.of(200)); + byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH); + Buffer responseBody = new Buffer().write(imageData); + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody)); + Uri uri = Uri.parse(mockWebServer.url("test_path").toString()); + + Bitmap bitmap = sizeAvoidingBitmapLoader.loadBitmap(uri).get(10, SECONDS); + + assertThat(bitmap.getHeight()).isEqualTo(201); + } + + @Test + public void loadBitmap_sizeAtLimit_changesSize() throws Exception { + SizeAvoidingBitmapLoader sizeAvoidingBitmapLoader = + new SizeAvoidingBitmapLoader( + new SizeLimitedBitmapLoader( + new DataSourceBitmapLoader.Builder(context).build(), + /* maxBitmapSize= */ 200, + /* makeShared= */ true), + /* avoidSizes= */ ImmutableIntArray.of(200)); + byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH); + Buffer responseBody = new Buffer().write(imageData); + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody)); + Uri uri = Uri.parse(mockWebServer.url("test_path").toString()); + + Bitmap bitmap = sizeAvoidingBitmapLoader.loadBitmap(uri).get(10, SECONDS); + + assertThat(bitmap.getHeight()).isNotEqualTo(200); + } + + @Test + public void loadBitmapFromMetadata_sizeBelowLimit_keepsSize() throws Exception { + SizeAvoidingBitmapLoader sizeAvoidingBitmapLoader = + new SizeAvoidingBitmapLoader( + new SizeLimitedBitmapLoader( + new DataSourceBitmapLoader.Builder(context).build(), + /* maxBitmapSize= */ 199, + /* makeShared= */ true), + /* avoidSizes= */ ImmutableIntArray.of(200)); + byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH); + MediaMetadata metadata = + new MediaMetadata.Builder() + .setArtworkData(imageData, MediaMetadata.PICTURE_TYPE_FRONT_COVER) + .build(); + + Bitmap bitmap = sizeAvoidingBitmapLoader.loadBitmapFromMetadata(metadata).get(10, SECONDS); + + assertThat(bitmap.getHeight()).isEqualTo(199); + } + + @Test + public void loadBitmapFromMetadata_sizeAboveLimit_keepsSize() throws Exception { + SizeAvoidingBitmapLoader sizeAvoidingBitmapLoader = + new SizeAvoidingBitmapLoader( + new SizeLimitedBitmapLoader( + new DataSourceBitmapLoader.Builder(context).build(), + /* maxBitmapSize= */ 201, + /* makeShared= */ true), + /* avoidSizes= */ ImmutableIntArray.of(200)); + byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH); + MediaMetadata metadata = + new MediaMetadata.Builder() + .setArtworkData(imageData, MediaMetadata.PICTURE_TYPE_FRONT_COVER) + .build(); + + Bitmap bitmap = sizeAvoidingBitmapLoader.loadBitmapFromMetadata(metadata).get(10, SECONDS); + + assertThat(bitmap.getHeight()).isEqualTo(201); + } + + @Test + public void loadBitmapFromMetadata_sizeAtLimit_changesSize() throws Exception { + SizeAvoidingBitmapLoader sizeAvoidingBitmapLoader = + new SizeAvoidingBitmapLoader( + new SizeLimitedBitmapLoader( + new DataSourceBitmapLoader.Builder(context).build(), + /* maxBitmapSize= */ 200, + /* makeShared= */ true), + /* avoidSizes= */ ImmutableIntArray.of(200)); + byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH); + MediaMetadata metadata = + new MediaMetadata.Builder() + .setArtworkData(imageData, MediaMetadata.PICTURE_TYPE_FRONT_COVER) + .build(); + + Bitmap bitmap = sizeAvoidingBitmapLoader.loadBitmapFromMetadata(metadata).get(10, SECONDS); + + assertThat(bitmap.getHeight()).isNotEqualTo(200); + } +} diff --git a/libraries/test_data/build.gradle b/libraries/test_data/build.gradle index 38b53a11537..627a2be8704 100644 --- a/libraries/test_data/build.gradle +++ b/libraries/test_data/build.gradle @@ -11,6 +11,7 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android.namespace 'androidx.media3.test.data' diff --git a/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.cmd.audiosink.dump b/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.cmd.audiosink.dump index 8f230fd2fab..75d697cbeb1 100644 --- a/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.cmd.audiosink.dump +++ b/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.cmd.audiosink.dump @@ -7,8 +7,8 @@ AudioSink: sampleRate = 48000 buffer #0: time = 1000000000000 - channel[0] = -1146365702 - channel[1] = -1146365702 + channel[0] = 282783674 + channel[1] = 282783674 buffer #1: time = 1000000021333 channel[0] = -1443166734 @@ -115,8 +115,8 @@ AudioSink: channel[1] = 640973283 buffer #27: time = 1000000576000 - channel[0] = 1648316275 - channel[1] = 1648316275 + channel[0] = 667927860 + channel[1] = 667927860 buffer #28: time = 1000000597333 channel[0] = -1400252340 @@ -127,12 +127,12 @@ AudioSink: channel[1] = -109250957 buffer #30: time = 1000000640000 - channel[0] = -438551578 - channel[1] = -438551578 + channel[0] = -1213175193 + channel[1] = -1213175193 buffer #31: time = 1000000661333 - channel[0] = 260576761 - channel[1] = 260576761 + channel[0] = 130930266 + channel[1] = 130930266 buffer #32: time = 1000000682666 channel[0] = 635431592 @@ -147,20 +147,20 @@ AudioSink: channel[1] = 1712210053 buffer #35: time = 1000000746666 - channel[0] = 1604220979 - channel[1] = 1604220979 + channel[0] = -1176226604 + channel[1] = -1176226604 buffer #36: time = 1000000768000 channel[0] = 960464384 channel[1] = 960464384 buffer #37: time = 1000000789333 - channel[0] = 1846787942 - channel[1] = 1846787942 + channel[0] = -712964186 + channel[1] = -712964186 buffer #38: time = 1000000810666 - channel[0] = -9325471 - channel[1] = -9325471 + channel[0] = -1435730719 + channel[1] = -1435730719 buffer #39: time = 1000000832000 channel[0] = -1658363839 diff --git a/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.cmd.persist.1.audiosink.dump b/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.cmd.persist.1.audiosink.dump index 8f230fd2fab..75d697cbeb1 100644 --- a/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.cmd.persist.1.audiosink.dump +++ b/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.cmd.persist.1.audiosink.dump @@ -7,8 +7,8 @@ AudioSink: sampleRate = 48000 buffer #0: time = 1000000000000 - channel[0] = -1146365702 - channel[1] = -1146365702 + channel[0] = 282783674 + channel[1] = 282783674 buffer #1: time = 1000000021333 channel[0] = -1443166734 @@ -115,8 +115,8 @@ AudioSink: channel[1] = 640973283 buffer #27: time = 1000000576000 - channel[0] = 1648316275 - channel[1] = 1648316275 + channel[0] = 667927860 + channel[1] = 667927860 buffer #28: time = 1000000597333 channel[0] = -1400252340 @@ -127,12 +127,12 @@ AudioSink: channel[1] = -109250957 buffer #30: time = 1000000640000 - channel[0] = -438551578 - channel[1] = -438551578 + channel[0] = -1213175193 + channel[1] = -1213175193 buffer #31: time = 1000000661333 - channel[0] = 260576761 - channel[1] = 260576761 + channel[0] = 130930266 + channel[1] = 130930266 buffer #32: time = 1000000682666 channel[0] = 635431592 @@ -147,20 +147,20 @@ AudioSink: channel[1] = 1712210053 buffer #35: time = 1000000746666 - channel[0] = 1604220979 - channel[1] = 1604220979 + channel[0] = -1176226604 + channel[1] = -1176226604 buffer #36: time = 1000000768000 channel[0] = 960464384 channel[1] = 960464384 buffer #37: time = 1000000789333 - channel[0] = 1846787942 - channel[1] = 1846787942 + channel[0] = -712964186 + channel[1] = -712964186 buffer #38: time = 1000000810666 - channel[0] = -9325471 - channel[1] = -9325471 + channel[0] = -1435730719 + channel[1] = -1435730719 buffer #39: time = 1000000832000 channel[0] = -1658363839 diff --git a/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.persist.2.audiosink.dump b/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.persist.2.audiosink.dump index 8f230fd2fab..75d697cbeb1 100644 --- a/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.persist.2.audiosink.dump +++ b/libraries/test_data/src/test/assets/audiosinkdumps/mp4/sample_mhm1_prefaudiolang.mp4.persist.2.audiosink.dump @@ -7,8 +7,8 @@ AudioSink: sampleRate = 48000 buffer #0: time = 1000000000000 - channel[0] = -1146365702 - channel[1] = -1146365702 + channel[0] = 282783674 + channel[1] = 282783674 buffer #1: time = 1000000021333 channel[0] = -1443166734 @@ -115,8 +115,8 @@ AudioSink: channel[1] = 640973283 buffer #27: time = 1000000576000 - channel[0] = 1648316275 - channel[1] = 1648316275 + channel[0] = 667927860 + channel[1] = 667927860 buffer #28: time = 1000000597333 channel[0] = -1400252340 @@ -127,12 +127,12 @@ AudioSink: channel[1] = -109250957 buffer #30: time = 1000000640000 - channel[0] = -438551578 - channel[1] = -438551578 + channel[0] = -1213175193 + channel[1] = -1213175193 buffer #31: time = 1000000661333 - channel[0] = 260576761 - channel[1] = 260576761 + channel[0] = 130930266 + channel[1] = 130930266 buffer #32: time = 1000000682666 channel[0] = 635431592 @@ -147,20 +147,20 @@ AudioSink: channel[1] = 1712210053 buffer #35: time = 1000000746666 - channel[0] = 1604220979 - channel[1] = 1604220979 + channel[0] = -1176226604 + channel[1] = -1176226604 buffer #36: time = 1000000768000 channel[0] = 960464384 channel[1] = 960464384 buffer #37: time = 1000000789333 - channel[0] = 1846787942 - channel[1] = 1846787942 + channel[0] = -712964186 + channel[1] = -712964186 buffer #38: time = 1000000810666 - channel[0] = -9325471 - channel[1] = -9325471 + channel[0] = -1435730719 + channel[1] = -1435730719 buffer #39: time = 1000000832000 channel[0] = -1658363839 diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.0.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.0.dump new file mode 100644 index 00000000000..36255c275ca --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.0.dump @@ -0,0 +1,155 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + track duration = 2741000 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + containerMimeType = audio/flac + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[VC: TITLE=test��titl, VC: ARTIST=test��artis] + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.1.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.1.dump new file mode 100644 index 00000000000..10c67aabf7f --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.1.dump @@ -0,0 +1,115 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 362432 + sample count = 23 + track duration = 2741000 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + containerMimeType = audio/flac + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[VC: TITLE=test��titl, VC: ARTIST=test��artis] + sample 0: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 1: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 2: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 3: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 4: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 5: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 6: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 7: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 8: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 9: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 10: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 11: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 12: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 13: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 14: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 15: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 16: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 17: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 18: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 19: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 20: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 21: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 22: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.2.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.2.dump new file mode 100644 index 00000000000..8776afc490a --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.2.dump @@ -0,0 +1,71 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 182208 + sample count = 12 + track duration = 2741000 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + containerMimeType = audio/flac + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[VC: TITLE=test��titl, VC: ARTIST=test��artis] + sample 0: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 1: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 2: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 3: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 4: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 5: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 6: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 7: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 8: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 9: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 10: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 11: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.3.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.3.dump new file mode 100644 index 00000000000..69cd80aa767 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.3.dump @@ -0,0 +1,31 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 18368 + sample count = 2 + track duration = 2741000 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + containerMimeType = audio/flac + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[VC: TITLE=test��titl, VC: ARTIST=test��artis] + sample 0: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 1: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.unknown_length.dump new file mode 100644 index 00000000000..36255c275ca --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/flac/bear_with_vorbis_comments_invalid_mutf8_raw.unknown_length.dump @@ -0,0 +1,155 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=8880]] + getPosition(1) = [[timeUs=0, position=8880]] + getPosition(1370500) = [[timeUs=1365333, position=90825], [timeUs=1450666, position=96601]] + getPosition(2741000) = [[timeUs=2645333, position=169482]] +numberOfTracks = 1 +track 0: + total output bytes = 526272 + sample count = 33 + track duration = 2741000 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + containerMimeType = audio/flac + sampleMimeType = audio/raw + maxInputSize = 16384 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + metadata = entries=[VC: TITLE=test��titl, VC: ARTIST=test��artis] + sample 0: + time = 0 + flags = 1 + data = length 16384, hash 61D2C5C2 + sample 1: + time = 85333 + flags = 1 + data = length 16384, hash E6D7F214 + sample 2: + time = 170666 + flags = 1 + data = length 16384, hash 59BF0D5D + sample 3: + time = 256000 + flags = 1 + data = length 16384, hash 3625F468 + sample 4: + time = 341333 + flags = 1 + data = length 16384, hash F66A323 + sample 5: + time = 426666 + flags = 1 + data = length 16384, hash CDBAE629 + sample 6: + time = 512000 + flags = 1 + data = length 16384, hash 536F3A91 + sample 7: + time = 597333 + flags = 1 + data = length 16384, hash D4F35C9C + sample 8: + time = 682666 + flags = 1 + data = length 16384, hash EE04CEBF + sample 9: + time = 768000 + flags = 1 + data = length 16384, hash 647E2A67 + sample 10: + time = 853333 + flags = 1 + data = length 16384, hash 31583F2C + sample 11: + time = 938666 + flags = 1 + data = length 16384, hash E433A93D + sample 12: + time = 1024000 + flags = 1 + data = length 16384, hash 5E1C7051 + sample 13: + time = 1109333 + flags = 1 + data = length 16384, hash 43E6E358 + sample 14: + time = 1194666 + flags = 1 + data = length 16384, hash 5DC1B256 + sample 15: + time = 1280000 + flags = 1 + data = length 16384, hash 3D9D95CF + sample 16: + time = 1365333 + flags = 1 + data = length 16384, hash 2A5BD2C0 + sample 17: + time = 1450666 + flags = 1 + data = length 16384, hash 93E25061 + sample 18: + time = 1536000 + flags = 1 + data = length 16384, hash B81793D8 + sample 19: + time = 1621333 + flags = 1 + data = length 16384, hash 1A3BD49F + sample 20: + time = 1706666 + flags = 1 + data = length 16384, hash FB672FF1 + sample 21: + time = 1792000 + flags = 1 + data = length 16384, hash 48AB8B45 + sample 22: + time = 1877333 + flags = 1 + data = length 16384, hash 13C9640A + sample 23: + time = 1962666 + flags = 1 + data = length 16384, hash 499E4A0B + sample 24: + time = 2048000 + flags = 1 + data = length 16384, hash F9A783E6 + sample 25: + time = 2133333 + flags = 1 + data = length 16384, hash D2B77598 + sample 26: + time = 2218666 + flags = 1 + data = length 16384, hash CE5B826C + sample 27: + time = 2304000 + flags = 1 + data = length 16384, hash E99EE956 + sample 28: + time = 2389333 + flags = 1 + data = length 16384, hash F2DB1486 + sample 29: + time = 2474666 + flags = 1 + data = length 16384, hash 1636EAB + sample 30: + time = 2560000 + flags = 1 + data = length 16384, hash 23457C08 + sample 31: + time = 2645333 + flags = 1 + data = length 16384, hash 30EB8381 + sample 32: + time = 2730666 + flags = 1 + data = length 1984, hash 59CFDE1B +tracksEnded = true 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.0.dump similarity index 100% rename from libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.0.dump rename to libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.0.dump 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.1.dump similarity index 100% rename from libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.1.dump rename to libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.1.dump 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.2.dump similarity index 100% rename from libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.2.dump rename to libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.2.dump 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.3.dump similarity index 100% rename from libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.3.dump rename to libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.3.dump 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 deleted file mode 100644 index cf0b315ef69..00000000000 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.0.dump +++ /dev/null @@ -1,2430 +0,0 @@ -seekMap: - isSeekable = true - duration = 8333333 - getPosition(0) = [[timeUs=0, position=871]] - getPosition(1) = [[timeUs=0, position=871]] - getPosition(4166666) = [[timeUs=0, position=871]] - getPosition(8333333) = [[timeUs=0, position=871]] -numberOfTracks = 1 -track 0: - total output bytes = 44195 - sample count = 600 - format 0: - averageBitrate = 17678 - peakBitrate = 17678 - id = 1 - containerMimeType = video/mp4 - sampleMimeType = video/avc - codecs = avc1.640028 - maxNumReorderSamples = 2 - width = 1920 - height = 1080 - colorInfo: - 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 - sample 0: - time = 0 - flags = 1 - data = length 1120, hash 352E33B0 - sample 1: - time = 133333 - flags = 0 - data = length 71, hash E6801693 - sample 2: - time = 66666 - flags = 0 - data = length 68, hash 83DC1BAB - sample 3: - time = 33333 - flags = 0 - data = length 68, hash 66214C04 - sample 4: - time = 100000 - flags = 0 - data = length 68, hash 4605E639 - sample 5: - time = 266666 - flags = 0 - data = length 77, hash 76DDE101 - sample 6: - time = 200000 - flags = 0 - data = length 70, hash 98EFD089 - sample 7: - time = 166666 - flags = 0 - data = length 68, hash 88475341 - sample 8: - time = 233333 - flags = 0 - data = length 68, hash 682BED74 - sample 9: - time = 400000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 10: - time = 333333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 11: - time = 300000 - flags = 0 - data = length 68, hash E8DCF97C - sample 12: - time = 366666 - flags = 0 - data = length 68, hash C8C193B0 - sample 13: - time = 533333 - flags = 0 - data = length 77, hash 6967C98D - sample 14: - time = 466666 - flags = 0 - data = length 70, hash 5B33157E - sample 15: - time = 433333 - flags = 0 - data = length 68, hash 3D0223FA - sample 16: - time = 500000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 17: - time = 666666 - flags = 0 - data = length 77, hash 434E1BAF - sample 18: - time = 600000 - flags = 0 - data = length 70, hash ECF81CBA - sample 19: - time = 566666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 20: - time = 633333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 21: - time = 800000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 22: - time = 733333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 23: - time = 700000 - flags = 0 - data = length 68, hash BFBDD172 - sample 24: - time = 766666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 25: - time = 933333 - flags = 0 - data = length 77, hash C6D21537 - sample 26: - time = 866666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 27: - time = 833333 - flags = 0 - data = length 68, hash 205377AD - sample 28: - time = 900000 - flags = 0 - data = length 68, hash 3811E2 - sample 29: - time = 1066666 - flags = 0 - data = length 77, hash B8F74A5C - sample 30: - time = 1000000 - flags = 0 - data = length 70, hash F383DB6C - sample 31: - time = 966666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 32: - time = 1033333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 33: - time = 1200000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 34: - time = 1133333 - flags = 0 - data = length 70, hash B92866C9 - sample 35: - time = 1100000 - flags = 0 - data = length 68, hash 66214C04 - sample 36: - time = 1166666 - flags = 0 - data = length 68, hash 4605E639 - sample 37: - time = 1333333 - flags = 0 - data = length 77, hash E5E3D005 - sample 38: - time = 1266666 - flags = 0 - data = length 70, hash E9E98F05 - sample 39: - time = 1233333 - flags = 0 - data = length 68, hash 88475341 - sample 40: - time = 1300000 - flags = 0 - data = length 68, hash 682BED74 - sample 41: - time = 1466666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 42: - time = 1400000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 43: - time = 1366666 - flags = 0 - data = length 68, hash E8DCF97C - sample 44: - time = 1433333 - flags = 0 - data = length 68, hash C8C193B0 - sample 45: - time = 1600000 - flags = 0 - data = length 77, hash 6967C98D - sample 46: - time = 1533333 - flags = 0 - data = length 70, hash 5B33157E - sample 47: - time = 1500000 - flags = 0 - data = length 68, hash 3D0223FA - sample 48: - time = 1566666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 49: - time = 1733333 - flags = 0 - data = length 77, hash 434E1BAF - sample 50: - time = 1666666 - flags = 0 - data = length 70, hash ECF81CBA - sample 51: - time = 1633333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 52: - time = 1700000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 53: - time = 1866666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 54: - time = 1800000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 55: - time = 1766666 - flags = 0 - data = length 68, hash BFBDD172 - sample 56: - time = 1833333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 57: - time = 2000000 - flags = 0 - data = length 77, hash C6D21537 - sample 58: - time = 1933333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 59: - time = 1900000 - flags = 0 - data = length 68, hash 205377AD - sample 60: - time = 1966666 - flags = 0 - data = length 68, hash 3811E2 - sample 61: - time = 2133333 - flags = 0 - data = length 77, hash B8F74A5C - sample 62: - time = 2066666 - flags = 0 - data = length 70, hash F383DB6C - sample 63: - time = 2033333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 64: - time = 2100000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 65: - time = 2266666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 66: - time = 2200000 - flags = 0 - data = length 70, hash B92866C9 - sample 67: - time = 2166666 - flags = 0 - data = length 68, hash 66214C04 - sample 68: - time = 2233333 - flags = 0 - data = length 68, hash 4605E639 - sample 69: - time = 2400000 - flags = 0 - data = length 77, hash E5E3D005 - sample 70: - time = 2333333 - flags = 0 - data = length 70, hash E9E98F05 - sample 71: - time = 2300000 - flags = 0 - data = length 68, hash 88475341 - sample 72: - time = 2366666 - flags = 0 - data = length 68, hash 682BED74 - sample 73: - time = 2533333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 74: - time = 2466666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 75: - time = 2433333 - flags = 0 - data = length 68, hash E8DCF97C - sample 76: - time = 2500000 - flags = 0 - data = length 68, hash C8C193B0 - sample 77: - time = 2666666 - flags = 0 - data = length 77, hash 6967C98D - sample 78: - time = 2600000 - flags = 0 - data = length 70, hash 5B33157E - sample 79: - time = 2566666 - flags = 0 - data = length 68, hash 3D0223FA - sample 80: - time = 2633333 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 81: - time = 2800000 - flags = 0 - data = length 77, hash 434E1BAF - sample 82: - time = 2733333 - flags = 0 - data = length 70, hash ECF81CBA - sample 83: - time = 2700000 - flags = 0 - data = length 68, hash 9D97CA35 - sample 84: - time = 2766666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 85: - time = 2933333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 86: - time = 2866666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 87: - time = 2833333 - flags = 0 - data = length 68, hash BFBDD172 - sample 88: - time = 2900000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 89: - time = 3066666 - flags = 0 - data = length 77, hash C6D21537 - sample 90: - time = 3000000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 91: - time = 2966666 - flags = 0 - data = length 68, hash 205377AD - sample 92: - time = 3033333 - flags = 0 - data = length 68, hash 3811E2 - sample 93: - time = 3200000 - flags = 0 - data = length 77, hash B8F74A5C - sample 94: - time = 3133333 - flags = 0 - data = length 70, hash F383DB6C - sample 95: - time = 3100000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 96: - time = 3166666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 97: - time = 3333333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 98: - time = 3266666 - flags = 0 - data = length 70, hash B92866C9 - sample 99: - time = 3233333 - flags = 0 - data = length 68, hash 66214C04 - sample 100: - time = 3300000 - flags = 0 - data = length 68, hash 4605E639 - sample 101: - time = 3466666 - flags = 0 - data = length 77, hash E5E3D005 - sample 102: - time = 3400000 - flags = 0 - data = length 70, hash E9E98F05 - sample 103: - time = 3366666 - flags = 0 - data = length 68, hash 88475341 - sample 104: - time = 3433333 - flags = 0 - data = length 68, hash 682BED74 - sample 105: - time = 3600000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 106: - time = 3533333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 107: - time = 3500000 - flags = 0 - data = length 68, hash E8DCF97C - sample 108: - time = 3566666 - flags = 0 - data = length 68, hash C8C193B0 - sample 109: - time = 3733333 - flags = 0 - data = length 77, hash 6967C98D - sample 110: - time = 3666666 - flags = 0 - data = length 70, hash 5B33157E - sample 111: - time = 3633333 - flags = 0 - data = length 68, hash 3D0223FA - sample 112: - time = 3700000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 113: - time = 3866666 - flags = 0 - data = length 77, hash 434E1BAF - sample 114: - time = 3800000 - flags = 0 - data = length 70, hash ECF81CBA - sample 115: - time = 3766666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 116: - time = 3833333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 117: - time = 4000000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 118: - time = 3933333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 119: - time = 3900000 - flags = 0 - data = length 68, hash BFBDD172 - sample 120: - time = 3966666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 121: - time = 4133333 - flags = 0 - data = length 77, hash C6D21537 - sample 122: - time = 4066666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 123: - time = 4033333 - flags = 0 - data = length 68, hash 205377AD - sample 124: - time = 4100000 - flags = 0 - data = length 68, hash 3811E2 - sample 125: - time = 4266666 - flags = 0 - data = length 77, hash B8F74A5C - sample 126: - time = 4200000 - flags = 0 - data = length 70, hash F383DB6C - sample 127: - time = 4166666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 128: - time = 4233333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 129: - time = 4400000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 130: - time = 4333333 - flags = 0 - data = length 70, hash B92866C9 - sample 131: - time = 4300000 - flags = 0 - data = length 68, hash 66214C04 - sample 132: - time = 4366666 - flags = 0 - data = length 68, hash 4605E639 - sample 133: - time = 4533333 - flags = 0 - data = length 77, hash E5E3D005 - sample 134: - time = 4466666 - flags = 0 - data = length 70, hash E9E98F05 - sample 135: - time = 4433333 - flags = 0 - data = length 68, hash 88475341 - sample 136: - time = 4500000 - flags = 0 - data = length 68, hash 682BED74 - sample 137: - time = 4666666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 138: - time = 4600000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 139: - time = 4566666 - flags = 0 - data = length 68, hash E8DCF97C - sample 140: - time = 4633333 - flags = 0 - data = length 68, hash C8C193B0 - sample 141: - time = 4800000 - flags = 0 - data = length 77, hash 6967C98D - sample 142: - time = 4733333 - flags = 0 - data = length 70, hash 5B33157E - sample 143: - time = 4700000 - flags = 0 - data = length 68, hash 3D0223FA - sample 144: - time = 4766666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 145: - time = 4933333 - flags = 0 - data = length 77, hash 434E1BAF - sample 146: - time = 4866666 - flags = 0 - data = length 70, hash ECF81CBA - sample 147: - time = 4833333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 148: - time = 4900000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 149: - time = 5066666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 150: - time = 5000000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 151: - time = 4966666 - flags = 0 - data = length 68, hash BFBDD172 - sample 152: - time = 5033333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 153: - time = 5200000 - flags = 0 - data = length 77, hash C6D21537 - sample 154: - time = 5133333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 155: - time = 5100000 - flags = 0 - data = length 68, hash 205377AD - sample 156: - time = 5166666 - flags = 0 - data = length 68, hash 3811E2 - sample 157: - time = 5333333 - flags = 0 - data = length 77, hash B8F74A5C - sample 158: - time = 5266666 - flags = 0 - data = length 70, hash F383DB6C - sample 159: - time = 5233333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 160: - time = 5300000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 161: - time = 5466666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 162: - time = 5400000 - flags = 0 - data = length 70, hash B92866C9 - sample 163: - time = 5366666 - flags = 0 - data = length 68, hash 66214C04 - sample 164: - time = 5433333 - flags = 0 - data = length 68, hash 4605E639 - sample 165: - time = 5600000 - flags = 0 - data = length 77, hash E5E3D005 - sample 166: - time = 5533333 - flags = 0 - data = length 70, hash E9E98F05 - sample 167: - time = 5500000 - flags = 0 - data = length 68, hash 88475341 - sample 168: - time = 5566666 - flags = 0 - data = length 68, hash 682BED74 - sample 169: - time = 5733333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 170: - time = 5666666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 171: - time = 5633333 - flags = 0 - data = length 68, hash E8DCF97C - sample 172: - time = 5700000 - flags = 0 - data = length 68, hash C8C193B0 - sample 173: - time = 5866666 - flags = 0 - data = length 77, hash 6967C98D - sample 174: - time = 5800000 - flags = 0 - data = length 70, hash 5B33157E - sample 175: - time = 5766666 - flags = 0 - data = length 68, hash 3D0223FA - sample 176: - time = 5833333 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 177: - time = 6000000 - flags = 0 - data = length 77, hash 434E1BAF - sample 178: - time = 5933333 - flags = 0 - data = length 70, hash ECF81CBA - sample 179: - time = 5900000 - flags = 0 - data = length 68, hash 9D97CA35 - sample 180: - time = 5966666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 181: - time = 6133333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 182: - time = 6066666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 183: - time = 6033333 - flags = 0 - data = length 68, hash BFBDD172 - sample 184: - time = 6100000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 185: - time = 6266666 - flags = 0 - data = length 77, hash C6D21537 - sample 186: - time = 6200000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 187: - time = 6166666 - flags = 0 - data = length 68, hash 205377AD - sample 188: - time = 6233333 - flags = 0 - data = length 68, hash 3811E2 - sample 189: - time = 6400000 - flags = 0 - data = length 77, hash B8F74A5C - sample 190: - time = 6333333 - flags = 0 - data = length 70, hash F383DB6C - sample 191: - time = 6300000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 192: - time = 6366666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 193: - time = 6533333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 194: - time = 6466666 - flags = 0 - data = length 70, hash B92866C9 - sample 195: - time = 6433333 - flags = 0 - data = length 68, hash 66214C04 - sample 196: - time = 6500000 - flags = 0 - data = length 68, hash 4605E639 - sample 197: - time = 6666666 - flags = 0 - data = length 77, hash E5E3D005 - sample 198: - time = 6600000 - flags = 0 - data = length 70, hash E9E98F05 - sample 199: - time = 6566666 - flags = 0 - data = length 68, hash 88475341 - sample 200: - time = 6633333 - flags = 0 - data = length 68, hash 682BED74 - sample 201: - time = 6800000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 202: - time = 6733333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 203: - time = 6700000 - flags = 0 - data = length 68, hash E8DCF97C - sample 204: - time = 6766666 - flags = 0 - data = length 68, hash C8C193B0 - sample 205: - time = 6933333 - flags = 0 - data = length 77, hash 6967C98D - sample 206: - time = 6866666 - flags = 0 - data = length 70, hash 5B33157E - sample 207: - time = 6833333 - flags = 0 - data = length 68, hash 3D0223FA - sample 208: - time = 6900000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 209: - time = 7066666 - flags = 0 - data = length 77, hash 434E1BAF - sample 210: - time = 7000000 - flags = 0 - data = length 70, hash ECF81CBA - sample 211: - time = 6966666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 212: - time = 7033333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 213: - time = 7200000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 214: - time = 7133333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 215: - time = 7100000 - flags = 0 - data = length 68, hash BFBDD172 - sample 216: - time = 7166666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 217: - time = 7333333 - flags = 0 - data = length 77, hash C6D21537 - sample 218: - time = 7266666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 219: - time = 7233333 - flags = 0 - data = length 68, hash 205377AD - sample 220: - time = 7300000 - flags = 0 - data = length 68, hash 3811E2 - sample 221: - time = 7466666 - flags = 0 - data = length 77, hash B8F74A5C - sample 222: - time = 7400000 - flags = 0 - data = length 70, hash F383DB6C - sample 223: - time = 7366666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 224: - time = 7433333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 225: - time = 7600000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 226: - time = 7533333 - flags = 0 - data = length 70, hash B92866C9 - sample 227: - time = 7500000 - flags = 0 - data = length 68, hash 66214C04 - sample 228: - time = 7566666 - flags = 0 - data = length 68, hash 4605E639 - sample 229: - time = 7733333 - flags = 0 - data = length 77, hash E5E3D005 - sample 230: - time = 7666666 - flags = 0 - data = length 70, hash E9E98F05 - sample 231: - time = 7633333 - flags = 0 - data = length 68, hash 88475341 - sample 232: - time = 7700000 - flags = 0 - data = length 68, hash 682BED74 - sample 233: - time = 7866666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 234: - time = 7800000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 235: - time = 7766666 - flags = 0 - data = length 68, hash E8DCF97C - sample 236: - time = 7833333 - flags = 0 - data = length 68, hash C8C193B0 - sample 237: - time = 8000000 - flags = 0 - data = length 77, hash 6967C98D - sample 238: - time = 7933333 - flags = 0 - data = length 70, hash 5B33157E - sample 239: - time = 7900000 - flags = 0 - data = length 68, hash 3D0223FA - sample 240: - time = 7966666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 241: - time = 8133333 - flags = 0 - data = length 77, hash 434E1BAF - sample 242: - time = 8066666 - flags = 0 - data = length 70, hash ECF81CBA - sample 243: - time = 8033333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 244: - time = 8100000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 245: - time = 8266666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 246: - time = 8200000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 247: - time = 8166666 - flags = 0 - data = length 68, hash BFBDD172 - sample 248: - time = 8233333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 249: - time = 8300000 - flags = 0 - data = length 77, hash AB0BE9E8 - sample 250: - time = 8333333 - flags = 1 - data = length 423, hash 724FF09B - sample 251: - time = 8466666 - flags = 0 - data = length 71, hash E6801693 - sample 252: - time = 8400000 - flags = 0 - data = length 68, hash 83DC1BAB - sample 253: - time = 8366666 - flags = 0 - data = length 68, hash 66214C05 - sample 254: - time = 8433333 - flags = 0 - data = length 68, hash 4605E638 - sample 255: - time = 8600000 - flags = 0 - data = length 77, hash 76DDE100 - sample 256: - time = 8533333 - flags = 0 - data = length 70, hash 98EFD089 - sample 257: - time = 8500000 - flags = 0 - data = length 68, hash 88475340 - sample 258: - time = 8566666 - flags = 0 - data = length 68, hash 682BED75 - sample 259: - time = 8733333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 260: - time = 8666666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 261: - time = 8633333 - flags = 0 - data = length 68, hash E8DCF97D - sample 262: - time = 8700000 - flags = 0 - data = length 68, hash C8C193B1 - sample 263: - time = 8866666 - flags = 0 - data = length 77, hash 6967C98D - sample 264: - time = 8800000 - flags = 0 - data = length 70, hash 5B33157D - sample 265: - time = 8766666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 266: - time = 8833333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 267: - time = 9000000 - flags = 0 - data = length 77, hash 434E1BAF - sample 268: - time = 8933333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 269: - time = 8900000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 270: - time = 8966666 - flags = 0 - data = length 68, hash 7D7C646A - sample 271: - time = 9133333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 272: - time = 9066666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 273: - time = 9033333 - flags = 0 - data = length 68, hash BFBDD171 - sample 274: - time = 9100000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 275: - time = 9266666 - flags = 0 - data = length 77, hash C6D21537 - sample 276: - time = 9200000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 277: - time = 9166666 - flags = 0 - data = length 68, hash 205377AE - sample 278: - time = 9233333 - flags = 0 - data = length 68, hash 3811E1 - sample 279: - time = 9400000 - flags = 0 - data = length 77, hash B8F74A5C - sample 280: - time = 9333333 - flags = 0 - data = length 70, hash F383DB6D - sample 281: - time = 9300000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 282: - time = 9366666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 283: - time = 9533333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 284: - time = 9466666 - flags = 0 - data = length 70, hash B92866C9 - sample 285: - time = 9433333 - flags = 0 - data = length 68, hash 66214C05 - sample 286: - time = 9500000 - flags = 0 - data = length 68, hash 4605E638 - sample 287: - time = 9666666 - flags = 0 - data = length 77, hash E5E3D004 - sample 288: - time = 9600000 - flags = 0 - data = length 70, hash E9E98F05 - sample 289: - time = 9566666 - flags = 0 - data = length 68, hash 88475340 - sample 290: - time = 9633333 - flags = 0 - data = length 68, hash 682BED75 - sample 291: - time = 9800000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 292: - time = 9733333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 293: - time = 9700000 - flags = 0 - data = length 68, hash E8DCF97D - sample 294: - time = 9766666 - flags = 0 - data = length 68, hash C8C193B1 - sample 295: - time = 9933333 - flags = 0 - data = length 77, hash 6967C98D - sample 296: - time = 9866666 - flags = 0 - data = length 70, hash 5B33157D - sample 297: - time = 9833333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 298: - time = 9900000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 299: - time = 10066666 - flags = 0 - data = length 77, hash 434E1BAF - sample 300: - time = 10000000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 301: - time = 9966666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 302: - time = 10033333 - flags = 0 - data = length 68, hash 7D7C646A - sample 303: - time = 10200000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 304: - time = 10133333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 305: - time = 10100000 - flags = 0 - data = length 68, hash BFBDD171 - sample 306: - time = 10166666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 307: - time = 10333333 - flags = 0 - data = length 77, hash C6D21537 - sample 308: - time = 10266666 - flags = 0 - data = length 70, hash AF7E4C31 - sample 309: - time = 10233333 - flags = 0 - data = length 68, hash 205377AE - sample 310: - time = 10300000 - flags = 0 - data = length 68, hash 3811E1 - sample 311: - time = 10466666 - flags = 0 - data = length 77, hash B8F74A5C - sample 312: - time = 10400000 - flags = 0 - data = length 70, hash F383DB6D - sample 313: - time = 10366666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 314: - time = 10433333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 315: - time = 10600000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 316: - time = 10533333 - flags = 0 - data = length 70, hash B92866C9 - sample 317: - time = 10500000 - flags = 0 - data = length 68, hash 66214C05 - sample 318: - time = 10566666 - flags = 0 - data = length 68, hash 4605E638 - sample 319: - time = 10733333 - flags = 0 - data = length 77, hash E5E3D004 - sample 320: - time = 10666666 - flags = 0 - data = length 70, hash E9E98F05 - sample 321: - time = 10633333 - flags = 0 - data = length 68, hash 88475340 - sample 322: - time = 10700000 - flags = 0 - data = length 68, hash 682BED75 - sample 323: - time = 10866666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 324: - time = 10800000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 325: - time = 10766666 - flags = 0 - data = length 68, hash E8DCF97D - sample 326: - time = 10833333 - flags = 0 - data = length 68, hash C8C193B1 - sample 327: - time = 11000000 - flags = 0 - data = length 77, hash 6967C98D - sample 328: - time = 10933333 - flags = 0 - data = length 70, hash 5B33157D - sample 329: - time = 10900000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 330: - time = 10966666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 331: - time = 11133333 - flags = 0 - data = length 77, hash 434E1BAF - sample 332: - time = 11066666 - flags = 0 - data = length 70, hash ECF81CB9 - sample 333: - time = 11033333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 334: - time = 11100000 - flags = 0 - data = length 68, hash 7D7C646A - sample 335: - time = 11266666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 336: - time = 11200000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 337: - time = 11166666 - flags = 0 - data = length 68, hash BFBDD171 - sample 338: - time = 11233333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 339: - time = 11400000 - flags = 0 - data = length 77, hash C6D21537 - sample 340: - time = 11333333 - flags = 0 - data = length 70, hash AF7E4C31 - sample 341: - time = 11300000 - flags = 0 - data = length 68, hash 205377AE - sample 342: - time = 11366666 - flags = 0 - data = length 68, hash 3811E1 - sample 343: - time = 11533333 - flags = 0 - data = length 77, hash B8F74A5C - sample 344: - time = 11466666 - flags = 0 - data = length 70, hash F383DB6D - sample 345: - time = 11433333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 346: - time = 11500000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 347: - time = 11666666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 348: - time = 11600000 - flags = 0 - data = length 70, hash B92866C9 - sample 349: - time = 11566666 - flags = 0 - data = length 68, hash 66214C05 - sample 350: - time = 11633333 - flags = 0 - data = length 68, hash 4605E638 - sample 351: - time = 11800000 - flags = 0 - data = length 77, hash E5E3D004 - sample 352: - time = 11733333 - flags = 0 - data = length 70, hash E9E98F05 - sample 353: - time = 11700000 - flags = 0 - data = length 68, hash 88475340 - sample 354: - time = 11766666 - flags = 0 - data = length 68, hash 682BED75 - sample 355: - time = 11933333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 356: - time = 11866666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 357: - time = 11833333 - flags = 0 - data = length 68, hash E8DCF97D - sample 358: - time = 11900000 - flags = 0 - data = length 68, hash C8C193B1 - sample 359: - time = 12066666 - flags = 0 - data = length 77, hash 6967C98D - sample 360: - time = 12000000 - flags = 0 - data = length 70, hash 5B33157D - sample 361: - time = 11966666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 362: - time = 12033333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 363: - time = 12200000 - flags = 0 - data = length 77, hash 434E1BAF - sample 364: - time = 12133333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 365: - time = 12100000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 366: - time = 12166666 - flags = 0 - data = length 68, hash 7D7C646A - sample 367: - time = 12333333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 368: - time = 12266666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 369: - time = 12233333 - flags = 0 - data = length 68, hash BFBDD171 - sample 370: - time = 12300000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 371: - time = 12466666 - flags = 0 - data = length 77, hash C6D21537 - sample 372: - time = 12400000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 373: - time = 12366666 - flags = 0 - data = length 68, hash 205377AE - sample 374: - time = 12433333 - flags = 0 - data = length 68, hash 3811E1 - sample 375: - time = 12600000 - flags = 0 - data = length 77, hash B8F74A5C - sample 376: - time = 12533333 - flags = 0 - data = length 70, hash F383DB6D - sample 377: - time = 12500000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 378: - time = 12566666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 379: - time = 12733333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 380: - time = 12666666 - flags = 0 - data = length 70, hash B92866C9 - sample 381: - time = 12633333 - flags = 0 - data = length 68, hash 66214C05 - sample 382: - time = 12700000 - flags = 0 - data = length 68, hash 4605E638 - sample 383: - time = 12866666 - flags = 0 - data = length 77, hash E5E3D004 - sample 384: - time = 12800000 - flags = 0 - data = length 70, hash E9E98F05 - sample 385: - time = 12766666 - flags = 0 - data = length 68, hash 88475340 - sample 386: - time = 12833333 - flags = 0 - data = length 68, hash 682BED75 - sample 387: - time = 13000000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 388: - time = 12933333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 389: - time = 12900000 - flags = 0 - data = length 68, hash E8DCF97D - sample 390: - time = 12966666 - flags = 0 - data = length 68, hash C8C193B1 - sample 391: - time = 13133333 - flags = 0 - data = length 77, hash 6967C98D - sample 392: - time = 13066666 - flags = 0 - data = length 70, hash 5B33157D - sample 393: - time = 13033333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 394: - time = 13100000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 395: - time = 13266666 - flags = 0 - data = length 77, hash 434E1BAF - sample 396: - time = 13200000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 397: - time = 13166666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 398: - time = 13233333 - flags = 0 - data = length 68, hash 7D7C646A - sample 399: - time = 13400000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 400: - time = 13333333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 401: - time = 13300000 - flags = 0 - data = length 68, hash BFBDD171 - sample 402: - time = 13366666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 403: - time = 13533333 - flags = 0 - data = length 77, hash C6D21537 - sample 404: - time = 13466666 - flags = 0 - data = length 70, hash AF7E4C31 - sample 405: - time = 13433333 - flags = 0 - data = length 68, hash 205377AE - sample 406: - time = 13500000 - flags = 0 - data = length 68, hash 3811E1 - sample 407: - time = 13666666 - flags = 0 - data = length 77, hash B8F74A5C - sample 408: - time = 13600000 - flags = 0 - data = length 70, hash F383DB6D - sample 409: - time = 13566666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 410: - time = 13633333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 411: - time = 13800000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 412: - time = 13733333 - flags = 0 - data = length 70, hash B92866C9 - sample 413: - time = 13700000 - flags = 0 - data = length 68, hash 66214C05 - sample 414: - time = 13766666 - flags = 0 - data = length 68, hash 4605E638 - sample 415: - time = 13933333 - flags = 0 - data = length 77, hash E5E3D004 - sample 416: - time = 13866666 - flags = 0 - data = length 70, hash E9E98F05 - sample 417: - time = 13833333 - flags = 0 - data = length 68, hash 88475340 - sample 418: - time = 13900000 - flags = 0 - data = length 68, hash 682BED75 - sample 419: - time = 14066666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 420: - time = 14000000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 421: - time = 13966666 - flags = 0 - data = length 68, hash E8DCF97D - sample 422: - time = 14033333 - flags = 0 - data = length 68, hash C8C193B1 - sample 423: - time = 14200000 - flags = 0 - data = length 77, hash 6967C98D - sample 424: - time = 14133333 - flags = 0 - data = length 70, hash 5B33157D - sample 425: - time = 14100000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 426: - time = 14166666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 427: - time = 14333333 - flags = 0 - data = length 77, hash 434E1BAF - sample 428: - time = 14266666 - flags = 0 - data = length 70, hash ECF81CB9 - sample 429: - time = 14233333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 430: - time = 14300000 - flags = 0 - data = length 68, hash 7D7C646A - sample 431: - time = 14466666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 432: - time = 14400000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 433: - time = 14366666 - flags = 0 - data = length 68, hash BFBDD171 - sample 434: - time = 14433333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 435: - time = 14600000 - flags = 0 - data = length 77, hash C6D21537 - sample 436: - time = 14533333 - flags = 0 - data = length 70, hash AF7E4C31 - sample 437: - time = 14500000 - flags = 0 - data = length 68, hash 205377AE - sample 438: - time = 14566666 - flags = 0 - data = length 68, hash 3811E1 - sample 439: - time = 14733333 - flags = 0 - data = length 77, hash B8F74A5C - sample 440: - time = 14666666 - flags = 0 - data = length 70, hash F383DB6D - sample 441: - time = 14633333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 442: - time = 14700000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 443: - time = 14866666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 444: - time = 14800000 - flags = 0 - data = length 70, hash B92866C9 - sample 445: - time = 14766666 - flags = 0 - data = length 68, hash 66214C05 - sample 446: - time = 14833333 - flags = 0 - data = length 68, hash 4605E638 - sample 447: - time = 15000000 - flags = 0 - data = length 77, hash E5E3D004 - sample 448: - time = 14933333 - flags = 0 - data = length 70, hash E9E98F05 - sample 449: - time = 14900000 - flags = 0 - data = length 68, hash 88475340 - sample 450: - time = 14966666 - flags = 0 - data = length 68, hash 682BED75 - sample 451: - time = 15133333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 452: - time = 15066666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 453: - time = 15033333 - flags = 0 - data = length 68, hash E8DCF97D - sample 454: - time = 15100000 - flags = 0 - data = length 68, hash C8C193B1 - sample 455: - time = 15266666 - flags = 0 - data = length 77, hash 6967C98D - sample 456: - time = 15200000 - flags = 0 - data = length 70, hash 5B33157D - sample 457: - time = 15166666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 458: - time = 15233333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 459: - time = 15400000 - flags = 0 - data = length 77, hash 434E1BAF - sample 460: - time = 15333333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 461: - time = 15300000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 462: - time = 15366666 - flags = 0 - data = length 68, hash 7D7C646A - sample 463: - time = 15533333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 464: - time = 15466666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 465: - time = 15433333 - flags = 0 - data = length 68, hash BFBDD171 - sample 466: - time = 15500000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 467: - time = 15666666 - flags = 0 - data = length 77, hash C6D21537 - sample 468: - time = 15600000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 469: - time = 15566666 - flags = 0 - data = length 68, hash 205377AE - sample 470: - time = 15633333 - flags = 0 - data = length 68, hash 3811E1 - sample 471: - time = 15800000 - flags = 0 - data = length 77, hash B8F74A5C - sample 472: - time = 15733333 - flags = 0 - data = length 70, hash F383DB6D - sample 473: - time = 15700000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 474: - time = 15766666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 475: - time = 15933333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 476: - time = 15866666 - flags = 0 - data = length 70, hash B92866C9 - sample 477: - time = 15833333 - flags = 0 - data = length 68, hash 66214C05 - sample 478: - time = 15900000 - flags = 0 - data = length 68, hash 4605E638 - sample 479: - time = 16066666 - flags = 0 - data = length 77, hash E5E3D004 - sample 480: - time = 16000000 - flags = 0 - data = length 70, hash E9E98F05 - sample 481: - time = 15966666 - flags = 0 - data = length 68, hash 88475340 - sample 482: - time = 16033333 - flags = 0 - data = length 68, hash 682BED75 - sample 483: - time = 16200000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 484: - time = 16133333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 485: - time = 16100000 - flags = 0 - data = length 68, hash E8DCF97D - sample 486: - time = 16166666 - flags = 0 - data = length 68, hash C8C193B1 - sample 487: - time = 16333333 - flags = 0 - data = length 77, hash 6967C98D - sample 488: - time = 16266666 - flags = 0 - data = length 70, hash 5B33157D - sample 489: - time = 16233333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 490: - time = 16300000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 491: - time = 16466666 - flags = 0 - data = length 77, hash 434E1BAF - sample 492: - time = 16400000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 493: - time = 16366666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 494: - time = 16433333 - flags = 0 - data = length 68, hash 7D7C646A - sample 495: - time = 16600000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 496: - time = 16533333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 497: - time = 16500000 - flags = 0 - data = length 68, hash BFBDD171 - sample 498: - time = 16566666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 499: - time = 16633333 - flags = 0 - data = length 77, hash AB0BE9E8 - sample 500: - time = 16666666 - flags = 1 - data = length 423, hash DDB543E8 - sample 501: - time = 16800000 - flags = 0 - data = length 71, hash E6801694 - sample 502: - time = 16733333 - flags = 0 - data = length 68, hash 83DC1BAA - sample 503: - time = 16700000 - flags = 0 - data = length 68, hash 66214C05 - sample 504: - time = 16766666 - flags = 0 - data = length 68, hash 4605E639 - sample 505: - time = 16933333 - flags = 0 - data = length 77, hash 76DDE100 - sample 506: - time = 16866666 - flags = 0 - data = length 70, hash 98EFD089 - sample 507: - time = 16833333 - flags = 0 - data = length 68, hash 88475340 - sample 508: - time = 16900000 - flags = 0 - data = length 68, hash 682BED75 - sample 509: - time = 17066666 - flags = 0 - data = length 77, hash 11CC2C49 - sample 510: - time = 17000000 - flags = 0 - data = length 70, hash 7BAE9640 - sample 511: - time = 16966666 - flags = 0 - data = length 68, hash E8DCF97C - sample 512: - time = 17033333 - flags = 0 - data = length 68, hash C8C193B1 - sample 513: - time = 17200000 - flags = 0 - data = length 77, hash 6967C98C - sample 514: - time = 17133333 - flags = 0 - data = length 70, hash 5B33157E - sample 515: - time = 17100000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 516: - time = 17166666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 517: - time = 17333333 - flags = 0 - data = length 77, hash 434E1BB0 - sample 518: - time = 17266666 - flags = 0 - data = length 70, hash ECF81CBA - sample 519: - time = 17233333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 520: - time = 17300000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 521: - time = 17466666 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 522: - time = 17400000 - flags = 0 - data = length 70, hash 1DB944F6 - sample 523: - time = 17366666 - flags = 0 - data = length 68, hash BFBDD171 - sample 524: - time = 17433333 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 525: - time = 17600000 - flags = 0 - data = length 77, hash C6D21538 - sample 526: - time = 17533333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 527: - time = 17500000 - flags = 0 - data = length 68, hash 205377AE - sample 528: - time = 17566666 - flags = 0 - data = length 68, hash 3811E1 - sample 529: - time = 17733333 - flags = 0 - data = length 77, hash B8F74A5B - sample 530: - time = 17666666 - flags = 0 - data = length 70, hash F383DB6D - sample 531: - time = 17633333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 532: - time = 17700000 - flags = 0 - data = length 68, hash 6D3E33DC - sample 533: - time = 17866666 - flags = 0 - data = length 77, hash 8E4832C1 - sample 534: - time = 17800000 - flags = 0 - data = length 70, hash B92866C8 - sample 535: - time = 17766666 - flags = 0 - data = length 68, hash 66214C05 - sample 536: - time = 17833333 - flags = 0 - data = length 68, hash 4605E639 - sample 537: - time = 18000000 - flags = 0 - data = length 77, hash E5E3D004 - sample 538: - time = 17933333 - flags = 0 - data = length 70, hash E9E98F05 - sample 539: - time = 17900000 - flags = 0 - data = length 68, hash 88475340 - sample 540: - time = 17966666 - flags = 0 - data = length 68, hash 682BED75 - sample 541: - time = 18133333 - flags = 0 - data = length 77, hash 11CC2C49 - sample 542: - time = 18066666 - flags = 0 - data = length 70, hash 7BAE9640 - sample 543: - time = 18033333 - flags = 0 - data = length 68, hash E8DCF97C - sample 544: - time = 18100000 - flags = 0 - data = length 68, hash C8C193B1 - sample 545: - time = 18266666 - flags = 0 - data = length 77, hash 6967C98C - sample 546: - time = 18200000 - flags = 0 - data = length 70, hash 5B33157E - sample 547: - time = 18166666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 548: - time = 18233333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 549: - time = 18400000 - flags = 0 - data = length 77, hash 434E1BB0 - sample 550: - time = 18333333 - flags = 0 - data = length 70, hash ECF81CBA - sample 551: - time = 18300000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 552: - time = 18366666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 553: - time = 18533333 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 554: - time = 18466666 - flags = 0 - data = length 70, hash 1DB944F6 - sample 555: - time = 18433333 - flags = 0 - data = length 68, hash BFBDD171 - sample 556: - time = 18500000 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 557: - time = 18666666 - flags = 0 - data = length 77, hash C6D21538 - sample 558: - time = 18600000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 559: - time = 18566666 - flags = 0 - data = length 68, hash 205377AE - sample 560: - time = 18633333 - flags = 0 - data = length 68, hash 3811E1 - sample 561: - time = 18800000 - flags = 0 - data = length 77, hash B8F74A5B - sample 562: - time = 18733333 - flags = 0 - data = length 70, hash F383DB6D - sample 563: - time = 18700000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 564: - time = 18766666 - flags = 0 - data = length 68, hash 6D3E33DC - sample 565: - time = 18933333 - flags = 0 - data = length 77, hash 8E4832C1 - sample 566: - time = 18866666 - flags = 0 - data = length 70, hash B92866C8 - sample 567: - time = 18833333 - flags = 0 - data = length 68, hash 66214C05 - sample 568: - time = 18900000 - flags = 0 - data = length 68, hash 4605E639 - sample 569: - time = 19066666 - flags = 0 - data = length 77, hash E5E3D004 - sample 570: - time = 19000000 - flags = 0 - data = length 70, hash E9E98F05 - sample 571: - time = 18966666 - flags = 0 - data = length 68, hash 88475340 - sample 572: - time = 19033333 - flags = 0 - data = length 68, hash 682BED75 - sample 573: - time = 19200000 - flags = 0 - data = length 77, hash 11CC2C49 - sample 574: - time = 19133333 - flags = 0 - data = length 70, hash 7BAE9640 - sample 575: - time = 19100000 - flags = 0 - data = length 68, hash E8DCF97C - sample 576: - time = 19166666 - flags = 0 - data = length 68, hash C8C193B1 - sample 577: - time = 19333333 - flags = 0 - data = length 77, hash 6967C98C - sample 578: - time = 19266666 - flags = 0 - data = length 70, hash 5B33157E - sample 579: - time = 19233333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 580: - time = 19300000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 581: - time = 19466666 - flags = 0 - data = length 77, hash 434E1BB0 - sample 582: - time = 19400000 - flags = 0 - data = length 70, hash ECF81CBA - sample 583: - time = 19366666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 584: - time = 19433333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 585: - time = 19600000 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 586: - time = 19533333 - flags = 0 - data = length 70, hash 1DB944F6 - sample 587: - time = 19500000 - flags = 0 - data = length 68, hash BFBDD171 - sample 588: - time = 19566666 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 589: - time = 19733333 - flags = 0 - data = length 77, hash 179FCFE9 - sample 590: - time = 19666666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 591: - time = 19633333 - flags = 0 - data = length 68, hash 205377AE - sample 592: - time = 19700000 - flags = 0 - data = length 68, hash 3811E1 - sample 593: - time = 19866666 - flags = 0 - data = length 77, hash BCD7CD76 - sample 594: - time = 19800000 - flags = 0 - data = length 70, hash F383DB6D - sample 595: - time = 19766666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 596: - time = 19833333 - flags = 0 - data = length 68, hash 6D3E33DC - sample 597: - time = 19966666 - flags = 0 - data = length 77, hash 38D96DF4 - sample 598: - time = 19900000 - flags = 0 - data = length 70, hash 3AC762E9 - sample 599: - time = 19933333 - flags = 0 - data = length 68, hash B1C7769A -tracksEnded = true 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 deleted file mode 100644 index cf0b315ef69..00000000000 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.1.dump +++ /dev/null @@ -1,2430 +0,0 @@ -seekMap: - isSeekable = true - duration = 8333333 - getPosition(0) = [[timeUs=0, position=871]] - getPosition(1) = [[timeUs=0, position=871]] - getPosition(4166666) = [[timeUs=0, position=871]] - getPosition(8333333) = [[timeUs=0, position=871]] -numberOfTracks = 1 -track 0: - total output bytes = 44195 - sample count = 600 - format 0: - averageBitrate = 17678 - peakBitrate = 17678 - id = 1 - containerMimeType = video/mp4 - sampleMimeType = video/avc - codecs = avc1.640028 - maxNumReorderSamples = 2 - width = 1920 - height = 1080 - colorInfo: - 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 - sample 0: - time = 0 - flags = 1 - data = length 1120, hash 352E33B0 - sample 1: - time = 133333 - flags = 0 - data = length 71, hash E6801693 - sample 2: - time = 66666 - flags = 0 - data = length 68, hash 83DC1BAB - sample 3: - time = 33333 - flags = 0 - data = length 68, hash 66214C04 - sample 4: - time = 100000 - flags = 0 - data = length 68, hash 4605E639 - sample 5: - time = 266666 - flags = 0 - data = length 77, hash 76DDE101 - sample 6: - time = 200000 - flags = 0 - data = length 70, hash 98EFD089 - sample 7: - time = 166666 - flags = 0 - data = length 68, hash 88475341 - sample 8: - time = 233333 - flags = 0 - data = length 68, hash 682BED74 - sample 9: - time = 400000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 10: - time = 333333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 11: - time = 300000 - flags = 0 - data = length 68, hash E8DCF97C - sample 12: - time = 366666 - flags = 0 - data = length 68, hash C8C193B0 - sample 13: - time = 533333 - flags = 0 - data = length 77, hash 6967C98D - sample 14: - time = 466666 - flags = 0 - data = length 70, hash 5B33157E - sample 15: - time = 433333 - flags = 0 - data = length 68, hash 3D0223FA - sample 16: - time = 500000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 17: - time = 666666 - flags = 0 - data = length 77, hash 434E1BAF - sample 18: - time = 600000 - flags = 0 - data = length 70, hash ECF81CBA - sample 19: - time = 566666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 20: - time = 633333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 21: - time = 800000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 22: - time = 733333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 23: - time = 700000 - flags = 0 - data = length 68, hash BFBDD172 - sample 24: - time = 766666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 25: - time = 933333 - flags = 0 - data = length 77, hash C6D21537 - sample 26: - time = 866666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 27: - time = 833333 - flags = 0 - data = length 68, hash 205377AD - sample 28: - time = 900000 - flags = 0 - data = length 68, hash 3811E2 - sample 29: - time = 1066666 - flags = 0 - data = length 77, hash B8F74A5C - sample 30: - time = 1000000 - flags = 0 - data = length 70, hash F383DB6C - sample 31: - time = 966666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 32: - time = 1033333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 33: - time = 1200000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 34: - time = 1133333 - flags = 0 - data = length 70, hash B92866C9 - sample 35: - time = 1100000 - flags = 0 - data = length 68, hash 66214C04 - sample 36: - time = 1166666 - flags = 0 - data = length 68, hash 4605E639 - sample 37: - time = 1333333 - flags = 0 - data = length 77, hash E5E3D005 - sample 38: - time = 1266666 - flags = 0 - data = length 70, hash E9E98F05 - sample 39: - time = 1233333 - flags = 0 - data = length 68, hash 88475341 - sample 40: - time = 1300000 - flags = 0 - data = length 68, hash 682BED74 - sample 41: - time = 1466666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 42: - time = 1400000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 43: - time = 1366666 - flags = 0 - data = length 68, hash E8DCF97C - sample 44: - time = 1433333 - flags = 0 - data = length 68, hash C8C193B0 - sample 45: - time = 1600000 - flags = 0 - data = length 77, hash 6967C98D - sample 46: - time = 1533333 - flags = 0 - data = length 70, hash 5B33157E - sample 47: - time = 1500000 - flags = 0 - data = length 68, hash 3D0223FA - sample 48: - time = 1566666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 49: - time = 1733333 - flags = 0 - data = length 77, hash 434E1BAF - sample 50: - time = 1666666 - flags = 0 - data = length 70, hash ECF81CBA - sample 51: - time = 1633333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 52: - time = 1700000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 53: - time = 1866666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 54: - time = 1800000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 55: - time = 1766666 - flags = 0 - data = length 68, hash BFBDD172 - sample 56: - time = 1833333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 57: - time = 2000000 - flags = 0 - data = length 77, hash C6D21537 - sample 58: - time = 1933333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 59: - time = 1900000 - flags = 0 - data = length 68, hash 205377AD - sample 60: - time = 1966666 - flags = 0 - data = length 68, hash 3811E2 - sample 61: - time = 2133333 - flags = 0 - data = length 77, hash B8F74A5C - sample 62: - time = 2066666 - flags = 0 - data = length 70, hash F383DB6C - sample 63: - time = 2033333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 64: - time = 2100000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 65: - time = 2266666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 66: - time = 2200000 - flags = 0 - data = length 70, hash B92866C9 - sample 67: - time = 2166666 - flags = 0 - data = length 68, hash 66214C04 - sample 68: - time = 2233333 - flags = 0 - data = length 68, hash 4605E639 - sample 69: - time = 2400000 - flags = 0 - data = length 77, hash E5E3D005 - sample 70: - time = 2333333 - flags = 0 - data = length 70, hash E9E98F05 - sample 71: - time = 2300000 - flags = 0 - data = length 68, hash 88475341 - sample 72: - time = 2366666 - flags = 0 - data = length 68, hash 682BED74 - sample 73: - time = 2533333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 74: - time = 2466666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 75: - time = 2433333 - flags = 0 - data = length 68, hash E8DCF97C - sample 76: - time = 2500000 - flags = 0 - data = length 68, hash C8C193B0 - sample 77: - time = 2666666 - flags = 0 - data = length 77, hash 6967C98D - sample 78: - time = 2600000 - flags = 0 - data = length 70, hash 5B33157E - sample 79: - time = 2566666 - flags = 0 - data = length 68, hash 3D0223FA - sample 80: - time = 2633333 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 81: - time = 2800000 - flags = 0 - data = length 77, hash 434E1BAF - sample 82: - time = 2733333 - flags = 0 - data = length 70, hash ECF81CBA - sample 83: - time = 2700000 - flags = 0 - data = length 68, hash 9D97CA35 - sample 84: - time = 2766666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 85: - time = 2933333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 86: - time = 2866666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 87: - time = 2833333 - flags = 0 - data = length 68, hash BFBDD172 - sample 88: - time = 2900000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 89: - time = 3066666 - flags = 0 - data = length 77, hash C6D21537 - sample 90: - time = 3000000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 91: - time = 2966666 - flags = 0 - data = length 68, hash 205377AD - sample 92: - time = 3033333 - flags = 0 - data = length 68, hash 3811E2 - sample 93: - time = 3200000 - flags = 0 - data = length 77, hash B8F74A5C - sample 94: - time = 3133333 - flags = 0 - data = length 70, hash F383DB6C - sample 95: - time = 3100000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 96: - time = 3166666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 97: - time = 3333333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 98: - time = 3266666 - flags = 0 - data = length 70, hash B92866C9 - sample 99: - time = 3233333 - flags = 0 - data = length 68, hash 66214C04 - sample 100: - time = 3300000 - flags = 0 - data = length 68, hash 4605E639 - sample 101: - time = 3466666 - flags = 0 - data = length 77, hash E5E3D005 - sample 102: - time = 3400000 - flags = 0 - data = length 70, hash E9E98F05 - sample 103: - time = 3366666 - flags = 0 - data = length 68, hash 88475341 - sample 104: - time = 3433333 - flags = 0 - data = length 68, hash 682BED74 - sample 105: - time = 3600000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 106: - time = 3533333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 107: - time = 3500000 - flags = 0 - data = length 68, hash E8DCF97C - sample 108: - time = 3566666 - flags = 0 - data = length 68, hash C8C193B0 - sample 109: - time = 3733333 - flags = 0 - data = length 77, hash 6967C98D - sample 110: - time = 3666666 - flags = 0 - data = length 70, hash 5B33157E - sample 111: - time = 3633333 - flags = 0 - data = length 68, hash 3D0223FA - sample 112: - time = 3700000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 113: - time = 3866666 - flags = 0 - data = length 77, hash 434E1BAF - sample 114: - time = 3800000 - flags = 0 - data = length 70, hash ECF81CBA - sample 115: - time = 3766666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 116: - time = 3833333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 117: - time = 4000000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 118: - time = 3933333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 119: - time = 3900000 - flags = 0 - data = length 68, hash BFBDD172 - sample 120: - time = 3966666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 121: - time = 4133333 - flags = 0 - data = length 77, hash C6D21537 - sample 122: - time = 4066666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 123: - time = 4033333 - flags = 0 - data = length 68, hash 205377AD - sample 124: - time = 4100000 - flags = 0 - data = length 68, hash 3811E2 - sample 125: - time = 4266666 - flags = 0 - data = length 77, hash B8F74A5C - sample 126: - time = 4200000 - flags = 0 - data = length 70, hash F383DB6C - sample 127: - time = 4166666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 128: - time = 4233333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 129: - time = 4400000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 130: - time = 4333333 - flags = 0 - data = length 70, hash B92866C9 - sample 131: - time = 4300000 - flags = 0 - data = length 68, hash 66214C04 - sample 132: - time = 4366666 - flags = 0 - data = length 68, hash 4605E639 - sample 133: - time = 4533333 - flags = 0 - data = length 77, hash E5E3D005 - sample 134: - time = 4466666 - flags = 0 - data = length 70, hash E9E98F05 - sample 135: - time = 4433333 - flags = 0 - data = length 68, hash 88475341 - sample 136: - time = 4500000 - flags = 0 - data = length 68, hash 682BED74 - sample 137: - time = 4666666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 138: - time = 4600000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 139: - time = 4566666 - flags = 0 - data = length 68, hash E8DCF97C - sample 140: - time = 4633333 - flags = 0 - data = length 68, hash C8C193B0 - sample 141: - time = 4800000 - flags = 0 - data = length 77, hash 6967C98D - sample 142: - time = 4733333 - flags = 0 - data = length 70, hash 5B33157E - sample 143: - time = 4700000 - flags = 0 - data = length 68, hash 3D0223FA - sample 144: - time = 4766666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 145: - time = 4933333 - flags = 0 - data = length 77, hash 434E1BAF - sample 146: - time = 4866666 - flags = 0 - data = length 70, hash ECF81CBA - sample 147: - time = 4833333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 148: - time = 4900000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 149: - time = 5066666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 150: - time = 5000000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 151: - time = 4966666 - flags = 0 - data = length 68, hash BFBDD172 - sample 152: - time = 5033333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 153: - time = 5200000 - flags = 0 - data = length 77, hash C6D21537 - sample 154: - time = 5133333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 155: - time = 5100000 - flags = 0 - data = length 68, hash 205377AD - sample 156: - time = 5166666 - flags = 0 - data = length 68, hash 3811E2 - sample 157: - time = 5333333 - flags = 0 - data = length 77, hash B8F74A5C - sample 158: - time = 5266666 - flags = 0 - data = length 70, hash F383DB6C - sample 159: - time = 5233333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 160: - time = 5300000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 161: - time = 5466666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 162: - time = 5400000 - flags = 0 - data = length 70, hash B92866C9 - sample 163: - time = 5366666 - flags = 0 - data = length 68, hash 66214C04 - sample 164: - time = 5433333 - flags = 0 - data = length 68, hash 4605E639 - sample 165: - time = 5600000 - flags = 0 - data = length 77, hash E5E3D005 - sample 166: - time = 5533333 - flags = 0 - data = length 70, hash E9E98F05 - sample 167: - time = 5500000 - flags = 0 - data = length 68, hash 88475341 - sample 168: - time = 5566666 - flags = 0 - data = length 68, hash 682BED74 - sample 169: - time = 5733333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 170: - time = 5666666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 171: - time = 5633333 - flags = 0 - data = length 68, hash E8DCF97C - sample 172: - time = 5700000 - flags = 0 - data = length 68, hash C8C193B0 - sample 173: - time = 5866666 - flags = 0 - data = length 77, hash 6967C98D - sample 174: - time = 5800000 - flags = 0 - data = length 70, hash 5B33157E - sample 175: - time = 5766666 - flags = 0 - data = length 68, hash 3D0223FA - sample 176: - time = 5833333 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 177: - time = 6000000 - flags = 0 - data = length 77, hash 434E1BAF - sample 178: - time = 5933333 - flags = 0 - data = length 70, hash ECF81CBA - sample 179: - time = 5900000 - flags = 0 - data = length 68, hash 9D97CA35 - sample 180: - time = 5966666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 181: - time = 6133333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 182: - time = 6066666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 183: - time = 6033333 - flags = 0 - data = length 68, hash BFBDD172 - sample 184: - time = 6100000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 185: - time = 6266666 - flags = 0 - data = length 77, hash C6D21537 - sample 186: - time = 6200000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 187: - time = 6166666 - flags = 0 - data = length 68, hash 205377AD - sample 188: - time = 6233333 - flags = 0 - data = length 68, hash 3811E2 - sample 189: - time = 6400000 - flags = 0 - data = length 77, hash B8F74A5C - sample 190: - time = 6333333 - flags = 0 - data = length 70, hash F383DB6C - sample 191: - time = 6300000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 192: - time = 6366666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 193: - time = 6533333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 194: - time = 6466666 - flags = 0 - data = length 70, hash B92866C9 - sample 195: - time = 6433333 - flags = 0 - data = length 68, hash 66214C04 - sample 196: - time = 6500000 - flags = 0 - data = length 68, hash 4605E639 - sample 197: - time = 6666666 - flags = 0 - data = length 77, hash E5E3D005 - sample 198: - time = 6600000 - flags = 0 - data = length 70, hash E9E98F05 - sample 199: - time = 6566666 - flags = 0 - data = length 68, hash 88475341 - sample 200: - time = 6633333 - flags = 0 - data = length 68, hash 682BED74 - sample 201: - time = 6800000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 202: - time = 6733333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 203: - time = 6700000 - flags = 0 - data = length 68, hash E8DCF97C - sample 204: - time = 6766666 - flags = 0 - data = length 68, hash C8C193B0 - sample 205: - time = 6933333 - flags = 0 - data = length 77, hash 6967C98D - sample 206: - time = 6866666 - flags = 0 - data = length 70, hash 5B33157E - sample 207: - time = 6833333 - flags = 0 - data = length 68, hash 3D0223FA - sample 208: - time = 6900000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 209: - time = 7066666 - flags = 0 - data = length 77, hash 434E1BAF - sample 210: - time = 7000000 - flags = 0 - data = length 70, hash ECF81CBA - sample 211: - time = 6966666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 212: - time = 7033333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 213: - time = 7200000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 214: - time = 7133333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 215: - time = 7100000 - flags = 0 - data = length 68, hash BFBDD172 - sample 216: - time = 7166666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 217: - time = 7333333 - flags = 0 - data = length 77, hash C6D21537 - sample 218: - time = 7266666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 219: - time = 7233333 - flags = 0 - data = length 68, hash 205377AD - sample 220: - time = 7300000 - flags = 0 - data = length 68, hash 3811E2 - sample 221: - time = 7466666 - flags = 0 - data = length 77, hash B8F74A5C - sample 222: - time = 7400000 - flags = 0 - data = length 70, hash F383DB6C - sample 223: - time = 7366666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 224: - time = 7433333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 225: - time = 7600000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 226: - time = 7533333 - flags = 0 - data = length 70, hash B92866C9 - sample 227: - time = 7500000 - flags = 0 - data = length 68, hash 66214C04 - sample 228: - time = 7566666 - flags = 0 - data = length 68, hash 4605E639 - sample 229: - time = 7733333 - flags = 0 - data = length 77, hash E5E3D005 - sample 230: - time = 7666666 - flags = 0 - data = length 70, hash E9E98F05 - sample 231: - time = 7633333 - flags = 0 - data = length 68, hash 88475341 - sample 232: - time = 7700000 - flags = 0 - data = length 68, hash 682BED74 - sample 233: - time = 7866666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 234: - time = 7800000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 235: - time = 7766666 - flags = 0 - data = length 68, hash E8DCF97C - sample 236: - time = 7833333 - flags = 0 - data = length 68, hash C8C193B0 - sample 237: - time = 8000000 - flags = 0 - data = length 77, hash 6967C98D - sample 238: - time = 7933333 - flags = 0 - data = length 70, hash 5B33157E - sample 239: - time = 7900000 - flags = 0 - data = length 68, hash 3D0223FA - sample 240: - time = 7966666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 241: - time = 8133333 - flags = 0 - data = length 77, hash 434E1BAF - sample 242: - time = 8066666 - flags = 0 - data = length 70, hash ECF81CBA - sample 243: - time = 8033333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 244: - time = 8100000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 245: - time = 8266666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 246: - time = 8200000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 247: - time = 8166666 - flags = 0 - data = length 68, hash BFBDD172 - sample 248: - time = 8233333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 249: - time = 8300000 - flags = 0 - data = length 77, hash AB0BE9E8 - sample 250: - time = 8333333 - flags = 1 - data = length 423, hash 724FF09B - sample 251: - time = 8466666 - flags = 0 - data = length 71, hash E6801693 - sample 252: - time = 8400000 - flags = 0 - data = length 68, hash 83DC1BAB - sample 253: - time = 8366666 - flags = 0 - data = length 68, hash 66214C05 - sample 254: - time = 8433333 - flags = 0 - data = length 68, hash 4605E638 - sample 255: - time = 8600000 - flags = 0 - data = length 77, hash 76DDE100 - sample 256: - time = 8533333 - flags = 0 - data = length 70, hash 98EFD089 - sample 257: - time = 8500000 - flags = 0 - data = length 68, hash 88475340 - sample 258: - time = 8566666 - flags = 0 - data = length 68, hash 682BED75 - sample 259: - time = 8733333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 260: - time = 8666666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 261: - time = 8633333 - flags = 0 - data = length 68, hash E8DCF97D - sample 262: - time = 8700000 - flags = 0 - data = length 68, hash C8C193B1 - sample 263: - time = 8866666 - flags = 0 - data = length 77, hash 6967C98D - sample 264: - time = 8800000 - flags = 0 - data = length 70, hash 5B33157D - sample 265: - time = 8766666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 266: - time = 8833333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 267: - time = 9000000 - flags = 0 - data = length 77, hash 434E1BAF - sample 268: - time = 8933333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 269: - time = 8900000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 270: - time = 8966666 - flags = 0 - data = length 68, hash 7D7C646A - sample 271: - time = 9133333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 272: - time = 9066666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 273: - time = 9033333 - flags = 0 - data = length 68, hash BFBDD171 - sample 274: - time = 9100000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 275: - time = 9266666 - flags = 0 - data = length 77, hash C6D21537 - sample 276: - time = 9200000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 277: - time = 9166666 - flags = 0 - data = length 68, hash 205377AE - sample 278: - time = 9233333 - flags = 0 - data = length 68, hash 3811E1 - sample 279: - time = 9400000 - flags = 0 - data = length 77, hash B8F74A5C - sample 280: - time = 9333333 - flags = 0 - data = length 70, hash F383DB6D - sample 281: - time = 9300000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 282: - time = 9366666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 283: - time = 9533333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 284: - time = 9466666 - flags = 0 - data = length 70, hash B92866C9 - sample 285: - time = 9433333 - flags = 0 - data = length 68, hash 66214C05 - sample 286: - time = 9500000 - flags = 0 - data = length 68, hash 4605E638 - sample 287: - time = 9666666 - flags = 0 - data = length 77, hash E5E3D004 - sample 288: - time = 9600000 - flags = 0 - data = length 70, hash E9E98F05 - sample 289: - time = 9566666 - flags = 0 - data = length 68, hash 88475340 - sample 290: - time = 9633333 - flags = 0 - data = length 68, hash 682BED75 - sample 291: - time = 9800000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 292: - time = 9733333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 293: - time = 9700000 - flags = 0 - data = length 68, hash E8DCF97D - sample 294: - time = 9766666 - flags = 0 - data = length 68, hash C8C193B1 - sample 295: - time = 9933333 - flags = 0 - data = length 77, hash 6967C98D - sample 296: - time = 9866666 - flags = 0 - data = length 70, hash 5B33157D - sample 297: - time = 9833333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 298: - time = 9900000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 299: - time = 10066666 - flags = 0 - data = length 77, hash 434E1BAF - sample 300: - time = 10000000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 301: - time = 9966666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 302: - time = 10033333 - flags = 0 - data = length 68, hash 7D7C646A - sample 303: - time = 10200000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 304: - time = 10133333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 305: - time = 10100000 - flags = 0 - data = length 68, hash BFBDD171 - sample 306: - time = 10166666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 307: - time = 10333333 - flags = 0 - data = length 77, hash C6D21537 - sample 308: - time = 10266666 - flags = 0 - data = length 70, hash AF7E4C31 - sample 309: - time = 10233333 - flags = 0 - data = length 68, hash 205377AE - sample 310: - time = 10300000 - flags = 0 - data = length 68, hash 3811E1 - sample 311: - time = 10466666 - flags = 0 - data = length 77, hash B8F74A5C - sample 312: - time = 10400000 - flags = 0 - data = length 70, hash F383DB6D - sample 313: - time = 10366666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 314: - time = 10433333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 315: - time = 10600000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 316: - time = 10533333 - flags = 0 - data = length 70, hash B92866C9 - sample 317: - time = 10500000 - flags = 0 - data = length 68, hash 66214C05 - sample 318: - time = 10566666 - flags = 0 - data = length 68, hash 4605E638 - sample 319: - time = 10733333 - flags = 0 - data = length 77, hash E5E3D004 - sample 320: - time = 10666666 - flags = 0 - data = length 70, hash E9E98F05 - sample 321: - time = 10633333 - flags = 0 - data = length 68, hash 88475340 - sample 322: - time = 10700000 - flags = 0 - data = length 68, hash 682BED75 - sample 323: - time = 10866666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 324: - time = 10800000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 325: - time = 10766666 - flags = 0 - data = length 68, hash E8DCF97D - sample 326: - time = 10833333 - flags = 0 - data = length 68, hash C8C193B1 - sample 327: - time = 11000000 - flags = 0 - data = length 77, hash 6967C98D - sample 328: - time = 10933333 - flags = 0 - data = length 70, hash 5B33157D - sample 329: - time = 10900000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 330: - time = 10966666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 331: - time = 11133333 - flags = 0 - data = length 77, hash 434E1BAF - sample 332: - time = 11066666 - flags = 0 - data = length 70, hash ECF81CB9 - sample 333: - time = 11033333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 334: - time = 11100000 - flags = 0 - data = length 68, hash 7D7C646A - sample 335: - time = 11266666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 336: - time = 11200000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 337: - time = 11166666 - flags = 0 - data = length 68, hash BFBDD171 - sample 338: - time = 11233333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 339: - time = 11400000 - flags = 0 - data = length 77, hash C6D21537 - sample 340: - time = 11333333 - flags = 0 - data = length 70, hash AF7E4C31 - sample 341: - time = 11300000 - flags = 0 - data = length 68, hash 205377AE - sample 342: - time = 11366666 - flags = 0 - data = length 68, hash 3811E1 - sample 343: - time = 11533333 - flags = 0 - data = length 77, hash B8F74A5C - sample 344: - time = 11466666 - flags = 0 - data = length 70, hash F383DB6D - sample 345: - time = 11433333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 346: - time = 11500000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 347: - time = 11666666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 348: - time = 11600000 - flags = 0 - data = length 70, hash B92866C9 - sample 349: - time = 11566666 - flags = 0 - data = length 68, hash 66214C05 - sample 350: - time = 11633333 - flags = 0 - data = length 68, hash 4605E638 - sample 351: - time = 11800000 - flags = 0 - data = length 77, hash E5E3D004 - sample 352: - time = 11733333 - flags = 0 - data = length 70, hash E9E98F05 - sample 353: - time = 11700000 - flags = 0 - data = length 68, hash 88475340 - sample 354: - time = 11766666 - flags = 0 - data = length 68, hash 682BED75 - sample 355: - time = 11933333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 356: - time = 11866666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 357: - time = 11833333 - flags = 0 - data = length 68, hash E8DCF97D - sample 358: - time = 11900000 - flags = 0 - data = length 68, hash C8C193B1 - sample 359: - time = 12066666 - flags = 0 - data = length 77, hash 6967C98D - sample 360: - time = 12000000 - flags = 0 - data = length 70, hash 5B33157D - sample 361: - time = 11966666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 362: - time = 12033333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 363: - time = 12200000 - flags = 0 - data = length 77, hash 434E1BAF - sample 364: - time = 12133333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 365: - time = 12100000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 366: - time = 12166666 - flags = 0 - data = length 68, hash 7D7C646A - sample 367: - time = 12333333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 368: - time = 12266666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 369: - time = 12233333 - flags = 0 - data = length 68, hash BFBDD171 - sample 370: - time = 12300000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 371: - time = 12466666 - flags = 0 - data = length 77, hash C6D21537 - sample 372: - time = 12400000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 373: - time = 12366666 - flags = 0 - data = length 68, hash 205377AE - sample 374: - time = 12433333 - flags = 0 - data = length 68, hash 3811E1 - sample 375: - time = 12600000 - flags = 0 - data = length 77, hash B8F74A5C - sample 376: - time = 12533333 - flags = 0 - data = length 70, hash F383DB6D - sample 377: - time = 12500000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 378: - time = 12566666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 379: - time = 12733333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 380: - time = 12666666 - flags = 0 - data = length 70, hash B92866C9 - sample 381: - time = 12633333 - flags = 0 - data = length 68, hash 66214C05 - sample 382: - time = 12700000 - flags = 0 - data = length 68, hash 4605E638 - sample 383: - time = 12866666 - flags = 0 - data = length 77, hash E5E3D004 - sample 384: - time = 12800000 - flags = 0 - data = length 70, hash E9E98F05 - sample 385: - time = 12766666 - flags = 0 - data = length 68, hash 88475340 - sample 386: - time = 12833333 - flags = 0 - data = length 68, hash 682BED75 - sample 387: - time = 13000000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 388: - time = 12933333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 389: - time = 12900000 - flags = 0 - data = length 68, hash E8DCF97D - sample 390: - time = 12966666 - flags = 0 - data = length 68, hash C8C193B1 - sample 391: - time = 13133333 - flags = 0 - data = length 77, hash 6967C98D - sample 392: - time = 13066666 - flags = 0 - data = length 70, hash 5B33157D - sample 393: - time = 13033333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 394: - time = 13100000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 395: - time = 13266666 - flags = 0 - data = length 77, hash 434E1BAF - sample 396: - time = 13200000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 397: - time = 13166666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 398: - time = 13233333 - flags = 0 - data = length 68, hash 7D7C646A - sample 399: - time = 13400000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 400: - time = 13333333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 401: - time = 13300000 - flags = 0 - data = length 68, hash BFBDD171 - sample 402: - time = 13366666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 403: - time = 13533333 - flags = 0 - data = length 77, hash C6D21537 - sample 404: - time = 13466666 - flags = 0 - data = length 70, hash AF7E4C31 - sample 405: - time = 13433333 - flags = 0 - data = length 68, hash 205377AE - sample 406: - time = 13500000 - flags = 0 - data = length 68, hash 3811E1 - sample 407: - time = 13666666 - flags = 0 - data = length 77, hash B8F74A5C - sample 408: - time = 13600000 - flags = 0 - data = length 70, hash F383DB6D - sample 409: - time = 13566666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 410: - time = 13633333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 411: - time = 13800000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 412: - time = 13733333 - flags = 0 - data = length 70, hash B92866C9 - sample 413: - time = 13700000 - flags = 0 - data = length 68, hash 66214C05 - sample 414: - time = 13766666 - flags = 0 - data = length 68, hash 4605E638 - sample 415: - time = 13933333 - flags = 0 - data = length 77, hash E5E3D004 - sample 416: - time = 13866666 - flags = 0 - data = length 70, hash E9E98F05 - sample 417: - time = 13833333 - flags = 0 - data = length 68, hash 88475340 - sample 418: - time = 13900000 - flags = 0 - data = length 68, hash 682BED75 - sample 419: - time = 14066666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 420: - time = 14000000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 421: - time = 13966666 - flags = 0 - data = length 68, hash E8DCF97D - sample 422: - time = 14033333 - flags = 0 - data = length 68, hash C8C193B1 - sample 423: - time = 14200000 - flags = 0 - data = length 77, hash 6967C98D - sample 424: - time = 14133333 - flags = 0 - data = length 70, hash 5B33157D - sample 425: - time = 14100000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 426: - time = 14166666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 427: - time = 14333333 - flags = 0 - data = length 77, hash 434E1BAF - sample 428: - time = 14266666 - flags = 0 - data = length 70, hash ECF81CB9 - sample 429: - time = 14233333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 430: - time = 14300000 - flags = 0 - data = length 68, hash 7D7C646A - sample 431: - time = 14466666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 432: - time = 14400000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 433: - time = 14366666 - flags = 0 - data = length 68, hash BFBDD171 - sample 434: - time = 14433333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 435: - time = 14600000 - flags = 0 - data = length 77, hash C6D21537 - sample 436: - time = 14533333 - flags = 0 - data = length 70, hash AF7E4C31 - sample 437: - time = 14500000 - flags = 0 - data = length 68, hash 205377AE - sample 438: - time = 14566666 - flags = 0 - data = length 68, hash 3811E1 - sample 439: - time = 14733333 - flags = 0 - data = length 77, hash B8F74A5C - sample 440: - time = 14666666 - flags = 0 - data = length 70, hash F383DB6D - sample 441: - time = 14633333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 442: - time = 14700000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 443: - time = 14866666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 444: - time = 14800000 - flags = 0 - data = length 70, hash B92866C9 - sample 445: - time = 14766666 - flags = 0 - data = length 68, hash 66214C05 - sample 446: - time = 14833333 - flags = 0 - data = length 68, hash 4605E638 - sample 447: - time = 15000000 - flags = 0 - data = length 77, hash E5E3D004 - sample 448: - time = 14933333 - flags = 0 - data = length 70, hash E9E98F05 - sample 449: - time = 14900000 - flags = 0 - data = length 68, hash 88475340 - sample 450: - time = 14966666 - flags = 0 - data = length 68, hash 682BED75 - sample 451: - time = 15133333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 452: - time = 15066666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 453: - time = 15033333 - flags = 0 - data = length 68, hash E8DCF97D - sample 454: - time = 15100000 - flags = 0 - data = length 68, hash C8C193B1 - sample 455: - time = 15266666 - flags = 0 - data = length 77, hash 6967C98D - sample 456: - time = 15200000 - flags = 0 - data = length 70, hash 5B33157D - sample 457: - time = 15166666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 458: - time = 15233333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 459: - time = 15400000 - flags = 0 - data = length 77, hash 434E1BAF - sample 460: - time = 15333333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 461: - time = 15300000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 462: - time = 15366666 - flags = 0 - data = length 68, hash 7D7C646A - sample 463: - time = 15533333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 464: - time = 15466666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 465: - time = 15433333 - flags = 0 - data = length 68, hash BFBDD171 - sample 466: - time = 15500000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 467: - time = 15666666 - flags = 0 - data = length 77, hash C6D21537 - sample 468: - time = 15600000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 469: - time = 15566666 - flags = 0 - data = length 68, hash 205377AE - sample 470: - time = 15633333 - flags = 0 - data = length 68, hash 3811E1 - sample 471: - time = 15800000 - flags = 0 - data = length 77, hash B8F74A5C - sample 472: - time = 15733333 - flags = 0 - data = length 70, hash F383DB6D - sample 473: - time = 15700000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 474: - time = 15766666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 475: - time = 15933333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 476: - time = 15866666 - flags = 0 - data = length 70, hash B92866C9 - sample 477: - time = 15833333 - flags = 0 - data = length 68, hash 66214C05 - sample 478: - time = 15900000 - flags = 0 - data = length 68, hash 4605E638 - sample 479: - time = 16066666 - flags = 0 - data = length 77, hash E5E3D004 - sample 480: - time = 16000000 - flags = 0 - data = length 70, hash E9E98F05 - sample 481: - time = 15966666 - flags = 0 - data = length 68, hash 88475340 - sample 482: - time = 16033333 - flags = 0 - data = length 68, hash 682BED75 - sample 483: - time = 16200000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 484: - time = 16133333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 485: - time = 16100000 - flags = 0 - data = length 68, hash E8DCF97D - sample 486: - time = 16166666 - flags = 0 - data = length 68, hash C8C193B1 - sample 487: - time = 16333333 - flags = 0 - data = length 77, hash 6967C98D - sample 488: - time = 16266666 - flags = 0 - data = length 70, hash 5B33157D - sample 489: - time = 16233333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 490: - time = 16300000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 491: - time = 16466666 - flags = 0 - data = length 77, hash 434E1BAF - sample 492: - time = 16400000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 493: - time = 16366666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 494: - time = 16433333 - flags = 0 - data = length 68, hash 7D7C646A - sample 495: - time = 16600000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 496: - time = 16533333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 497: - time = 16500000 - flags = 0 - data = length 68, hash BFBDD171 - sample 498: - time = 16566666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 499: - time = 16633333 - flags = 0 - data = length 77, hash AB0BE9E8 - sample 500: - time = 16666666 - flags = 1 - data = length 423, hash DDB543E8 - sample 501: - time = 16800000 - flags = 0 - data = length 71, hash E6801694 - sample 502: - time = 16733333 - flags = 0 - data = length 68, hash 83DC1BAA - sample 503: - time = 16700000 - flags = 0 - data = length 68, hash 66214C05 - sample 504: - time = 16766666 - flags = 0 - data = length 68, hash 4605E639 - sample 505: - time = 16933333 - flags = 0 - data = length 77, hash 76DDE100 - sample 506: - time = 16866666 - flags = 0 - data = length 70, hash 98EFD089 - sample 507: - time = 16833333 - flags = 0 - data = length 68, hash 88475340 - sample 508: - time = 16900000 - flags = 0 - data = length 68, hash 682BED75 - sample 509: - time = 17066666 - flags = 0 - data = length 77, hash 11CC2C49 - sample 510: - time = 17000000 - flags = 0 - data = length 70, hash 7BAE9640 - sample 511: - time = 16966666 - flags = 0 - data = length 68, hash E8DCF97C - sample 512: - time = 17033333 - flags = 0 - data = length 68, hash C8C193B1 - sample 513: - time = 17200000 - flags = 0 - data = length 77, hash 6967C98C - sample 514: - time = 17133333 - flags = 0 - data = length 70, hash 5B33157E - sample 515: - time = 17100000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 516: - time = 17166666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 517: - time = 17333333 - flags = 0 - data = length 77, hash 434E1BB0 - sample 518: - time = 17266666 - flags = 0 - data = length 70, hash ECF81CBA - sample 519: - time = 17233333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 520: - time = 17300000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 521: - time = 17466666 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 522: - time = 17400000 - flags = 0 - data = length 70, hash 1DB944F6 - sample 523: - time = 17366666 - flags = 0 - data = length 68, hash BFBDD171 - sample 524: - time = 17433333 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 525: - time = 17600000 - flags = 0 - data = length 77, hash C6D21538 - sample 526: - time = 17533333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 527: - time = 17500000 - flags = 0 - data = length 68, hash 205377AE - sample 528: - time = 17566666 - flags = 0 - data = length 68, hash 3811E1 - sample 529: - time = 17733333 - flags = 0 - data = length 77, hash B8F74A5B - sample 530: - time = 17666666 - flags = 0 - data = length 70, hash F383DB6D - sample 531: - time = 17633333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 532: - time = 17700000 - flags = 0 - data = length 68, hash 6D3E33DC - sample 533: - time = 17866666 - flags = 0 - data = length 77, hash 8E4832C1 - sample 534: - time = 17800000 - flags = 0 - data = length 70, hash B92866C8 - sample 535: - time = 17766666 - flags = 0 - data = length 68, hash 66214C05 - sample 536: - time = 17833333 - flags = 0 - data = length 68, hash 4605E639 - sample 537: - time = 18000000 - flags = 0 - data = length 77, hash E5E3D004 - sample 538: - time = 17933333 - flags = 0 - data = length 70, hash E9E98F05 - sample 539: - time = 17900000 - flags = 0 - data = length 68, hash 88475340 - sample 540: - time = 17966666 - flags = 0 - data = length 68, hash 682BED75 - sample 541: - time = 18133333 - flags = 0 - data = length 77, hash 11CC2C49 - sample 542: - time = 18066666 - flags = 0 - data = length 70, hash 7BAE9640 - sample 543: - time = 18033333 - flags = 0 - data = length 68, hash E8DCF97C - sample 544: - time = 18100000 - flags = 0 - data = length 68, hash C8C193B1 - sample 545: - time = 18266666 - flags = 0 - data = length 77, hash 6967C98C - sample 546: - time = 18200000 - flags = 0 - data = length 70, hash 5B33157E - sample 547: - time = 18166666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 548: - time = 18233333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 549: - time = 18400000 - flags = 0 - data = length 77, hash 434E1BB0 - sample 550: - time = 18333333 - flags = 0 - data = length 70, hash ECF81CBA - sample 551: - time = 18300000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 552: - time = 18366666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 553: - time = 18533333 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 554: - time = 18466666 - flags = 0 - data = length 70, hash 1DB944F6 - sample 555: - time = 18433333 - flags = 0 - data = length 68, hash BFBDD171 - sample 556: - time = 18500000 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 557: - time = 18666666 - flags = 0 - data = length 77, hash C6D21538 - sample 558: - time = 18600000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 559: - time = 18566666 - flags = 0 - data = length 68, hash 205377AE - sample 560: - time = 18633333 - flags = 0 - data = length 68, hash 3811E1 - sample 561: - time = 18800000 - flags = 0 - data = length 77, hash B8F74A5B - sample 562: - time = 18733333 - flags = 0 - data = length 70, hash F383DB6D - sample 563: - time = 18700000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 564: - time = 18766666 - flags = 0 - data = length 68, hash 6D3E33DC - sample 565: - time = 18933333 - flags = 0 - data = length 77, hash 8E4832C1 - sample 566: - time = 18866666 - flags = 0 - data = length 70, hash B92866C8 - sample 567: - time = 18833333 - flags = 0 - data = length 68, hash 66214C05 - sample 568: - time = 18900000 - flags = 0 - data = length 68, hash 4605E639 - sample 569: - time = 19066666 - flags = 0 - data = length 77, hash E5E3D004 - sample 570: - time = 19000000 - flags = 0 - data = length 70, hash E9E98F05 - sample 571: - time = 18966666 - flags = 0 - data = length 68, hash 88475340 - sample 572: - time = 19033333 - flags = 0 - data = length 68, hash 682BED75 - sample 573: - time = 19200000 - flags = 0 - data = length 77, hash 11CC2C49 - sample 574: - time = 19133333 - flags = 0 - data = length 70, hash 7BAE9640 - sample 575: - time = 19100000 - flags = 0 - data = length 68, hash E8DCF97C - sample 576: - time = 19166666 - flags = 0 - data = length 68, hash C8C193B1 - sample 577: - time = 19333333 - flags = 0 - data = length 77, hash 6967C98C - sample 578: - time = 19266666 - flags = 0 - data = length 70, hash 5B33157E - sample 579: - time = 19233333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 580: - time = 19300000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 581: - time = 19466666 - flags = 0 - data = length 77, hash 434E1BB0 - sample 582: - time = 19400000 - flags = 0 - data = length 70, hash ECF81CBA - sample 583: - time = 19366666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 584: - time = 19433333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 585: - time = 19600000 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 586: - time = 19533333 - flags = 0 - data = length 70, hash 1DB944F6 - sample 587: - time = 19500000 - flags = 0 - data = length 68, hash BFBDD171 - sample 588: - time = 19566666 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 589: - time = 19733333 - flags = 0 - data = length 77, hash 179FCFE9 - sample 590: - time = 19666666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 591: - time = 19633333 - flags = 0 - data = length 68, hash 205377AE - sample 592: - time = 19700000 - flags = 0 - data = length 68, hash 3811E1 - sample 593: - time = 19866666 - flags = 0 - data = length 77, hash BCD7CD76 - sample 594: - time = 19800000 - flags = 0 - data = length 70, hash F383DB6D - sample 595: - time = 19766666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 596: - time = 19833333 - flags = 0 - data = length 68, hash 6D3E33DC - sample 597: - time = 19966666 - flags = 0 - data = length 77, hash 38D96DF4 - sample 598: - time = 19900000 - flags = 0 - data = length 70, hash 3AC762E9 - sample 599: - time = 19933333 - flags = 0 - data = length 68, hash B1C7769A -tracksEnded = true 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 deleted file mode 100644 index cf0b315ef69..00000000000 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.2.dump +++ /dev/null @@ -1,2430 +0,0 @@ -seekMap: - isSeekable = true - duration = 8333333 - getPosition(0) = [[timeUs=0, position=871]] - getPosition(1) = [[timeUs=0, position=871]] - getPosition(4166666) = [[timeUs=0, position=871]] - getPosition(8333333) = [[timeUs=0, position=871]] -numberOfTracks = 1 -track 0: - total output bytes = 44195 - sample count = 600 - format 0: - averageBitrate = 17678 - peakBitrate = 17678 - id = 1 - containerMimeType = video/mp4 - sampleMimeType = video/avc - codecs = avc1.640028 - maxNumReorderSamples = 2 - width = 1920 - height = 1080 - colorInfo: - 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 - sample 0: - time = 0 - flags = 1 - data = length 1120, hash 352E33B0 - sample 1: - time = 133333 - flags = 0 - data = length 71, hash E6801693 - sample 2: - time = 66666 - flags = 0 - data = length 68, hash 83DC1BAB - sample 3: - time = 33333 - flags = 0 - data = length 68, hash 66214C04 - sample 4: - time = 100000 - flags = 0 - data = length 68, hash 4605E639 - sample 5: - time = 266666 - flags = 0 - data = length 77, hash 76DDE101 - sample 6: - time = 200000 - flags = 0 - data = length 70, hash 98EFD089 - sample 7: - time = 166666 - flags = 0 - data = length 68, hash 88475341 - sample 8: - time = 233333 - flags = 0 - data = length 68, hash 682BED74 - sample 9: - time = 400000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 10: - time = 333333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 11: - time = 300000 - flags = 0 - data = length 68, hash E8DCF97C - sample 12: - time = 366666 - flags = 0 - data = length 68, hash C8C193B0 - sample 13: - time = 533333 - flags = 0 - data = length 77, hash 6967C98D - sample 14: - time = 466666 - flags = 0 - data = length 70, hash 5B33157E - sample 15: - time = 433333 - flags = 0 - data = length 68, hash 3D0223FA - sample 16: - time = 500000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 17: - time = 666666 - flags = 0 - data = length 77, hash 434E1BAF - sample 18: - time = 600000 - flags = 0 - data = length 70, hash ECF81CBA - sample 19: - time = 566666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 20: - time = 633333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 21: - time = 800000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 22: - time = 733333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 23: - time = 700000 - flags = 0 - data = length 68, hash BFBDD172 - sample 24: - time = 766666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 25: - time = 933333 - flags = 0 - data = length 77, hash C6D21537 - sample 26: - time = 866666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 27: - time = 833333 - flags = 0 - data = length 68, hash 205377AD - sample 28: - time = 900000 - flags = 0 - data = length 68, hash 3811E2 - sample 29: - time = 1066666 - flags = 0 - data = length 77, hash B8F74A5C - sample 30: - time = 1000000 - flags = 0 - data = length 70, hash F383DB6C - sample 31: - time = 966666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 32: - time = 1033333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 33: - time = 1200000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 34: - time = 1133333 - flags = 0 - data = length 70, hash B92866C9 - sample 35: - time = 1100000 - flags = 0 - data = length 68, hash 66214C04 - sample 36: - time = 1166666 - flags = 0 - data = length 68, hash 4605E639 - sample 37: - time = 1333333 - flags = 0 - data = length 77, hash E5E3D005 - sample 38: - time = 1266666 - flags = 0 - data = length 70, hash E9E98F05 - sample 39: - time = 1233333 - flags = 0 - data = length 68, hash 88475341 - sample 40: - time = 1300000 - flags = 0 - data = length 68, hash 682BED74 - sample 41: - time = 1466666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 42: - time = 1400000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 43: - time = 1366666 - flags = 0 - data = length 68, hash E8DCF97C - sample 44: - time = 1433333 - flags = 0 - data = length 68, hash C8C193B0 - sample 45: - time = 1600000 - flags = 0 - data = length 77, hash 6967C98D - sample 46: - time = 1533333 - flags = 0 - data = length 70, hash 5B33157E - sample 47: - time = 1500000 - flags = 0 - data = length 68, hash 3D0223FA - sample 48: - time = 1566666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 49: - time = 1733333 - flags = 0 - data = length 77, hash 434E1BAF - sample 50: - time = 1666666 - flags = 0 - data = length 70, hash ECF81CBA - sample 51: - time = 1633333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 52: - time = 1700000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 53: - time = 1866666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 54: - time = 1800000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 55: - time = 1766666 - flags = 0 - data = length 68, hash BFBDD172 - sample 56: - time = 1833333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 57: - time = 2000000 - flags = 0 - data = length 77, hash C6D21537 - sample 58: - time = 1933333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 59: - time = 1900000 - flags = 0 - data = length 68, hash 205377AD - sample 60: - time = 1966666 - flags = 0 - data = length 68, hash 3811E2 - sample 61: - time = 2133333 - flags = 0 - data = length 77, hash B8F74A5C - sample 62: - time = 2066666 - flags = 0 - data = length 70, hash F383DB6C - sample 63: - time = 2033333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 64: - time = 2100000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 65: - time = 2266666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 66: - time = 2200000 - flags = 0 - data = length 70, hash B92866C9 - sample 67: - time = 2166666 - flags = 0 - data = length 68, hash 66214C04 - sample 68: - time = 2233333 - flags = 0 - data = length 68, hash 4605E639 - sample 69: - time = 2400000 - flags = 0 - data = length 77, hash E5E3D005 - sample 70: - time = 2333333 - flags = 0 - data = length 70, hash E9E98F05 - sample 71: - time = 2300000 - flags = 0 - data = length 68, hash 88475341 - sample 72: - time = 2366666 - flags = 0 - data = length 68, hash 682BED74 - sample 73: - time = 2533333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 74: - time = 2466666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 75: - time = 2433333 - flags = 0 - data = length 68, hash E8DCF97C - sample 76: - time = 2500000 - flags = 0 - data = length 68, hash C8C193B0 - sample 77: - time = 2666666 - flags = 0 - data = length 77, hash 6967C98D - sample 78: - time = 2600000 - flags = 0 - data = length 70, hash 5B33157E - sample 79: - time = 2566666 - flags = 0 - data = length 68, hash 3D0223FA - sample 80: - time = 2633333 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 81: - time = 2800000 - flags = 0 - data = length 77, hash 434E1BAF - sample 82: - time = 2733333 - flags = 0 - data = length 70, hash ECF81CBA - sample 83: - time = 2700000 - flags = 0 - data = length 68, hash 9D97CA35 - sample 84: - time = 2766666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 85: - time = 2933333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 86: - time = 2866666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 87: - time = 2833333 - flags = 0 - data = length 68, hash BFBDD172 - sample 88: - time = 2900000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 89: - time = 3066666 - flags = 0 - data = length 77, hash C6D21537 - sample 90: - time = 3000000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 91: - time = 2966666 - flags = 0 - data = length 68, hash 205377AD - sample 92: - time = 3033333 - flags = 0 - data = length 68, hash 3811E2 - sample 93: - time = 3200000 - flags = 0 - data = length 77, hash B8F74A5C - sample 94: - time = 3133333 - flags = 0 - data = length 70, hash F383DB6C - sample 95: - time = 3100000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 96: - time = 3166666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 97: - time = 3333333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 98: - time = 3266666 - flags = 0 - data = length 70, hash B92866C9 - sample 99: - time = 3233333 - flags = 0 - data = length 68, hash 66214C04 - sample 100: - time = 3300000 - flags = 0 - data = length 68, hash 4605E639 - sample 101: - time = 3466666 - flags = 0 - data = length 77, hash E5E3D005 - sample 102: - time = 3400000 - flags = 0 - data = length 70, hash E9E98F05 - sample 103: - time = 3366666 - flags = 0 - data = length 68, hash 88475341 - sample 104: - time = 3433333 - flags = 0 - data = length 68, hash 682BED74 - sample 105: - time = 3600000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 106: - time = 3533333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 107: - time = 3500000 - flags = 0 - data = length 68, hash E8DCF97C - sample 108: - time = 3566666 - flags = 0 - data = length 68, hash C8C193B0 - sample 109: - time = 3733333 - flags = 0 - data = length 77, hash 6967C98D - sample 110: - time = 3666666 - flags = 0 - data = length 70, hash 5B33157E - sample 111: - time = 3633333 - flags = 0 - data = length 68, hash 3D0223FA - sample 112: - time = 3700000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 113: - time = 3866666 - flags = 0 - data = length 77, hash 434E1BAF - sample 114: - time = 3800000 - flags = 0 - data = length 70, hash ECF81CBA - sample 115: - time = 3766666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 116: - time = 3833333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 117: - time = 4000000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 118: - time = 3933333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 119: - time = 3900000 - flags = 0 - data = length 68, hash BFBDD172 - sample 120: - time = 3966666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 121: - time = 4133333 - flags = 0 - data = length 77, hash C6D21537 - sample 122: - time = 4066666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 123: - time = 4033333 - flags = 0 - data = length 68, hash 205377AD - sample 124: - time = 4100000 - flags = 0 - data = length 68, hash 3811E2 - sample 125: - time = 4266666 - flags = 0 - data = length 77, hash B8F74A5C - sample 126: - time = 4200000 - flags = 0 - data = length 70, hash F383DB6C - sample 127: - time = 4166666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 128: - time = 4233333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 129: - time = 4400000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 130: - time = 4333333 - flags = 0 - data = length 70, hash B92866C9 - sample 131: - time = 4300000 - flags = 0 - data = length 68, hash 66214C04 - sample 132: - time = 4366666 - flags = 0 - data = length 68, hash 4605E639 - sample 133: - time = 4533333 - flags = 0 - data = length 77, hash E5E3D005 - sample 134: - time = 4466666 - flags = 0 - data = length 70, hash E9E98F05 - sample 135: - time = 4433333 - flags = 0 - data = length 68, hash 88475341 - sample 136: - time = 4500000 - flags = 0 - data = length 68, hash 682BED74 - sample 137: - time = 4666666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 138: - time = 4600000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 139: - time = 4566666 - flags = 0 - data = length 68, hash E8DCF97C - sample 140: - time = 4633333 - flags = 0 - data = length 68, hash C8C193B0 - sample 141: - time = 4800000 - flags = 0 - data = length 77, hash 6967C98D - sample 142: - time = 4733333 - flags = 0 - data = length 70, hash 5B33157E - sample 143: - time = 4700000 - flags = 0 - data = length 68, hash 3D0223FA - sample 144: - time = 4766666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 145: - time = 4933333 - flags = 0 - data = length 77, hash 434E1BAF - sample 146: - time = 4866666 - flags = 0 - data = length 70, hash ECF81CBA - sample 147: - time = 4833333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 148: - time = 4900000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 149: - time = 5066666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 150: - time = 5000000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 151: - time = 4966666 - flags = 0 - data = length 68, hash BFBDD172 - sample 152: - time = 5033333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 153: - time = 5200000 - flags = 0 - data = length 77, hash C6D21537 - sample 154: - time = 5133333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 155: - time = 5100000 - flags = 0 - data = length 68, hash 205377AD - sample 156: - time = 5166666 - flags = 0 - data = length 68, hash 3811E2 - sample 157: - time = 5333333 - flags = 0 - data = length 77, hash B8F74A5C - sample 158: - time = 5266666 - flags = 0 - data = length 70, hash F383DB6C - sample 159: - time = 5233333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 160: - time = 5300000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 161: - time = 5466666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 162: - time = 5400000 - flags = 0 - data = length 70, hash B92866C9 - sample 163: - time = 5366666 - flags = 0 - data = length 68, hash 66214C04 - sample 164: - time = 5433333 - flags = 0 - data = length 68, hash 4605E639 - sample 165: - time = 5600000 - flags = 0 - data = length 77, hash E5E3D005 - sample 166: - time = 5533333 - flags = 0 - data = length 70, hash E9E98F05 - sample 167: - time = 5500000 - flags = 0 - data = length 68, hash 88475341 - sample 168: - time = 5566666 - flags = 0 - data = length 68, hash 682BED74 - sample 169: - time = 5733333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 170: - time = 5666666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 171: - time = 5633333 - flags = 0 - data = length 68, hash E8DCF97C - sample 172: - time = 5700000 - flags = 0 - data = length 68, hash C8C193B0 - sample 173: - time = 5866666 - flags = 0 - data = length 77, hash 6967C98D - sample 174: - time = 5800000 - flags = 0 - data = length 70, hash 5B33157E - sample 175: - time = 5766666 - flags = 0 - data = length 68, hash 3D0223FA - sample 176: - time = 5833333 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 177: - time = 6000000 - flags = 0 - data = length 77, hash 434E1BAF - sample 178: - time = 5933333 - flags = 0 - data = length 70, hash ECF81CBA - sample 179: - time = 5900000 - flags = 0 - data = length 68, hash 9D97CA35 - sample 180: - time = 5966666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 181: - time = 6133333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 182: - time = 6066666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 183: - time = 6033333 - flags = 0 - data = length 68, hash BFBDD172 - sample 184: - time = 6100000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 185: - time = 6266666 - flags = 0 - data = length 77, hash C6D21537 - sample 186: - time = 6200000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 187: - time = 6166666 - flags = 0 - data = length 68, hash 205377AD - sample 188: - time = 6233333 - flags = 0 - data = length 68, hash 3811E2 - sample 189: - time = 6400000 - flags = 0 - data = length 77, hash B8F74A5C - sample 190: - time = 6333333 - flags = 0 - data = length 70, hash F383DB6C - sample 191: - time = 6300000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 192: - time = 6366666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 193: - time = 6533333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 194: - time = 6466666 - flags = 0 - data = length 70, hash B92866C9 - sample 195: - time = 6433333 - flags = 0 - data = length 68, hash 66214C04 - sample 196: - time = 6500000 - flags = 0 - data = length 68, hash 4605E639 - sample 197: - time = 6666666 - flags = 0 - data = length 77, hash E5E3D005 - sample 198: - time = 6600000 - flags = 0 - data = length 70, hash E9E98F05 - sample 199: - time = 6566666 - flags = 0 - data = length 68, hash 88475341 - sample 200: - time = 6633333 - flags = 0 - data = length 68, hash 682BED74 - sample 201: - time = 6800000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 202: - time = 6733333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 203: - time = 6700000 - flags = 0 - data = length 68, hash E8DCF97C - sample 204: - time = 6766666 - flags = 0 - data = length 68, hash C8C193B0 - sample 205: - time = 6933333 - flags = 0 - data = length 77, hash 6967C98D - sample 206: - time = 6866666 - flags = 0 - data = length 70, hash 5B33157E - sample 207: - time = 6833333 - flags = 0 - data = length 68, hash 3D0223FA - sample 208: - time = 6900000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 209: - time = 7066666 - flags = 0 - data = length 77, hash 434E1BAF - sample 210: - time = 7000000 - flags = 0 - data = length 70, hash ECF81CBA - sample 211: - time = 6966666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 212: - time = 7033333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 213: - time = 7200000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 214: - time = 7133333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 215: - time = 7100000 - flags = 0 - data = length 68, hash BFBDD172 - sample 216: - time = 7166666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 217: - time = 7333333 - flags = 0 - data = length 77, hash C6D21537 - sample 218: - time = 7266666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 219: - time = 7233333 - flags = 0 - data = length 68, hash 205377AD - sample 220: - time = 7300000 - flags = 0 - data = length 68, hash 3811E2 - sample 221: - time = 7466666 - flags = 0 - data = length 77, hash B8F74A5C - sample 222: - time = 7400000 - flags = 0 - data = length 70, hash F383DB6C - sample 223: - time = 7366666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 224: - time = 7433333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 225: - time = 7600000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 226: - time = 7533333 - flags = 0 - data = length 70, hash B92866C9 - sample 227: - time = 7500000 - flags = 0 - data = length 68, hash 66214C04 - sample 228: - time = 7566666 - flags = 0 - data = length 68, hash 4605E639 - sample 229: - time = 7733333 - flags = 0 - data = length 77, hash E5E3D005 - sample 230: - time = 7666666 - flags = 0 - data = length 70, hash E9E98F05 - sample 231: - time = 7633333 - flags = 0 - data = length 68, hash 88475341 - sample 232: - time = 7700000 - flags = 0 - data = length 68, hash 682BED74 - sample 233: - time = 7866666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 234: - time = 7800000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 235: - time = 7766666 - flags = 0 - data = length 68, hash E8DCF97C - sample 236: - time = 7833333 - flags = 0 - data = length 68, hash C8C193B0 - sample 237: - time = 8000000 - flags = 0 - data = length 77, hash 6967C98D - sample 238: - time = 7933333 - flags = 0 - data = length 70, hash 5B33157E - sample 239: - time = 7900000 - flags = 0 - data = length 68, hash 3D0223FA - sample 240: - time = 7966666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 241: - time = 8133333 - flags = 0 - data = length 77, hash 434E1BAF - sample 242: - time = 8066666 - flags = 0 - data = length 70, hash ECF81CBA - sample 243: - time = 8033333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 244: - time = 8100000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 245: - time = 8266666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 246: - time = 8200000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 247: - time = 8166666 - flags = 0 - data = length 68, hash BFBDD172 - sample 248: - time = 8233333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 249: - time = 8300000 - flags = 0 - data = length 77, hash AB0BE9E8 - sample 250: - time = 8333333 - flags = 1 - data = length 423, hash 724FF09B - sample 251: - time = 8466666 - flags = 0 - data = length 71, hash E6801693 - sample 252: - time = 8400000 - flags = 0 - data = length 68, hash 83DC1BAB - sample 253: - time = 8366666 - flags = 0 - data = length 68, hash 66214C05 - sample 254: - time = 8433333 - flags = 0 - data = length 68, hash 4605E638 - sample 255: - time = 8600000 - flags = 0 - data = length 77, hash 76DDE100 - sample 256: - time = 8533333 - flags = 0 - data = length 70, hash 98EFD089 - sample 257: - time = 8500000 - flags = 0 - data = length 68, hash 88475340 - sample 258: - time = 8566666 - flags = 0 - data = length 68, hash 682BED75 - sample 259: - time = 8733333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 260: - time = 8666666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 261: - time = 8633333 - flags = 0 - data = length 68, hash E8DCF97D - sample 262: - time = 8700000 - flags = 0 - data = length 68, hash C8C193B1 - sample 263: - time = 8866666 - flags = 0 - data = length 77, hash 6967C98D - sample 264: - time = 8800000 - flags = 0 - data = length 70, hash 5B33157D - sample 265: - time = 8766666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 266: - time = 8833333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 267: - time = 9000000 - flags = 0 - data = length 77, hash 434E1BAF - sample 268: - time = 8933333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 269: - time = 8900000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 270: - time = 8966666 - flags = 0 - data = length 68, hash 7D7C646A - sample 271: - time = 9133333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 272: - time = 9066666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 273: - time = 9033333 - flags = 0 - data = length 68, hash BFBDD171 - sample 274: - time = 9100000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 275: - time = 9266666 - flags = 0 - data = length 77, hash C6D21537 - sample 276: - time = 9200000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 277: - time = 9166666 - flags = 0 - data = length 68, hash 205377AE - sample 278: - time = 9233333 - flags = 0 - data = length 68, hash 3811E1 - sample 279: - time = 9400000 - flags = 0 - data = length 77, hash B8F74A5C - sample 280: - time = 9333333 - flags = 0 - data = length 70, hash F383DB6D - sample 281: - time = 9300000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 282: - time = 9366666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 283: - time = 9533333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 284: - time = 9466666 - flags = 0 - data = length 70, hash B92866C9 - sample 285: - time = 9433333 - flags = 0 - data = length 68, hash 66214C05 - sample 286: - time = 9500000 - flags = 0 - data = length 68, hash 4605E638 - sample 287: - time = 9666666 - flags = 0 - data = length 77, hash E5E3D004 - sample 288: - time = 9600000 - flags = 0 - data = length 70, hash E9E98F05 - sample 289: - time = 9566666 - flags = 0 - data = length 68, hash 88475340 - sample 290: - time = 9633333 - flags = 0 - data = length 68, hash 682BED75 - sample 291: - time = 9800000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 292: - time = 9733333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 293: - time = 9700000 - flags = 0 - data = length 68, hash E8DCF97D - sample 294: - time = 9766666 - flags = 0 - data = length 68, hash C8C193B1 - sample 295: - time = 9933333 - flags = 0 - data = length 77, hash 6967C98D - sample 296: - time = 9866666 - flags = 0 - data = length 70, hash 5B33157D - sample 297: - time = 9833333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 298: - time = 9900000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 299: - time = 10066666 - flags = 0 - data = length 77, hash 434E1BAF - sample 300: - time = 10000000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 301: - time = 9966666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 302: - time = 10033333 - flags = 0 - data = length 68, hash 7D7C646A - sample 303: - time = 10200000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 304: - time = 10133333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 305: - time = 10100000 - flags = 0 - data = length 68, hash BFBDD171 - sample 306: - time = 10166666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 307: - time = 10333333 - flags = 0 - data = length 77, hash C6D21537 - sample 308: - time = 10266666 - flags = 0 - data = length 70, hash AF7E4C31 - sample 309: - time = 10233333 - flags = 0 - data = length 68, hash 205377AE - sample 310: - time = 10300000 - flags = 0 - data = length 68, hash 3811E1 - sample 311: - time = 10466666 - flags = 0 - data = length 77, hash B8F74A5C - sample 312: - time = 10400000 - flags = 0 - data = length 70, hash F383DB6D - sample 313: - time = 10366666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 314: - time = 10433333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 315: - time = 10600000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 316: - time = 10533333 - flags = 0 - data = length 70, hash B92866C9 - sample 317: - time = 10500000 - flags = 0 - data = length 68, hash 66214C05 - sample 318: - time = 10566666 - flags = 0 - data = length 68, hash 4605E638 - sample 319: - time = 10733333 - flags = 0 - data = length 77, hash E5E3D004 - sample 320: - time = 10666666 - flags = 0 - data = length 70, hash E9E98F05 - sample 321: - time = 10633333 - flags = 0 - data = length 68, hash 88475340 - sample 322: - time = 10700000 - flags = 0 - data = length 68, hash 682BED75 - sample 323: - time = 10866666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 324: - time = 10800000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 325: - time = 10766666 - flags = 0 - data = length 68, hash E8DCF97D - sample 326: - time = 10833333 - flags = 0 - data = length 68, hash C8C193B1 - sample 327: - time = 11000000 - flags = 0 - data = length 77, hash 6967C98D - sample 328: - time = 10933333 - flags = 0 - data = length 70, hash 5B33157D - sample 329: - time = 10900000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 330: - time = 10966666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 331: - time = 11133333 - flags = 0 - data = length 77, hash 434E1BAF - sample 332: - time = 11066666 - flags = 0 - data = length 70, hash ECF81CB9 - sample 333: - time = 11033333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 334: - time = 11100000 - flags = 0 - data = length 68, hash 7D7C646A - sample 335: - time = 11266666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 336: - time = 11200000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 337: - time = 11166666 - flags = 0 - data = length 68, hash BFBDD171 - sample 338: - time = 11233333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 339: - time = 11400000 - flags = 0 - data = length 77, hash C6D21537 - sample 340: - time = 11333333 - flags = 0 - data = length 70, hash AF7E4C31 - sample 341: - time = 11300000 - flags = 0 - data = length 68, hash 205377AE - sample 342: - time = 11366666 - flags = 0 - data = length 68, hash 3811E1 - sample 343: - time = 11533333 - flags = 0 - data = length 77, hash B8F74A5C - sample 344: - time = 11466666 - flags = 0 - data = length 70, hash F383DB6D - sample 345: - time = 11433333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 346: - time = 11500000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 347: - time = 11666666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 348: - time = 11600000 - flags = 0 - data = length 70, hash B92866C9 - sample 349: - time = 11566666 - flags = 0 - data = length 68, hash 66214C05 - sample 350: - time = 11633333 - flags = 0 - data = length 68, hash 4605E638 - sample 351: - time = 11800000 - flags = 0 - data = length 77, hash E5E3D004 - sample 352: - time = 11733333 - flags = 0 - data = length 70, hash E9E98F05 - sample 353: - time = 11700000 - flags = 0 - data = length 68, hash 88475340 - sample 354: - time = 11766666 - flags = 0 - data = length 68, hash 682BED75 - sample 355: - time = 11933333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 356: - time = 11866666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 357: - time = 11833333 - flags = 0 - data = length 68, hash E8DCF97D - sample 358: - time = 11900000 - flags = 0 - data = length 68, hash C8C193B1 - sample 359: - time = 12066666 - flags = 0 - data = length 77, hash 6967C98D - sample 360: - time = 12000000 - flags = 0 - data = length 70, hash 5B33157D - sample 361: - time = 11966666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 362: - time = 12033333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 363: - time = 12200000 - flags = 0 - data = length 77, hash 434E1BAF - sample 364: - time = 12133333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 365: - time = 12100000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 366: - time = 12166666 - flags = 0 - data = length 68, hash 7D7C646A - sample 367: - time = 12333333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 368: - time = 12266666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 369: - time = 12233333 - flags = 0 - data = length 68, hash BFBDD171 - sample 370: - time = 12300000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 371: - time = 12466666 - flags = 0 - data = length 77, hash C6D21537 - sample 372: - time = 12400000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 373: - time = 12366666 - flags = 0 - data = length 68, hash 205377AE - sample 374: - time = 12433333 - flags = 0 - data = length 68, hash 3811E1 - sample 375: - time = 12600000 - flags = 0 - data = length 77, hash B8F74A5C - sample 376: - time = 12533333 - flags = 0 - data = length 70, hash F383DB6D - sample 377: - time = 12500000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 378: - time = 12566666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 379: - time = 12733333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 380: - time = 12666666 - flags = 0 - data = length 70, hash B92866C9 - sample 381: - time = 12633333 - flags = 0 - data = length 68, hash 66214C05 - sample 382: - time = 12700000 - flags = 0 - data = length 68, hash 4605E638 - sample 383: - time = 12866666 - flags = 0 - data = length 77, hash E5E3D004 - sample 384: - time = 12800000 - flags = 0 - data = length 70, hash E9E98F05 - sample 385: - time = 12766666 - flags = 0 - data = length 68, hash 88475340 - sample 386: - time = 12833333 - flags = 0 - data = length 68, hash 682BED75 - sample 387: - time = 13000000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 388: - time = 12933333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 389: - time = 12900000 - flags = 0 - data = length 68, hash E8DCF97D - sample 390: - time = 12966666 - flags = 0 - data = length 68, hash C8C193B1 - sample 391: - time = 13133333 - flags = 0 - data = length 77, hash 6967C98D - sample 392: - time = 13066666 - flags = 0 - data = length 70, hash 5B33157D - sample 393: - time = 13033333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 394: - time = 13100000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 395: - time = 13266666 - flags = 0 - data = length 77, hash 434E1BAF - sample 396: - time = 13200000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 397: - time = 13166666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 398: - time = 13233333 - flags = 0 - data = length 68, hash 7D7C646A - sample 399: - time = 13400000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 400: - time = 13333333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 401: - time = 13300000 - flags = 0 - data = length 68, hash BFBDD171 - sample 402: - time = 13366666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 403: - time = 13533333 - flags = 0 - data = length 77, hash C6D21537 - sample 404: - time = 13466666 - flags = 0 - data = length 70, hash AF7E4C31 - sample 405: - time = 13433333 - flags = 0 - data = length 68, hash 205377AE - sample 406: - time = 13500000 - flags = 0 - data = length 68, hash 3811E1 - sample 407: - time = 13666666 - flags = 0 - data = length 77, hash B8F74A5C - sample 408: - time = 13600000 - flags = 0 - data = length 70, hash F383DB6D - sample 409: - time = 13566666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 410: - time = 13633333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 411: - time = 13800000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 412: - time = 13733333 - flags = 0 - data = length 70, hash B92866C9 - sample 413: - time = 13700000 - flags = 0 - data = length 68, hash 66214C05 - sample 414: - time = 13766666 - flags = 0 - data = length 68, hash 4605E638 - sample 415: - time = 13933333 - flags = 0 - data = length 77, hash E5E3D004 - sample 416: - time = 13866666 - flags = 0 - data = length 70, hash E9E98F05 - sample 417: - time = 13833333 - flags = 0 - data = length 68, hash 88475340 - sample 418: - time = 13900000 - flags = 0 - data = length 68, hash 682BED75 - sample 419: - time = 14066666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 420: - time = 14000000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 421: - time = 13966666 - flags = 0 - data = length 68, hash E8DCF97D - sample 422: - time = 14033333 - flags = 0 - data = length 68, hash C8C193B1 - sample 423: - time = 14200000 - flags = 0 - data = length 77, hash 6967C98D - sample 424: - time = 14133333 - flags = 0 - data = length 70, hash 5B33157D - sample 425: - time = 14100000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 426: - time = 14166666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 427: - time = 14333333 - flags = 0 - data = length 77, hash 434E1BAF - sample 428: - time = 14266666 - flags = 0 - data = length 70, hash ECF81CB9 - sample 429: - time = 14233333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 430: - time = 14300000 - flags = 0 - data = length 68, hash 7D7C646A - sample 431: - time = 14466666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 432: - time = 14400000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 433: - time = 14366666 - flags = 0 - data = length 68, hash BFBDD171 - sample 434: - time = 14433333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 435: - time = 14600000 - flags = 0 - data = length 77, hash C6D21537 - sample 436: - time = 14533333 - flags = 0 - data = length 70, hash AF7E4C31 - sample 437: - time = 14500000 - flags = 0 - data = length 68, hash 205377AE - sample 438: - time = 14566666 - flags = 0 - data = length 68, hash 3811E1 - sample 439: - time = 14733333 - flags = 0 - data = length 77, hash B8F74A5C - sample 440: - time = 14666666 - flags = 0 - data = length 70, hash F383DB6D - sample 441: - time = 14633333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 442: - time = 14700000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 443: - time = 14866666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 444: - time = 14800000 - flags = 0 - data = length 70, hash B92866C9 - sample 445: - time = 14766666 - flags = 0 - data = length 68, hash 66214C05 - sample 446: - time = 14833333 - flags = 0 - data = length 68, hash 4605E638 - sample 447: - time = 15000000 - flags = 0 - data = length 77, hash E5E3D004 - sample 448: - time = 14933333 - flags = 0 - data = length 70, hash E9E98F05 - sample 449: - time = 14900000 - flags = 0 - data = length 68, hash 88475340 - sample 450: - time = 14966666 - flags = 0 - data = length 68, hash 682BED75 - sample 451: - time = 15133333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 452: - time = 15066666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 453: - time = 15033333 - flags = 0 - data = length 68, hash E8DCF97D - sample 454: - time = 15100000 - flags = 0 - data = length 68, hash C8C193B1 - sample 455: - time = 15266666 - flags = 0 - data = length 77, hash 6967C98D - sample 456: - time = 15200000 - flags = 0 - data = length 70, hash 5B33157D - sample 457: - time = 15166666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 458: - time = 15233333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 459: - time = 15400000 - flags = 0 - data = length 77, hash 434E1BAF - sample 460: - time = 15333333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 461: - time = 15300000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 462: - time = 15366666 - flags = 0 - data = length 68, hash 7D7C646A - sample 463: - time = 15533333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 464: - time = 15466666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 465: - time = 15433333 - flags = 0 - data = length 68, hash BFBDD171 - sample 466: - time = 15500000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 467: - time = 15666666 - flags = 0 - data = length 77, hash C6D21537 - sample 468: - time = 15600000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 469: - time = 15566666 - flags = 0 - data = length 68, hash 205377AE - sample 470: - time = 15633333 - flags = 0 - data = length 68, hash 3811E1 - sample 471: - time = 15800000 - flags = 0 - data = length 77, hash B8F74A5C - sample 472: - time = 15733333 - flags = 0 - data = length 70, hash F383DB6D - sample 473: - time = 15700000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 474: - time = 15766666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 475: - time = 15933333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 476: - time = 15866666 - flags = 0 - data = length 70, hash B92866C9 - sample 477: - time = 15833333 - flags = 0 - data = length 68, hash 66214C05 - sample 478: - time = 15900000 - flags = 0 - data = length 68, hash 4605E638 - sample 479: - time = 16066666 - flags = 0 - data = length 77, hash E5E3D004 - sample 480: - time = 16000000 - flags = 0 - data = length 70, hash E9E98F05 - sample 481: - time = 15966666 - flags = 0 - data = length 68, hash 88475340 - sample 482: - time = 16033333 - flags = 0 - data = length 68, hash 682BED75 - sample 483: - time = 16200000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 484: - time = 16133333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 485: - time = 16100000 - flags = 0 - data = length 68, hash E8DCF97D - sample 486: - time = 16166666 - flags = 0 - data = length 68, hash C8C193B1 - sample 487: - time = 16333333 - flags = 0 - data = length 77, hash 6967C98D - sample 488: - time = 16266666 - flags = 0 - data = length 70, hash 5B33157D - sample 489: - time = 16233333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 490: - time = 16300000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 491: - time = 16466666 - flags = 0 - data = length 77, hash 434E1BAF - sample 492: - time = 16400000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 493: - time = 16366666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 494: - time = 16433333 - flags = 0 - data = length 68, hash 7D7C646A - sample 495: - time = 16600000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 496: - time = 16533333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 497: - time = 16500000 - flags = 0 - data = length 68, hash BFBDD171 - sample 498: - time = 16566666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 499: - time = 16633333 - flags = 0 - data = length 77, hash AB0BE9E8 - sample 500: - time = 16666666 - flags = 1 - data = length 423, hash DDB543E8 - sample 501: - time = 16800000 - flags = 0 - data = length 71, hash E6801694 - sample 502: - time = 16733333 - flags = 0 - data = length 68, hash 83DC1BAA - sample 503: - time = 16700000 - flags = 0 - data = length 68, hash 66214C05 - sample 504: - time = 16766666 - flags = 0 - data = length 68, hash 4605E639 - sample 505: - time = 16933333 - flags = 0 - data = length 77, hash 76DDE100 - sample 506: - time = 16866666 - flags = 0 - data = length 70, hash 98EFD089 - sample 507: - time = 16833333 - flags = 0 - data = length 68, hash 88475340 - sample 508: - time = 16900000 - flags = 0 - data = length 68, hash 682BED75 - sample 509: - time = 17066666 - flags = 0 - data = length 77, hash 11CC2C49 - sample 510: - time = 17000000 - flags = 0 - data = length 70, hash 7BAE9640 - sample 511: - time = 16966666 - flags = 0 - data = length 68, hash E8DCF97C - sample 512: - time = 17033333 - flags = 0 - data = length 68, hash C8C193B1 - sample 513: - time = 17200000 - flags = 0 - data = length 77, hash 6967C98C - sample 514: - time = 17133333 - flags = 0 - data = length 70, hash 5B33157E - sample 515: - time = 17100000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 516: - time = 17166666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 517: - time = 17333333 - flags = 0 - data = length 77, hash 434E1BB0 - sample 518: - time = 17266666 - flags = 0 - data = length 70, hash ECF81CBA - sample 519: - time = 17233333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 520: - time = 17300000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 521: - time = 17466666 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 522: - time = 17400000 - flags = 0 - data = length 70, hash 1DB944F6 - sample 523: - time = 17366666 - flags = 0 - data = length 68, hash BFBDD171 - sample 524: - time = 17433333 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 525: - time = 17600000 - flags = 0 - data = length 77, hash C6D21538 - sample 526: - time = 17533333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 527: - time = 17500000 - flags = 0 - data = length 68, hash 205377AE - sample 528: - time = 17566666 - flags = 0 - data = length 68, hash 3811E1 - sample 529: - time = 17733333 - flags = 0 - data = length 77, hash B8F74A5B - sample 530: - time = 17666666 - flags = 0 - data = length 70, hash F383DB6D - sample 531: - time = 17633333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 532: - time = 17700000 - flags = 0 - data = length 68, hash 6D3E33DC - sample 533: - time = 17866666 - flags = 0 - data = length 77, hash 8E4832C1 - sample 534: - time = 17800000 - flags = 0 - data = length 70, hash B92866C8 - sample 535: - time = 17766666 - flags = 0 - data = length 68, hash 66214C05 - sample 536: - time = 17833333 - flags = 0 - data = length 68, hash 4605E639 - sample 537: - time = 18000000 - flags = 0 - data = length 77, hash E5E3D004 - sample 538: - time = 17933333 - flags = 0 - data = length 70, hash E9E98F05 - sample 539: - time = 17900000 - flags = 0 - data = length 68, hash 88475340 - sample 540: - time = 17966666 - flags = 0 - data = length 68, hash 682BED75 - sample 541: - time = 18133333 - flags = 0 - data = length 77, hash 11CC2C49 - sample 542: - time = 18066666 - flags = 0 - data = length 70, hash 7BAE9640 - sample 543: - time = 18033333 - flags = 0 - data = length 68, hash E8DCF97C - sample 544: - time = 18100000 - flags = 0 - data = length 68, hash C8C193B1 - sample 545: - time = 18266666 - flags = 0 - data = length 77, hash 6967C98C - sample 546: - time = 18200000 - flags = 0 - data = length 70, hash 5B33157E - sample 547: - time = 18166666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 548: - time = 18233333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 549: - time = 18400000 - flags = 0 - data = length 77, hash 434E1BB0 - sample 550: - time = 18333333 - flags = 0 - data = length 70, hash ECF81CBA - sample 551: - time = 18300000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 552: - time = 18366666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 553: - time = 18533333 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 554: - time = 18466666 - flags = 0 - data = length 70, hash 1DB944F6 - sample 555: - time = 18433333 - flags = 0 - data = length 68, hash BFBDD171 - sample 556: - time = 18500000 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 557: - time = 18666666 - flags = 0 - data = length 77, hash C6D21538 - sample 558: - time = 18600000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 559: - time = 18566666 - flags = 0 - data = length 68, hash 205377AE - sample 560: - time = 18633333 - flags = 0 - data = length 68, hash 3811E1 - sample 561: - time = 18800000 - flags = 0 - data = length 77, hash B8F74A5B - sample 562: - time = 18733333 - flags = 0 - data = length 70, hash F383DB6D - sample 563: - time = 18700000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 564: - time = 18766666 - flags = 0 - data = length 68, hash 6D3E33DC - sample 565: - time = 18933333 - flags = 0 - data = length 77, hash 8E4832C1 - sample 566: - time = 18866666 - flags = 0 - data = length 70, hash B92866C8 - sample 567: - time = 18833333 - flags = 0 - data = length 68, hash 66214C05 - sample 568: - time = 18900000 - flags = 0 - data = length 68, hash 4605E639 - sample 569: - time = 19066666 - flags = 0 - data = length 77, hash E5E3D004 - sample 570: - time = 19000000 - flags = 0 - data = length 70, hash E9E98F05 - sample 571: - time = 18966666 - flags = 0 - data = length 68, hash 88475340 - sample 572: - time = 19033333 - flags = 0 - data = length 68, hash 682BED75 - sample 573: - time = 19200000 - flags = 0 - data = length 77, hash 11CC2C49 - sample 574: - time = 19133333 - flags = 0 - data = length 70, hash 7BAE9640 - sample 575: - time = 19100000 - flags = 0 - data = length 68, hash E8DCF97C - sample 576: - time = 19166666 - flags = 0 - data = length 68, hash C8C193B1 - sample 577: - time = 19333333 - flags = 0 - data = length 77, hash 6967C98C - sample 578: - time = 19266666 - flags = 0 - data = length 70, hash 5B33157E - sample 579: - time = 19233333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 580: - time = 19300000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 581: - time = 19466666 - flags = 0 - data = length 77, hash 434E1BB0 - sample 582: - time = 19400000 - flags = 0 - data = length 70, hash ECF81CBA - sample 583: - time = 19366666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 584: - time = 19433333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 585: - time = 19600000 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 586: - time = 19533333 - flags = 0 - data = length 70, hash 1DB944F6 - sample 587: - time = 19500000 - flags = 0 - data = length 68, hash BFBDD171 - sample 588: - time = 19566666 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 589: - time = 19733333 - flags = 0 - data = length 77, hash 179FCFE9 - sample 590: - time = 19666666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 591: - time = 19633333 - flags = 0 - data = length 68, hash 205377AE - sample 592: - time = 19700000 - flags = 0 - data = length 68, hash 3811E1 - sample 593: - time = 19866666 - flags = 0 - data = length 77, hash BCD7CD76 - sample 594: - time = 19800000 - flags = 0 - data = length 70, hash F383DB6D - sample 595: - time = 19766666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 596: - time = 19833333 - flags = 0 - data = length 68, hash 6D3E33DC - sample 597: - time = 19966666 - flags = 0 - data = length 77, hash 38D96DF4 - sample 598: - time = 19900000 - flags = 0 - data = length 70, hash 3AC762E9 - sample 599: - time = 19933333 - flags = 0 - data = length 68, hash B1C7769A -tracksEnded = true 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 deleted file mode 100644 index cf0b315ef69..00000000000 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.3.dump +++ /dev/null @@ -1,2430 +0,0 @@ -seekMap: - isSeekable = true - duration = 8333333 - getPosition(0) = [[timeUs=0, position=871]] - getPosition(1) = [[timeUs=0, position=871]] - getPosition(4166666) = [[timeUs=0, position=871]] - getPosition(8333333) = [[timeUs=0, position=871]] -numberOfTracks = 1 -track 0: - total output bytes = 44195 - sample count = 600 - format 0: - averageBitrate = 17678 - peakBitrate = 17678 - id = 1 - containerMimeType = video/mp4 - sampleMimeType = video/avc - codecs = avc1.640028 - maxNumReorderSamples = 2 - width = 1920 - height = 1080 - colorInfo: - 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 - sample 0: - time = 0 - flags = 1 - data = length 1120, hash 352E33B0 - sample 1: - time = 133333 - flags = 0 - data = length 71, hash E6801693 - sample 2: - time = 66666 - flags = 0 - data = length 68, hash 83DC1BAB - sample 3: - time = 33333 - flags = 0 - data = length 68, hash 66214C04 - sample 4: - time = 100000 - flags = 0 - data = length 68, hash 4605E639 - sample 5: - time = 266666 - flags = 0 - data = length 77, hash 76DDE101 - sample 6: - time = 200000 - flags = 0 - data = length 70, hash 98EFD089 - sample 7: - time = 166666 - flags = 0 - data = length 68, hash 88475341 - sample 8: - time = 233333 - flags = 0 - data = length 68, hash 682BED74 - sample 9: - time = 400000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 10: - time = 333333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 11: - time = 300000 - flags = 0 - data = length 68, hash E8DCF97C - sample 12: - time = 366666 - flags = 0 - data = length 68, hash C8C193B0 - sample 13: - time = 533333 - flags = 0 - data = length 77, hash 6967C98D - sample 14: - time = 466666 - flags = 0 - data = length 70, hash 5B33157E - sample 15: - time = 433333 - flags = 0 - data = length 68, hash 3D0223FA - sample 16: - time = 500000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 17: - time = 666666 - flags = 0 - data = length 77, hash 434E1BAF - sample 18: - time = 600000 - flags = 0 - data = length 70, hash ECF81CBA - sample 19: - time = 566666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 20: - time = 633333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 21: - time = 800000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 22: - time = 733333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 23: - time = 700000 - flags = 0 - data = length 68, hash BFBDD172 - sample 24: - time = 766666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 25: - time = 933333 - flags = 0 - data = length 77, hash C6D21537 - sample 26: - time = 866666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 27: - time = 833333 - flags = 0 - data = length 68, hash 205377AD - sample 28: - time = 900000 - flags = 0 - data = length 68, hash 3811E2 - sample 29: - time = 1066666 - flags = 0 - data = length 77, hash B8F74A5C - sample 30: - time = 1000000 - flags = 0 - data = length 70, hash F383DB6C - sample 31: - time = 966666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 32: - time = 1033333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 33: - time = 1200000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 34: - time = 1133333 - flags = 0 - data = length 70, hash B92866C9 - sample 35: - time = 1100000 - flags = 0 - data = length 68, hash 66214C04 - sample 36: - time = 1166666 - flags = 0 - data = length 68, hash 4605E639 - sample 37: - time = 1333333 - flags = 0 - data = length 77, hash E5E3D005 - sample 38: - time = 1266666 - flags = 0 - data = length 70, hash E9E98F05 - sample 39: - time = 1233333 - flags = 0 - data = length 68, hash 88475341 - sample 40: - time = 1300000 - flags = 0 - data = length 68, hash 682BED74 - sample 41: - time = 1466666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 42: - time = 1400000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 43: - time = 1366666 - flags = 0 - data = length 68, hash E8DCF97C - sample 44: - time = 1433333 - flags = 0 - data = length 68, hash C8C193B0 - sample 45: - time = 1600000 - flags = 0 - data = length 77, hash 6967C98D - sample 46: - time = 1533333 - flags = 0 - data = length 70, hash 5B33157E - sample 47: - time = 1500000 - flags = 0 - data = length 68, hash 3D0223FA - sample 48: - time = 1566666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 49: - time = 1733333 - flags = 0 - data = length 77, hash 434E1BAF - sample 50: - time = 1666666 - flags = 0 - data = length 70, hash ECF81CBA - sample 51: - time = 1633333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 52: - time = 1700000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 53: - time = 1866666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 54: - time = 1800000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 55: - time = 1766666 - flags = 0 - data = length 68, hash BFBDD172 - sample 56: - time = 1833333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 57: - time = 2000000 - flags = 0 - data = length 77, hash C6D21537 - sample 58: - time = 1933333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 59: - time = 1900000 - flags = 0 - data = length 68, hash 205377AD - sample 60: - time = 1966666 - flags = 0 - data = length 68, hash 3811E2 - sample 61: - time = 2133333 - flags = 0 - data = length 77, hash B8F74A5C - sample 62: - time = 2066666 - flags = 0 - data = length 70, hash F383DB6C - sample 63: - time = 2033333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 64: - time = 2100000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 65: - time = 2266666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 66: - time = 2200000 - flags = 0 - data = length 70, hash B92866C9 - sample 67: - time = 2166666 - flags = 0 - data = length 68, hash 66214C04 - sample 68: - time = 2233333 - flags = 0 - data = length 68, hash 4605E639 - sample 69: - time = 2400000 - flags = 0 - data = length 77, hash E5E3D005 - sample 70: - time = 2333333 - flags = 0 - data = length 70, hash E9E98F05 - sample 71: - time = 2300000 - flags = 0 - data = length 68, hash 88475341 - sample 72: - time = 2366666 - flags = 0 - data = length 68, hash 682BED74 - sample 73: - time = 2533333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 74: - time = 2466666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 75: - time = 2433333 - flags = 0 - data = length 68, hash E8DCF97C - sample 76: - time = 2500000 - flags = 0 - data = length 68, hash C8C193B0 - sample 77: - time = 2666666 - flags = 0 - data = length 77, hash 6967C98D - sample 78: - time = 2600000 - flags = 0 - data = length 70, hash 5B33157E - sample 79: - time = 2566666 - flags = 0 - data = length 68, hash 3D0223FA - sample 80: - time = 2633333 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 81: - time = 2800000 - flags = 0 - data = length 77, hash 434E1BAF - sample 82: - time = 2733333 - flags = 0 - data = length 70, hash ECF81CBA - sample 83: - time = 2700000 - flags = 0 - data = length 68, hash 9D97CA35 - sample 84: - time = 2766666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 85: - time = 2933333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 86: - time = 2866666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 87: - time = 2833333 - flags = 0 - data = length 68, hash BFBDD172 - sample 88: - time = 2900000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 89: - time = 3066666 - flags = 0 - data = length 77, hash C6D21537 - sample 90: - time = 3000000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 91: - time = 2966666 - flags = 0 - data = length 68, hash 205377AD - sample 92: - time = 3033333 - flags = 0 - data = length 68, hash 3811E2 - sample 93: - time = 3200000 - flags = 0 - data = length 77, hash B8F74A5C - sample 94: - time = 3133333 - flags = 0 - data = length 70, hash F383DB6C - sample 95: - time = 3100000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 96: - time = 3166666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 97: - time = 3333333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 98: - time = 3266666 - flags = 0 - data = length 70, hash B92866C9 - sample 99: - time = 3233333 - flags = 0 - data = length 68, hash 66214C04 - sample 100: - time = 3300000 - flags = 0 - data = length 68, hash 4605E639 - sample 101: - time = 3466666 - flags = 0 - data = length 77, hash E5E3D005 - sample 102: - time = 3400000 - flags = 0 - data = length 70, hash E9E98F05 - sample 103: - time = 3366666 - flags = 0 - data = length 68, hash 88475341 - sample 104: - time = 3433333 - flags = 0 - data = length 68, hash 682BED74 - sample 105: - time = 3600000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 106: - time = 3533333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 107: - time = 3500000 - flags = 0 - data = length 68, hash E8DCF97C - sample 108: - time = 3566666 - flags = 0 - data = length 68, hash C8C193B0 - sample 109: - time = 3733333 - flags = 0 - data = length 77, hash 6967C98D - sample 110: - time = 3666666 - flags = 0 - data = length 70, hash 5B33157E - sample 111: - time = 3633333 - flags = 0 - data = length 68, hash 3D0223FA - sample 112: - time = 3700000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 113: - time = 3866666 - flags = 0 - data = length 77, hash 434E1BAF - sample 114: - time = 3800000 - flags = 0 - data = length 70, hash ECF81CBA - sample 115: - time = 3766666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 116: - time = 3833333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 117: - time = 4000000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 118: - time = 3933333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 119: - time = 3900000 - flags = 0 - data = length 68, hash BFBDD172 - sample 120: - time = 3966666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 121: - time = 4133333 - flags = 0 - data = length 77, hash C6D21537 - sample 122: - time = 4066666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 123: - time = 4033333 - flags = 0 - data = length 68, hash 205377AD - sample 124: - time = 4100000 - flags = 0 - data = length 68, hash 3811E2 - sample 125: - time = 4266666 - flags = 0 - data = length 77, hash B8F74A5C - sample 126: - time = 4200000 - flags = 0 - data = length 70, hash F383DB6C - sample 127: - time = 4166666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 128: - time = 4233333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 129: - time = 4400000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 130: - time = 4333333 - flags = 0 - data = length 70, hash B92866C9 - sample 131: - time = 4300000 - flags = 0 - data = length 68, hash 66214C04 - sample 132: - time = 4366666 - flags = 0 - data = length 68, hash 4605E639 - sample 133: - time = 4533333 - flags = 0 - data = length 77, hash E5E3D005 - sample 134: - time = 4466666 - flags = 0 - data = length 70, hash E9E98F05 - sample 135: - time = 4433333 - flags = 0 - data = length 68, hash 88475341 - sample 136: - time = 4500000 - flags = 0 - data = length 68, hash 682BED74 - sample 137: - time = 4666666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 138: - time = 4600000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 139: - time = 4566666 - flags = 0 - data = length 68, hash E8DCF97C - sample 140: - time = 4633333 - flags = 0 - data = length 68, hash C8C193B0 - sample 141: - time = 4800000 - flags = 0 - data = length 77, hash 6967C98D - sample 142: - time = 4733333 - flags = 0 - data = length 70, hash 5B33157E - sample 143: - time = 4700000 - flags = 0 - data = length 68, hash 3D0223FA - sample 144: - time = 4766666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 145: - time = 4933333 - flags = 0 - data = length 77, hash 434E1BAF - sample 146: - time = 4866666 - flags = 0 - data = length 70, hash ECF81CBA - sample 147: - time = 4833333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 148: - time = 4900000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 149: - time = 5066666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 150: - time = 5000000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 151: - time = 4966666 - flags = 0 - data = length 68, hash BFBDD172 - sample 152: - time = 5033333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 153: - time = 5200000 - flags = 0 - data = length 77, hash C6D21537 - sample 154: - time = 5133333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 155: - time = 5100000 - flags = 0 - data = length 68, hash 205377AD - sample 156: - time = 5166666 - flags = 0 - data = length 68, hash 3811E2 - sample 157: - time = 5333333 - flags = 0 - data = length 77, hash B8F74A5C - sample 158: - time = 5266666 - flags = 0 - data = length 70, hash F383DB6C - sample 159: - time = 5233333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 160: - time = 5300000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 161: - time = 5466666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 162: - time = 5400000 - flags = 0 - data = length 70, hash B92866C9 - sample 163: - time = 5366666 - flags = 0 - data = length 68, hash 66214C04 - sample 164: - time = 5433333 - flags = 0 - data = length 68, hash 4605E639 - sample 165: - time = 5600000 - flags = 0 - data = length 77, hash E5E3D005 - sample 166: - time = 5533333 - flags = 0 - data = length 70, hash E9E98F05 - sample 167: - time = 5500000 - flags = 0 - data = length 68, hash 88475341 - sample 168: - time = 5566666 - flags = 0 - data = length 68, hash 682BED74 - sample 169: - time = 5733333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 170: - time = 5666666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 171: - time = 5633333 - flags = 0 - data = length 68, hash E8DCF97C - sample 172: - time = 5700000 - flags = 0 - data = length 68, hash C8C193B0 - sample 173: - time = 5866666 - flags = 0 - data = length 77, hash 6967C98D - sample 174: - time = 5800000 - flags = 0 - data = length 70, hash 5B33157E - sample 175: - time = 5766666 - flags = 0 - data = length 68, hash 3D0223FA - sample 176: - time = 5833333 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 177: - time = 6000000 - flags = 0 - data = length 77, hash 434E1BAF - sample 178: - time = 5933333 - flags = 0 - data = length 70, hash ECF81CBA - sample 179: - time = 5900000 - flags = 0 - data = length 68, hash 9D97CA35 - sample 180: - time = 5966666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 181: - time = 6133333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 182: - time = 6066666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 183: - time = 6033333 - flags = 0 - data = length 68, hash BFBDD172 - sample 184: - time = 6100000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 185: - time = 6266666 - flags = 0 - data = length 77, hash C6D21537 - sample 186: - time = 6200000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 187: - time = 6166666 - flags = 0 - data = length 68, hash 205377AD - sample 188: - time = 6233333 - flags = 0 - data = length 68, hash 3811E2 - sample 189: - time = 6400000 - flags = 0 - data = length 77, hash B8F74A5C - sample 190: - time = 6333333 - flags = 0 - data = length 70, hash F383DB6C - sample 191: - time = 6300000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 192: - time = 6366666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 193: - time = 6533333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 194: - time = 6466666 - flags = 0 - data = length 70, hash B92866C9 - sample 195: - time = 6433333 - flags = 0 - data = length 68, hash 66214C04 - sample 196: - time = 6500000 - flags = 0 - data = length 68, hash 4605E639 - sample 197: - time = 6666666 - flags = 0 - data = length 77, hash E5E3D005 - sample 198: - time = 6600000 - flags = 0 - data = length 70, hash E9E98F05 - sample 199: - time = 6566666 - flags = 0 - data = length 68, hash 88475341 - sample 200: - time = 6633333 - flags = 0 - data = length 68, hash 682BED74 - sample 201: - time = 6800000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 202: - time = 6733333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 203: - time = 6700000 - flags = 0 - data = length 68, hash E8DCF97C - sample 204: - time = 6766666 - flags = 0 - data = length 68, hash C8C193B0 - sample 205: - time = 6933333 - flags = 0 - data = length 77, hash 6967C98D - sample 206: - time = 6866666 - flags = 0 - data = length 70, hash 5B33157E - sample 207: - time = 6833333 - flags = 0 - data = length 68, hash 3D0223FA - sample 208: - time = 6900000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 209: - time = 7066666 - flags = 0 - data = length 77, hash 434E1BAF - sample 210: - time = 7000000 - flags = 0 - data = length 70, hash ECF81CBA - sample 211: - time = 6966666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 212: - time = 7033333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 213: - time = 7200000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 214: - time = 7133333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 215: - time = 7100000 - flags = 0 - data = length 68, hash BFBDD172 - sample 216: - time = 7166666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 217: - time = 7333333 - flags = 0 - data = length 77, hash C6D21537 - sample 218: - time = 7266666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 219: - time = 7233333 - flags = 0 - data = length 68, hash 205377AD - sample 220: - time = 7300000 - flags = 0 - data = length 68, hash 3811E2 - sample 221: - time = 7466666 - flags = 0 - data = length 77, hash B8F74A5C - sample 222: - time = 7400000 - flags = 0 - data = length 70, hash F383DB6C - sample 223: - time = 7366666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 224: - time = 7433333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 225: - time = 7600000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 226: - time = 7533333 - flags = 0 - data = length 70, hash B92866C9 - sample 227: - time = 7500000 - flags = 0 - data = length 68, hash 66214C04 - sample 228: - time = 7566666 - flags = 0 - data = length 68, hash 4605E639 - sample 229: - time = 7733333 - flags = 0 - data = length 77, hash E5E3D005 - sample 230: - time = 7666666 - flags = 0 - data = length 70, hash E9E98F05 - sample 231: - time = 7633333 - flags = 0 - data = length 68, hash 88475341 - sample 232: - time = 7700000 - flags = 0 - data = length 68, hash 682BED74 - sample 233: - time = 7866666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 234: - time = 7800000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 235: - time = 7766666 - flags = 0 - data = length 68, hash E8DCF97C - sample 236: - time = 7833333 - flags = 0 - data = length 68, hash C8C193B0 - sample 237: - time = 8000000 - flags = 0 - data = length 77, hash 6967C98D - sample 238: - time = 7933333 - flags = 0 - data = length 70, hash 5B33157E - sample 239: - time = 7900000 - flags = 0 - data = length 68, hash 3D0223FA - sample 240: - time = 7966666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 241: - time = 8133333 - flags = 0 - data = length 77, hash 434E1BAF - sample 242: - time = 8066666 - flags = 0 - data = length 70, hash ECF81CBA - sample 243: - time = 8033333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 244: - time = 8100000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 245: - time = 8266666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 246: - time = 8200000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 247: - time = 8166666 - flags = 0 - data = length 68, hash BFBDD172 - sample 248: - time = 8233333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 249: - time = 8300000 - flags = 0 - data = length 77, hash AB0BE9E8 - sample 250: - time = 8333333 - flags = 1 - data = length 423, hash 724FF09B - sample 251: - time = 8466666 - flags = 0 - data = length 71, hash E6801693 - sample 252: - time = 8400000 - flags = 0 - data = length 68, hash 83DC1BAB - sample 253: - time = 8366666 - flags = 0 - data = length 68, hash 66214C05 - sample 254: - time = 8433333 - flags = 0 - data = length 68, hash 4605E638 - sample 255: - time = 8600000 - flags = 0 - data = length 77, hash 76DDE100 - sample 256: - time = 8533333 - flags = 0 - data = length 70, hash 98EFD089 - sample 257: - time = 8500000 - flags = 0 - data = length 68, hash 88475340 - sample 258: - time = 8566666 - flags = 0 - data = length 68, hash 682BED75 - sample 259: - time = 8733333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 260: - time = 8666666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 261: - time = 8633333 - flags = 0 - data = length 68, hash E8DCF97D - sample 262: - time = 8700000 - flags = 0 - data = length 68, hash C8C193B1 - sample 263: - time = 8866666 - flags = 0 - data = length 77, hash 6967C98D - sample 264: - time = 8800000 - flags = 0 - data = length 70, hash 5B33157D - sample 265: - time = 8766666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 266: - time = 8833333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 267: - time = 9000000 - flags = 0 - data = length 77, hash 434E1BAF - sample 268: - time = 8933333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 269: - time = 8900000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 270: - time = 8966666 - flags = 0 - data = length 68, hash 7D7C646A - sample 271: - time = 9133333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 272: - time = 9066666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 273: - time = 9033333 - flags = 0 - data = length 68, hash BFBDD171 - sample 274: - time = 9100000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 275: - time = 9266666 - flags = 0 - data = length 77, hash C6D21537 - sample 276: - time = 9200000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 277: - time = 9166666 - flags = 0 - data = length 68, hash 205377AE - sample 278: - time = 9233333 - flags = 0 - data = length 68, hash 3811E1 - sample 279: - time = 9400000 - flags = 0 - data = length 77, hash B8F74A5C - sample 280: - time = 9333333 - flags = 0 - data = length 70, hash F383DB6D - sample 281: - time = 9300000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 282: - time = 9366666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 283: - time = 9533333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 284: - time = 9466666 - flags = 0 - data = length 70, hash B92866C9 - sample 285: - time = 9433333 - flags = 0 - data = length 68, hash 66214C05 - sample 286: - time = 9500000 - flags = 0 - data = length 68, hash 4605E638 - sample 287: - time = 9666666 - flags = 0 - data = length 77, hash E5E3D004 - sample 288: - time = 9600000 - flags = 0 - data = length 70, hash E9E98F05 - sample 289: - time = 9566666 - flags = 0 - data = length 68, hash 88475340 - sample 290: - time = 9633333 - flags = 0 - data = length 68, hash 682BED75 - sample 291: - time = 9800000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 292: - time = 9733333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 293: - time = 9700000 - flags = 0 - data = length 68, hash E8DCF97D - sample 294: - time = 9766666 - flags = 0 - data = length 68, hash C8C193B1 - sample 295: - time = 9933333 - flags = 0 - data = length 77, hash 6967C98D - sample 296: - time = 9866666 - flags = 0 - data = length 70, hash 5B33157D - sample 297: - time = 9833333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 298: - time = 9900000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 299: - time = 10066666 - flags = 0 - data = length 77, hash 434E1BAF - sample 300: - time = 10000000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 301: - time = 9966666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 302: - time = 10033333 - flags = 0 - data = length 68, hash 7D7C646A - sample 303: - time = 10200000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 304: - time = 10133333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 305: - time = 10100000 - flags = 0 - data = length 68, hash BFBDD171 - sample 306: - time = 10166666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 307: - time = 10333333 - flags = 0 - data = length 77, hash C6D21537 - sample 308: - time = 10266666 - flags = 0 - data = length 70, hash AF7E4C31 - sample 309: - time = 10233333 - flags = 0 - data = length 68, hash 205377AE - sample 310: - time = 10300000 - flags = 0 - data = length 68, hash 3811E1 - sample 311: - time = 10466666 - flags = 0 - data = length 77, hash B8F74A5C - sample 312: - time = 10400000 - flags = 0 - data = length 70, hash F383DB6D - sample 313: - time = 10366666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 314: - time = 10433333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 315: - time = 10600000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 316: - time = 10533333 - flags = 0 - data = length 70, hash B92866C9 - sample 317: - time = 10500000 - flags = 0 - data = length 68, hash 66214C05 - sample 318: - time = 10566666 - flags = 0 - data = length 68, hash 4605E638 - sample 319: - time = 10733333 - flags = 0 - data = length 77, hash E5E3D004 - sample 320: - time = 10666666 - flags = 0 - data = length 70, hash E9E98F05 - sample 321: - time = 10633333 - flags = 0 - data = length 68, hash 88475340 - sample 322: - time = 10700000 - flags = 0 - data = length 68, hash 682BED75 - sample 323: - time = 10866666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 324: - time = 10800000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 325: - time = 10766666 - flags = 0 - data = length 68, hash E8DCF97D - sample 326: - time = 10833333 - flags = 0 - data = length 68, hash C8C193B1 - sample 327: - time = 11000000 - flags = 0 - data = length 77, hash 6967C98D - sample 328: - time = 10933333 - flags = 0 - data = length 70, hash 5B33157D - sample 329: - time = 10900000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 330: - time = 10966666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 331: - time = 11133333 - flags = 0 - data = length 77, hash 434E1BAF - sample 332: - time = 11066666 - flags = 0 - data = length 70, hash ECF81CB9 - sample 333: - time = 11033333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 334: - time = 11100000 - flags = 0 - data = length 68, hash 7D7C646A - sample 335: - time = 11266666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 336: - time = 11200000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 337: - time = 11166666 - flags = 0 - data = length 68, hash BFBDD171 - sample 338: - time = 11233333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 339: - time = 11400000 - flags = 0 - data = length 77, hash C6D21537 - sample 340: - time = 11333333 - flags = 0 - data = length 70, hash AF7E4C31 - sample 341: - time = 11300000 - flags = 0 - data = length 68, hash 205377AE - sample 342: - time = 11366666 - flags = 0 - data = length 68, hash 3811E1 - sample 343: - time = 11533333 - flags = 0 - data = length 77, hash B8F74A5C - sample 344: - time = 11466666 - flags = 0 - data = length 70, hash F383DB6D - sample 345: - time = 11433333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 346: - time = 11500000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 347: - time = 11666666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 348: - time = 11600000 - flags = 0 - data = length 70, hash B92866C9 - sample 349: - time = 11566666 - flags = 0 - data = length 68, hash 66214C05 - sample 350: - time = 11633333 - flags = 0 - data = length 68, hash 4605E638 - sample 351: - time = 11800000 - flags = 0 - data = length 77, hash E5E3D004 - sample 352: - time = 11733333 - flags = 0 - data = length 70, hash E9E98F05 - sample 353: - time = 11700000 - flags = 0 - data = length 68, hash 88475340 - sample 354: - time = 11766666 - flags = 0 - data = length 68, hash 682BED75 - sample 355: - time = 11933333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 356: - time = 11866666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 357: - time = 11833333 - flags = 0 - data = length 68, hash E8DCF97D - sample 358: - time = 11900000 - flags = 0 - data = length 68, hash C8C193B1 - sample 359: - time = 12066666 - flags = 0 - data = length 77, hash 6967C98D - sample 360: - time = 12000000 - flags = 0 - data = length 70, hash 5B33157D - sample 361: - time = 11966666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 362: - time = 12033333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 363: - time = 12200000 - flags = 0 - data = length 77, hash 434E1BAF - sample 364: - time = 12133333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 365: - time = 12100000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 366: - time = 12166666 - flags = 0 - data = length 68, hash 7D7C646A - sample 367: - time = 12333333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 368: - time = 12266666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 369: - time = 12233333 - flags = 0 - data = length 68, hash BFBDD171 - sample 370: - time = 12300000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 371: - time = 12466666 - flags = 0 - data = length 77, hash C6D21537 - sample 372: - time = 12400000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 373: - time = 12366666 - flags = 0 - data = length 68, hash 205377AE - sample 374: - time = 12433333 - flags = 0 - data = length 68, hash 3811E1 - sample 375: - time = 12600000 - flags = 0 - data = length 77, hash B8F74A5C - sample 376: - time = 12533333 - flags = 0 - data = length 70, hash F383DB6D - sample 377: - time = 12500000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 378: - time = 12566666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 379: - time = 12733333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 380: - time = 12666666 - flags = 0 - data = length 70, hash B92866C9 - sample 381: - time = 12633333 - flags = 0 - data = length 68, hash 66214C05 - sample 382: - time = 12700000 - flags = 0 - data = length 68, hash 4605E638 - sample 383: - time = 12866666 - flags = 0 - data = length 77, hash E5E3D004 - sample 384: - time = 12800000 - flags = 0 - data = length 70, hash E9E98F05 - sample 385: - time = 12766666 - flags = 0 - data = length 68, hash 88475340 - sample 386: - time = 12833333 - flags = 0 - data = length 68, hash 682BED75 - sample 387: - time = 13000000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 388: - time = 12933333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 389: - time = 12900000 - flags = 0 - data = length 68, hash E8DCF97D - sample 390: - time = 12966666 - flags = 0 - data = length 68, hash C8C193B1 - sample 391: - time = 13133333 - flags = 0 - data = length 77, hash 6967C98D - sample 392: - time = 13066666 - flags = 0 - data = length 70, hash 5B33157D - sample 393: - time = 13033333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 394: - time = 13100000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 395: - time = 13266666 - flags = 0 - data = length 77, hash 434E1BAF - sample 396: - time = 13200000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 397: - time = 13166666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 398: - time = 13233333 - flags = 0 - data = length 68, hash 7D7C646A - sample 399: - time = 13400000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 400: - time = 13333333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 401: - time = 13300000 - flags = 0 - data = length 68, hash BFBDD171 - sample 402: - time = 13366666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 403: - time = 13533333 - flags = 0 - data = length 77, hash C6D21537 - sample 404: - time = 13466666 - flags = 0 - data = length 70, hash AF7E4C31 - sample 405: - time = 13433333 - flags = 0 - data = length 68, hash 205377AE - sample 406: - time = 13500000 - flags = 0 - data = length 68, hash 3811E1 - sample 407: - time = 13666666 - flags = 0 - data = length 77, hash B8F74A5C - sample 408: - time = 13600000 - flags = 0 - data = length 70, hash F383DB6D - sample 409: - time = 13566666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 410: - time = 13633333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 411: - time = 13800000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 412: - time = 13733333 - flags = 0 - data = length 70, hash B92866C9 - sample 413: - time = 13700000 - flags = 0 - data = length 68, hash 66214C05 - sample 414: - time = 13766666 - flags = 0 - data = length 68, hash 4605E638 - sample 415: - time = 13933333 - flags = 0 - data = length 77, hash E5E3D004 - sample 416: - time = 13866666 - flags = 0 - data = length 70, hash E9E98F05 - sample 417: - time = 13833333 - flags = 0 - data = length 68, hash 88475340 - sample 418: - time = 13900000 - flags = 0 - data = length 68, hash 682BED75 - sample 419: - time = 14066666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 420: - time = 14000000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 421: - time = 13966666 - flags = 0 - data = length 68, hash E8DCF97D - sample 422: - time = 14033333 - flags = 0 - data = length 68, hash C8C193B1 - sample 423: - time = 14200000 - flags = 0 - data = length 77, hash 6967C98D - sample 424: - time = 14133333 - flags = 0 - data = length 70, hash 5B33157D - sample 425: - time = 14100000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 426: - time = 14166666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 427: - time = 14333333 - flags = 0 - data = length 77, hash 434E1BAF - sample 428: - time = 14266666 - flags = 0 - data = length 70, hash ECF81CB9 - sample 429: - time = 14233333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 430: - time = 14300000 - flags = 0 - data = length 68, hash 7D7C646A - sample 431: - time = 14466666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 432: - time = 14400000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 433: - time = 14366666 - flags = 0 - data = length 68, hash BFBDD171 - sample 434: - time = 14433333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 435: - time = 14600000 - flags = 0 - data = length 77, hash C6D21537 - sample 436: - time = 14533333 - flags = 0 - data = length 70, hash AF7E4C31 - sample 437: - time = 14500000 - flags = 0 - data = length 68, hash 205377AE - sample 438: - time = 14566666 - flags = 0 - data = length 68, hash 3811E1 - sample 439: - time = 14733333 - flags = 0 - data = length 77, hash B8F74A5C - sample 440: - time = 14666666 - flags = 0 - data = length 70, hash F383DB6D - sample 441: - time = 14633333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 442: - time = 14700000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 443: - time = 14866666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 444: - time = 14800000 - flags = 0 - data = length 70, hash B92866C9 - sample 445: - time = 14766666 - flags = 0 - data = length 68, hash 66214C05 - sample 446: - time = 14833333 - flags = 0 - data = length 68, hash 4605E638 - sample 447: - time = 15000000 - flags = 0 - data = length 77, hash E5E3D004 - sample 448: - time = 14933333 - flags = 0 - data = length 70, hash E9E98F05 - sample 449: - time = 14900000 - flags = 0 - data = length 68, hash 88475340 - sample 450: - time = 14966666 - flags = 0 - data = length 68, hash 682BED75 - sample 451: - time = 15133333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 452: - time = 15066666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 453: - time = 15033333 - flags = 0 - data = length 68, hash E8DCF97D - sample 454: - time = 15100000 - flags = 0 - data = length 68, hash C8C193B1 - sample 455: - time = 15266666 - flags = 0 - data = length 77, hash 6967C98D - sample 456: - time = 15200000 - flags = 0 - data = length 70, hash 5B33157D - sample 457: - time = 15166666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 458: - time = 15233333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 459: - time = 15400000 - flags = 0 - data = length 77, hash 434E1BAF - sample 460: - time = 15333333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 461: - time = 15300000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 462: - time = 15366666 - flags = 0 - data = length 68, hash 7D7C646A - sample 463: - time = 15533333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 464: - time = 15466666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 465: - time = 15433333 - flags = 0 - data = length 68, hash BFBDD171 - sample 466: - time = 15500000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 467: - time = 15666666 - flags = 0 - data = length 77, hash C6D21537 - sample 468: - time = 15600000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 469: - time = 15566666 - flags = 0 - data = length 68, hash 205377AE - sample 470: - time = 15633333 - flags = 0 - data = length 68, hash 3811E1 - sample 471: - time = 15800000 - flags = 0 - data = length 77, hash B8F74A5C - sample 472: - time = 15733333 - flags = 0 - data = length 70, hash F383DB6D - sample 473: - time = 15700000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 474: - time = 15766666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 475: - time = 15933333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 476: - time = 15866666 - flags = 0 - data = length 70, hash B92866C9 - sample 477: - time = 15833333 - flags = 0 - data = length 68, hash 66214C05 - sample 478: - time = 15900000 - flags = 0 - data = length 68, hash 4605E638 - sample 479: - time = 16066666 - flags = 0 - data = length 77, hash E5E3D004 - sample 480: - time = 16000000 - flags = 0 - data = length 70, hash E9E98F05 - sample 481: - time = 15966666 - flags = 0 - data = length 68, hash 88475340 - sample 482: - time = 16033333 - flags = 0 - data = length 68, hash 682BED75 - sample 483: - time = 16200000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 484: - time = 16133333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 485: - time = 16100000 - flags = 0 - data = length 68, hash E8DCF97D - sample 486: - time = 16166666 - flags = 0 - data = length 68, hash C8C193B1 - sample 487: - time = 16333333 - flags = 0 - data = length 77, hash 6967C98D - sample 488: - time = 16266666 - flags = 0 - data = length 70, hash 5B33157D - sample 489: - time = 16233333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 490: - time = 16300000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 491: - time = 16466666 - flags = 0 - data = length 77, hash 434E1BAF - sample 492: - time = 16400000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 493: - time = 16366666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 494: - time = 16433333 - flags = 0 - data = length 68, hash 7D7C646A - sample 495: - time = 16600000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 496: - time = 16533333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 497: - time = 16500000 - flags = 0 - data = length 68, hash BFBDD171 - sample 498: - time = 16566666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 499: - time = 16633333 - flags = 0 - data = length 77, hash AB0BE9E8 - sample 500: - time = 16666666 - flags = 1 - data = length 423, hash DDB543E8 - sample 501: - time = 16800000 - flags = 0 - data = length 71, hash E6801694 - sample 502: - time = 16733333 - flags = 0 - data = length 68, hash 83DC1BAA - sample 503: - time = 16700000 - flags = 0 - data = length 68, hash 66214C05 - sample 504: - time = 16766666 - flags = 0 - data = length 68, hash 4605E639 - sample 505: - time = 16933333 - flags = 0 - data = length 77, hash 76DDE100 - sample 506: - time = 16866666 - flags = 0 - data = length 70, hash 98EFD089 - sample 507: - time = 16833333 - flags = 0 - data = length 68, hash 88475340 - sample 508: - time = 16900000 - flags = 0 - data = length 68, hash 682BED75 - sample 509: - time = 17066666 - flags = 0 - data = length 77, hash 11CC2C49 - sample 510: - time = 17000000 - flags = 0 - data = length 70, hash 7BAE9640 - sample 511: - time = 16966666 - flags = 0 - data = length 68, hash E8DCF97C - sample 512: - time = 17033333 - flags = 0 - data = length 68, hash C8C193B1 - sample 513: - time = 17200000 - flags = 0 - data = length 77, hash 6967C98C - sample 514: - time = 17133333 - flags = 0 - data = length 70, hash 5B33157E - sample 515: - time = 17100000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 516: - time = 17166666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 517: - time = 17333333 - flags = 0 - data = length 77, hash 434E1BB0 - sample 518: - time = 17266666 - flags = 0 - data = length 70, hash ECF81CBA - sample 519: - time = 17233333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 520: - time = 17300000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 521: - time = 17466666 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 522: - time = 17400000 - flags = 0 - data = length 70, hash 1DB944F6 - sample 523: - time = 17366666 - flags = 0 - data = length 68, hash BFBDD171 - sample 524: - time = 17433333 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 525: - time = 17600000 - flags = 0 - data = length 77, hash C6D21538 - sample 526: - time = 17533333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 527: - time = 17500000 - flags = 0 - data = length 68, hash 205377AE - sample 528: - time = 17566666 - flags = 0 - data = length 68, hash 3811E1 - sample 529: - time = 17733333 - flags = 0 - data = length 77, hash B8F74A5B - sample 530: - time = 17666666 - flags = 0 - data = length 70, hash F383DB6D - sample 531: - time = 17633333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 532: - time = 17700000 - flags = 0 - data = length 68, hash 6D3E33DC - sample 533: - time = 17866666 - flags = 0 - data = length 77, hash 8E4832C1 - sample 534: - time = 17800000 - flags = 0 - data = length 70, hash B92866C8 - sample 535: - time = 17766666 - flags = 0 - data = length 68, hash 66214C05 - sample 536: - time = 17833333 - flags = 0 - data = length 68, hash 4605E639 - sample 537: - time = 18000000 - flags = 0 - data = length 77, hash E5E3D004 - sample 538: - time = 17933333 - flags = 0 - data = length 70, hash E9E98F05 - sample 539: - time = 17900000 - flags = 0 - data = length 68, hash 88475340 - sample 540: - time = 17966666 - flags = 0 - data = length 68, hash 682BED75 - sample 541: - time = 18133333 - flags = 0 - data = length 77, hash 11CC2C49 - sample 542: - time = 18066666 - flags = 0 - data = length 70, hash 7BAE9640 - sample 543: - time = 18033333 - flags = 0 - data = length 68, hash E8DCF97C - sample 544: - time = 18100000 - flags = 0 - data = length 68, hash C8C193B1 - sample 545: - time = 18266666 - flags = 0 - data = length 77, hash 6967C98C - sample 546: - time = 18200000 - flags = 0 - data = length 70, hash 5B33157E - sample 547: - time = 18166666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 548: - time = 18233333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 549: - time = 18400000 - flags = 0 - data = length 77, hash 434E1BB0 - sample 550: - time = 18333333 - flags = 0 - data = length 70, hash ECF81CBA - sample 551: - time = 18300000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 552: - time = 18366666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 553: - time = 18533333 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 554: - time = 18466666 - flags = 0 - data = length 70, hash 1DB944F6 - sample 555: - time = 18433333 - flags = 0 - data = length 68, hash BFBDD171 - sample 556: - time = 18500000 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 557: - time = 18666666 - flags = 0 - data = length 77, hash C6D21538 - sample 558: - time = 18600000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 559: - time = 18566666 - flags = 0 - data = length 68, hash 205377AE - sample 560: - time = 18633333 - flags = 0 - data = length 68, hash 3811E1 - sample 561: - time = 18800000 - flags = 0 - data = length 77, hash B8F74A5B - sample 562: - time = 18733333 - flags = 0 - data = length 70, hash F383DB6D - sample 563: - time = 18700000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 564: - time = 18766666 - flags = 0 - data = length 68, hash 6D3E33DC - sample 565: - time = 18933333 - flags = 0 - data = length 77, hash 8E4832C1 - sample 566: - time = 18866666 - flags = 0 - data = length 70, hash B92866C8 - sample 567: - time = 18833333 - flags = 0 - data = length 68, hash 66214C05 - sample 568: - time = 18900000 - flags = 0 - data = length 68, hash 4605E639 - sample 569: - time = 19066666 - flags = 0 - data = length 77, hash E5E3D004 - sample 570: - time = 19000000 - flags = 0 - data = length 70, hash E9E98F05 - sample 571: - time = 18966666 - flags = 0 - data = length 68, hash 88475340 - sample 572: - time = 19033333 - flags = 0 - data = length 68, hash 682BED75 - sample 573: - time = 19200000 - flags = 0 - data = length 77, hash 11CC2C49 - sample 574: - time = 19133333 - flags = 0 - data = length 70, hash 7BAE9640 - sample 575: - time = 19100000 - flags = 0 - data = length 68, hash E8DCF97C - sample 576: - time = 19166666 - flags = 0 - data = length 68, hash C8C193B1 - sample 577: - time = 19333333 - flags = 0 - data = length 77, hash 6967C98C - sample 578: - time = 19266666 - flags = 0 - data = length 70, hash 5B33157E - sample 579: - time = 19233333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 580: - time = 19300000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 581: - time = 19466666 - flags = 0 - data = length 77, hash 434E1BB0 - sample 582: - time = 19400000 - flags = 0 - data = length 70, hash ECF81CBA - sample 583: - time = 19366666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 584: - time = 19433333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 585: - time = 19600000 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 586: - time = 19533333 - flags = 0 - data = length 70, hash 1DB944F6 - sample 587: - time = 19500000 - flags = 0 - data = length 68, hash BFBDD171 - sample 588: - time = 19566666 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 589: - time = 19733333 - flags = 0 - data = length 77, hash 179FCFE9 - sample 590: - time = 19666666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 591: - time = 19633333 - flags = 0 - data = length 68, hash 205377AE - sample 592: - time = 19700000 - flags = 0 - data = length 68, hash 3811E1 - sample 593: - time = 19866666 - flags = 0 - data = length 77, hash BCD7CD76 - sample 594: - time = 19800000 - flags = 0 - data = length 70, hash F383DB6D - sample 595: - time = 19766666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 596: - time = 19833333 - flags = 0 - data = length 68, hash 6D3E33DC - sample 597: - time = 19966666 - flags = 0 - data = length 77, hash 38D96DF4 - sample 598: - time = 19900000 - flags = 0 - data = length 70, hash 3AC762E9 - sample 599: - time = 19933333 - flags = 0 - data = length 68, hash B1C7769A -tracksEnded = true 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 deleted file mode 100644 index cf0b315ef69..00000000000 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.no-merge-fragmented-sidx.unknown_length.dump +++ /dev/null @@ -1,2430 +0,0 @@ -seekMap: - isSeekable = true - duration = 8333333 - getPosition(0) = [[timeUs=0, position=871]] - getPosition(1) = [[timeUs=0, position=871]] - getPosition(4166666) = [[timeUs=0, position=871]] - getPosition(8333333) = [[timeUs=0, position=871]] -numberOfTracks = 1 -track 0: - total output bytes = 44195 - sample count = 600 - format 0: - averageBitrate = 17678 - peakBitrate = 17678 - id = 1 - containerMimeType = video/mp4 - sampleMimeType = video/avc - codecs = avc1.640028 - maxNumReorderSamples = 2 - width = 1920 - height = 1080 - colorInfo: - 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 - sample 0: - time = 0 - flags = 1 - data = length 1120, hash 352E33B0 - sample 1: - time = 133333 - flags = 0 - data = length 71, hash E6801693 - sample 2: - time = 66666 - flags = 0 - data = length 68, hash 83DC1BAB - sample 3: - time = 33333 - flags = 0 - data = length 68, hash 66214C04 - sample 4: - time = 100000 - flags = 0 - data = length 68, hash 4605E639 - sample 5: - time = 266666 - flags = 0 - data = length 77, hash 76DDE101 - sample 6: - time = 200000 - flags = 0 - data = length 70, hash 98EFD089 - sample 7: - time = 166666 - flags = 0 - data = length 68, hash 88475341 - sample 8: - time = 233333 - flags = 0 - data = length 68, hash 682BED74 - sample 9: - time = 400000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 10: - time = 333333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 11: - time = 300000 - flags = 0 - data = length 68, hash E8DCF97C - sample 12: - time = 366666 - flags = 0 - data = length 68, hash C8C193B0 - sample 13: - time = 533333 - flags = 0 - data = length 77, hash 6967C98D - sample 14: - time = 466666 - flags = 0 - data = length 70, hash 5B33157E - sample 15: - time = 433333 - flags = 0 - data = length 68, hash 3D0223FA - sample 16: - time = 500000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 17: - time = 666666 - flags = 0 - data = length 77, hash 434E1BAF - sample 18: - time = 600000 - flags = 0 - data = length 70, hash ECF81CBA - sample 19: - time = 566666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 20: - time = 633333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 21: - time = 800000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 22: - time = 733333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 23: - time = 700000 - flags = 0 - data = length 68, hash BFBDD172 - sample 24: - time = 766666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 25: - time = 933333 - flags = 0 - data = length 77, hash C6D21537 - sample 26: - time = 866666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 27: - time = 833333 - flags = 0 - data = length 68, hash 205377AD - sample 28: - time = 900000 - flags = 0 - data = length 68, hash 3811E2 - sample 29: - time = 1066666 - flags = 0 - data = length 77, hash B8F74A5C - sample 30: - time = 1000000 - flags = 0 - data = length 70, hash F383DB6C - sample 31: - time = 966666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 32: - time = 1033333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 33: - time = 1200000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 34: - time = 1133333 - flags = 0 - data = length 70, hash B92866C9 - sample 35: - time = 1100000 - flags = 0 - data = length 68, hash 66214C04 - sample 36: - time = 1166666 - flags = 0 - data = length 68, hash 4605E639 - sample 37: - time = 1333333 - flags = 0 - data = length 77, hash E5E3D005 - sample 38: - time = 1266666 - flags = 0 - data = length 70, hash E9E98F05 - sample 39: - time = 1233333 - flags = 0 - data = length 68, hash 88475341 - sample 40: - time = 1300000 - flags = 0 - data = length 68, hash 682BED74 - sample 41: - time = 1466666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 42: - time = 1400000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 43: - time = 1366666 - flags = 0 - data = length 68, hash E8DCF97C - sample 44: - time = 1433333 - flags = 0 - data = length 68, hash C8C193B0 - sample 45: - time = 1600000 - flags = 0 - data = length 77, hash 6967C98D - sample 46: - time = 1533333 - flags = 0 - data = length 70, hash 5B33157E - sample 47: - time = 1500000 - flags = 0 - data = length 68, hash 3D0223FA - sample 48: - time = 1566666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 49: - time = 1733333 - flags = 0 - data = length 77, hash 434E1BAF - sample 50: - time = 1666666 - flags = 0 - data = length 70, hash ECF81CBA - sample 51: - time = 1633333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 52: - time = 1700000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 53: - time = 1866666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 54: - time = 1800000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 55: - time = 1766666 - flags = 0 - data = length 68, hash BFBDD172 - sample 56: - time = 1833333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 57: - time = 2000000 - flags = 0 - data = length 77, hash C6D21537 - sample 58: - time = 1933333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 59: - time = 1900000 - flags = 0 - data = length 68, hash 205377AD - sample 60: - time = 1966666 - flags = 0 - data = length 68, hash 3811E2 - sample 61: - time = 2133333 - flags = 0 - data = length 77, hash B8F74A5C - sample 62: - time = 2066666 - flags = 0 - data = length 70, hash F383DB6C - sample 63: - time = 2033333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 64: - time = 2100000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 65: - time = 2266666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 66: - time = 2200000 - flags = 0 - data = length 70, hash B92866C9 - sample 67: - time = 2166666 - flags = 0 - data = length 68, hash 66214C04 - sample 68: - time = 2233333 - flags = 0 - data = length 68, hash 4605E639 - sample 69: - time = 2400000 - flags = 0 - data = length 77, hash E5E3D005 - sample 70: - time = 2333333 - flags = 0 - data = length 70, hash E9E98F05 - sample 71: - time = 2300000 - flags = 0 - data = length 68, hash 88475341 - sample 72: - time = 2366666 - flags = 0 - data = length 68, hash 682BED74 - sample 73: - time = 2533333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 74: - time = 2466666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 75: - time = 2433333 - flags = 0 - data = length 68, hash E8DCF97C - sample 76: - time = 2500000 - flags = 0 - data = length 68, hash C8C193B0 - sample 77: - time = 2666666 - flags = 0 - data = length 77, hash 6967C98D - sample 78: - time = 2600000 - flags = 0 - data = length 70, hash 5B33157E - sample 79: - time = 2566666 - flags = 0 - data = length 68, hash 3D0223FA - sample 80: - time = 2633333 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 81: - time = 2800000 - flags = 0 - data = length 77, hash 434E1BAF - sample 82: - time = 2733333 - flags = 0 - data = length 70, hash ECF81CBA - sample 83: - time = 2700000 - flags = 0 - data = length 68, hash 9D97CA35 - sample 84: - time = 2766666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 85: - time = 2933333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 86: - time = 2866666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 87: - time = 2833333 - flags = 0 - data = length 68, hash BFBDD172 - sample 88: - time = 2900000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 89: - time = 3066666 - flags = 0 - data = length 77, hash C6D21537 - sample 90: - time = 3000000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 91: - time = 2966666 - flags = 0 - data = length 68, hash 205377AD - sample 92: - time = 3033333 - flags = 0 - data = length 68, hash 3811E2 - sample 93: - time = 3200000 - flags = 0 - data = length 77, hash B8F74A5C - sample 94: - time = 3133333 - flags = 0 - data = length 70, hash F383DB6C - sample 95: - time = 3100000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 96: - time = 3166666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 97: - time = 3333333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 98: - time = 3266666 - flags = 0 - data = length 70, hash B92866C9 - sample 99: - time = 3233333 - flags = 0 - data = length 68, hash 66214C04 - sample 100: - time = 3300000 - flags = 0 - data = length 68, hash 4605E639 - sample 101: - time = 3466666 - flags = 0 - data = length 77, hash E5E3D005 - sample 102: - time = 3400000 - flags = 0 - data = length 70, hash E9E98F05 - sample 103: - time = 3366666 - flags = 0 - data = length 68, hash 88475341 - sample 104: - time = 3433333 - flags = 0 - data = length 68, hash 682BED74 - sample 105: - time = 3600000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 106: - time = 3533333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 107: - time = 3500000 - flags = 0 - data = length 68, hash E8DCF97C - sample 108: - time = 3566666 - flags = 0 - data = length 68, hash C8C193B0 - sample 109: - time = 3733333 - flags = 0 - data = length 77, hash 6967C98D - sample 110: - time = 3666666 - flags = 0 - data = length 70, hash 5B33157E - sample 111: - time = 3633333 - flags = 0 - data = length 68, hash 3D0223FA - sample 112: - time = 3700000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 113: - time = 3866666 - flags = 0 - data = length 77, hash 434E1BAF - sample 114: - time = 3800000 - flags = 0 - data = length 70, hash ECF81CBA - sample 115: - time = 3766666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 116: - time = 3833333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 117: - time = 4000000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 118: - time = 3933333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 119: - time = 3900000 - flags = 0 - data = length 68, hash BFBDD172 - sample 120: - time = 3966666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 121: - time = 4133333 - flags = 0 - data = length 77, hash C6D21537 - sample 122: - time = 4066666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 123: - time = 4033333 - flags = 0 - data = length 68, hash 205377AD - sample 124: - time = 4100000 - flags = 0 - data = length 68, hash 3811E2 - sample 125: - time = 4266666 - flags = 0 - data = length 77, hash B8F74A5C - sample 126: - time = 4200000 - flags = 0 - data = length 70, hash F383DB6C - sample 127: - time = 4166666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 128: - time = 4233333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 129: - time = 4400000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 130: - time = 4333333 - flags = 0 - data = length 70, hash B92866C9 - sample 131: - time = 4300000 - flags = 0 - data = length 68, hash 66214C04 - sample 132: - time = 4366666 - flags = 0 - data = length 68, hash 4605E639 - sample 133: - time = 4533333 - flags = 0 - data = length 77, hash E5E3D005 - sample 134: - time = 4466666 - flags = 0 - data = length 70, hash E9E98F05 - sample 135: - time = 4433333 - flags = 0 - data = length 68, hash 88475341 - sample 136: - time = 4500000 - flags = 0 - data = length 68, hash 682BED74 - sample 137: - time = 4666666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 138: - time = 4600000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 139: - time = 4566666 - flags = 0 - data = length 68, hash E8DCF97C - sample 140: - time = 4633333 - flags = 0 - data = length 68, hash C8C193B0 - sample 141: - time = 4800000 - flags = 0 - data = length 77, hash 6967C98D - sample 142: - time = 4733333 - flags = 0 - data = length 70, hash 5B33157E - sample 143: - time = 4700000 - flags = 0 - data = length 68, hash 3D0223FA - sample 144: - time = 4766666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 145: - time = 4933333 - flags = 0 - data = length 77, hash 434E1BAF - sample 146: - time = 4866666 - flags = 0 - data = length 70, hash ECF81CBA - sample 147: - time = 4833333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 148: - time = 4900000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 149: - time = 5066666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 150: - time = 5000000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 151: - time = 4966666 - flags = 0 - data = length 68, hash BFBDD172 - sample 152: - time = 5033333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 153: - time = 5200000 - flags = 0 - data = length 77, hash C6D21537 - sample 154: - time = 5133333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 155: - time = 5100000 - flags = 0 - data = length 68, hash 205377AD - sample 156: - time = 5166666 - flags = 0 - data = length 68, hash 3811E2 - sample 157: - time = 5333333 - flags = 0 - data = length 77, hash B8F74A5C - sample 158: - time = 5266666 - flags = 0 - data = length 70, hash F383DB6C - sample 159: - time = 5233333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 160: - time = 5300000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 161: - time = 5466666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 162: - time = 5400000 - flags = 0 - data = length 70, hash B92866C9 - sample 163: - time = 5366666 - flags = 0 - data = length 68, hash 66214C04 - sample 164: - time = 5433333 - flags = 0 - data = length 68, hash 4605E639 - sample 165: - time = 5600000 - flags = 0 - data = length 77, hash E5E3D005 - sample 166: - time = 5533333 - flags = 0 - data = length 70, hash E9E98F05 - sample 167: - time = 5500000 - flags = 0 - data = length 68, hash 88475341 - sample 168: - time = 5566666 - flags = 0 - data = length 68, hash 682BED74 - sample 169: - time = 5733333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 170: - time = 5666666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 171: - time = 5633333 - flags = 0 - data = length 68, hash E8DCF97C - sample 172: - time = 5700000 - flags = 0 - data = length 68, hash C8C193B0 - sample 173: - time = 5866666 - flags = 0 - data = length 77, hash 6967C98D - sample 174: - time = 5800000 - flags = 0 - data = length 70, hash 5B33157E - sample 175: - time = 5766666 - flags = 0 - data = length 68, hash 3D0223FA - sample 176: - time = 5833333 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 177: - time = 6000000 - flags = 0 - data = length 77, hash 434E1BAF - sample 178: - time = 5933333 - flags = 0 - data = length 70, hash ECF81CBA - sample 179: - time = 5900000 - flags = 0 - data = length 68, hash 9D97CA35 - sample 180: - time = 5966666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 181: - time = 6133333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 182: - time = 6066666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 183: - time = 6033333 - flags = 0 - data = length 68, hash BFBDD172 - sample 184: - time = 6100000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 185: - time = 6266666 - flags = 0 - data = length 77, hash C6D21537 - sample 186: - time = 6200000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 187: - time = 6166666 - flags = 0 - data = length 68, hash 205377AD - sample 188: - time = 6233333 - flags = 0 - data = length 68, hash 3811E2 - sample 189: - time = 6400000 - flags = 0 - data = length 77, hash B8F74A5C - sample 190: - time = 6333333 - flags = 0 - data = length 70, hash F383DB6C - sample 191: - time = 6300000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 192: - time = 6366666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 193: - time = 6533333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 194: - time = 6466666 - flags = 0 - data = length 70, hash B92866C9 - sample 195: - time = 6433333 - flags = 0 - data = length 68, hash 66214C04 - sample 196: - time = 6500000 - flags = 0 - data = length 68, hash 4605E639 - sample 197: - time = 6666666 - flags = 0 - data = length 77, hash E5E3D005 - sample 198: - time = 6600000 - flags = 0 - data = length 70, hash E9E98F05 - sample 199: - time = 6566666 - flags = 0 - data = length 68, hash 88475341 - sample 200: - time = 6633333 - flags = 0 - data = length 68, hash 682BED74 - sample 201: - time = 6800000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 202: - time = 6733333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 203: - time = 6700000 - flags = 0 - data = length 68, hash E8DCF97C - sample 204: - time = 6766666 - flags = 0 - data = length 68, hash C8C193B0 - sample 205: - time = 6933333 - flags = 0 - data = length 77, hash 6967C98D - sample 206: - time = 6866666 - flags = 0 - data = length 70, hash 5B33157E - sample 207: - time = 6833333 - flags = 0 - data = length 68, hash 3D0223FA - sample 208: - time = 6900000 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 209: - time = 7066666 - flags = 0 - data = length 77, hash 434E1BAF - sample 210: - time = 7000000 - flags = 0 - data = length 70, hash ECF81CBA - sample 211: - time = 6966666 - flags = 0 - data = length 68, hash 9D97CA35 - sample 212: - time = 7033333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 213: - time = 7200000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 214: - time = 7133333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 215: - time = 7100000 - flags = 0 - data = length 68, hash BFBDD172 - sample 216: - time = 7166666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 217: - time = 7333333 - flags = 0 - data = length 77, hash C6D21537 - sample 218: - time = 7266666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 219: - time = 7233333 - flags = 0 - data = length 68, hash 205377AD - sample 220: - time = 7300000 - flags = 0 - data = length 68, hash 3811E2 - sample 221: - time = 7466666 - flags = 0 - data = length 77, hash B8F74A5C - sample 222: - time = 7400000 - flags = 0 - data = length 70, hash F383DB6C - sample 223: - time = 7366666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 224: - time = 7433333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 225: - time = 7600000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 226: - time = 7533333 - flags = 0 - data = length 70, hash B92866C9 - sample 227: - time = 7500000 - flags = 0 - data = length 68, hash 66214C04 - sample 228: - time = 7566666 - flags = 0 - data = length 68, hash 4605E639 - sample 229: - time = 7733333 - flags = 0 - data = length 77, hash E5E3D005 - sample 230: - time = 7666666 - flags = 0 - data = length 70, hash E9E98F05 - sample 231: - time = 7633333 - flags = 0 - data = length 68, hash 88475341 - sample 232: - time = 7700000 - flags = 0 - data = length 68, hash 682BED74 - sample 233: - time = 7866666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 234: - time = 7800000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 235: - time = 7766666 - flags = 0 - data = length 68, hash E8DCF97C - sample 236: - time = 7833333 - flags = 0 - data = length 68, hash C8C193B0 - sample 237: - time = 8000000 - flags = 0 - data = length 77, hash 6967C98D - sample 238: - time = 7933333 - flags = 0 - data = length 70, hash 5B33157E - sample 239: - time = 7900000 - flags = 0 - data = length 68, hash 3D0223FA - sample 240: - time = 7966666 - flags = 0 - data = length 68, hash 1CE6BE2D - sample 241: - time = 8133333 - flags = 0 - data = length 77, hash 434E1BAF - sample 242: - time = 8066666 - flags = 0 - data = length 70, hash ECF81CBA - sample 243: - time = 8033333 - flags = 0 - data = length 68, hash 9D97CA35 - sample 244: - time = 8100000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 245: - time = 8266666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 246: - time = 8200000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 247: - time = 8166666 - flags = 0 - data = length 68, hash BFBDD172 - sample 248: - time = 8233333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 249: - time = 8300000 - flags = 0 - data = length 77, hash AB0BE9E8 - sample 250: - time = 8333333 - flags = 1 - data = length 423, hash 724FF09B - sample 251: - time = 8466666 - flags = 0 - data = length 71, hash E6801693 - sample 252: - time = 8400000 - flags = 0 - data = length 68, hash 83DC1BAB - sample 253: - time = 8366666 - flags = 0 - data = length 68, hash 66214C05 - sample 254: - time = 8433333 - flags = 0 - data = length 68, hash 4605E638 - sample 255: - time = 8600000 - flags = 0 - data = length 77, hash 76DDE100 - sample 256: - time = 8533333 - flags = 0 - data = length 70, hash 98EFD089 - sample 257: - time = 8500000 - flags = 0 - data = length 68, hash 88475340 - sample 258: - time = 8566666 - flags = 0 - data = length 68, hash 682BED75 - sample 259: - time = 8733333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 260: - time = 8666666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 261: - time = 8633333 - flags = 0 - data = length 68, hash E8DCF97D - sample 262: - time = 8700000 - flags = 0 - data = length 68, hash C8C193B1 - sample 263: - time = 8866666 - flags = 0 - data = length 77, hash 6967C98D - sample 264: - time = 8800000 - flags = 0 - data = length 70, hash 5B33157D - sample 265: - time = 8766666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 266: - time = 8833333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 267: - time = 9000000 - flags = 0 - data = length 77, hash 434E1BAF - sample 268: - time = 8933333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 269: - time = 8900000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 270: - time = 8966666 - flags = 0 - data = length 68, hash 7D7C646A - sample 271: - time = 9133333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 272: - time = 9066666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 273: - time = 9033333 - flags = 0 - data = length 68, hash BFBDD171 - sample 274: - time = 9100000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 275: - time = 9266666 - flags = 0 - data = length 77, hash C6D21537 - sample 276: - time = 9200000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 277: - time = 9166666 - flags = 0 - data = length 68, hash 205377AE - sample 278: - time = 9233333 - flags = 0 - data = length 68, hash 3811E1 - sample 279: - time = 9400000 - flags = 0 - data = length 77, hash B8F74A5C - sample 280: - time = 9333333 - flags = 0 - data = length 70, hash F383DB6D - sample 281: - time = 9300000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 282: - time = 9366666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 283: - time = 9533333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 284: - time = 9466666 - flags = 0 - data = length 70, hash B92866C9 - sample 285: - time = 9433333 - flags = 0 - data = length 68, hash 66214C05 - sample 286: - time = 9500000 - flags = 0 - data = length 68, hash 4605E638 - sample 287: - time = 9666666 - flags = 0 - data = length 77, hash E5E3D004 - sample 288: - time = 9600000 - flags = 0 - data = length 70, hash E9E98F05 - sample 289: - time = 9566666 - flags = 0 - data = length 68, hash 88475340 - sample 290: - time = 9633333 - flags = 0 - data = length 68, hash 682BED75 - sample 291: - time = 9800000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 292: - time = 9733333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 293: - time = 9700000 - flags = 0 - data = length 68, hash E8DCF97D - sample 294: - time = 9766666 - flags = 0 - data = length 68, hash C8C193B1 - sample 295: - time = 9933333 - flags = 0 - data = length 77, hash 6967C98D - sample 296: - time = 9866666 - flags = 0 - data = length 70, hash 5B33157D - sample 297: - time = 9833333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 298: - time = 9900000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 299: - time = 10066666 - flags = 0 - data = length 77, hash 434E1BAF - sample 300: - time = 10000000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 301: - time = 9966666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 302: - time = 10033333 - flags = 0 - data = length 68, hash 7D7C646A - sample 303: - time = 10200000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 304: - time = 10133333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 305: - time = 10100000 - flags = 0 - data = length 68, hash BFBDD171 - sample 306: - time = 10166666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 307: - time = 10333333 - flags = 0 - data = length 77, hash C6D21537 - sample 308: - time = 10266666 - flags = 0 - data = length 70, hash AF7E4C31 - sample 309: - time = 10233333 - flags = 0 - data = length 68, hash 205377AE - sample 310: - time = 10300000 - flags = 0 - data = length 68, hash 3811E1 - sample 311: - time = 10466666 - flags = 0 - data = length 77, hash B8F74A5C - sample 312: - time = 10400000 - flags = 0 - data = length 70, hash F383DB6D - sample 313: - time = 10366666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 314: - time = 10433333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 315: - time = 10600000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 316: - time = 10533333 - flags = 0 - data = length 70, hash B92866C9 - sample 317: - time = 10500000 - flags = 0 - data = length 68, hash 66214C05 - sample 318: - time = 10566666 - flags = 0 - data = length 68, hash 4605E638 - sample 319: - time = 10733333 - flags = 0 - data = length 77, hash E5E3D004 - sample 320: - time = 10666666 - flags = 0 - data = length 70, hash E9E98F05 - sample 321: - time = 10633333 - flags = 0 - data = length 68, hash 88475340 - sample 322: - time = 10700000 - flags = 0 - data = length 68, hash 682BED75 - sample 323: - time = 10866666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 324: - time = 10800000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 325: - time = 10766666 - flags = 0 - data = length 68, hash E8DCF97D - sample 326: - time = 10833333 - flags = 0 - data = length 68, hash C8C193B1 - sample 327: - time = 11000000 - flags = 0 - data = length 77, hash 6967C98D - sample 328: - time = 10933333 - flags = 0 - data = length 70, hash 5B33157D - sample 329: - time = 10900000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 330: - time = 10966666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 331: - time = 11133333 - flags = 0 - data = length 77, hash 434E1BAF - sample 332: - time = 11066666 - flags = 0 - data = length 70, hash ECF81CB9 - sample 333: - time = 11033333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 334: - time = 11100000 - flags = 0 - data = length 68, hash 7D7C646A - sample 335: - time = 11266666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 336: - time = 11200000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 337: - time = 11166666 - flags = 0 - data = length 68, hash BFBDD171 - sample 338: - time = 11233333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 339: - time = 11400000 - flags = 0 - data = length 77, hash C6D21537 - sample 340: - time = 11333333 - flags = 0 - data = length 70, hash AF7E4C31 - sample 341: - time = 11300000 - flags = 0 - data = length 68, hash 205377AE - sample 342: - time = 11366666 - flags = 0 - data = length 68, hash 3811E1 - sample 343: - time = 11533333 - flags = 0 - data = length 77, hash B8F74A5C - sample 344: - time = 11466666 - flags = 0 - data = length 70, hash F383DB6D - sample 345: - time = 11433333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 346: - time = 11500000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 347: - time = 11666666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 348: - time = 11600000 - flags = 0 - data = length 70, hash B92866C9 - sample 349: - time = 11566666 - flags = 0 - data = length 68, hash 66214C05 - sample 350: - time = 11633333 - flags = 0 - data = length 68, hash 4605E638 - sample 351: - time = 11800000 - flags = 0 - data = length 77, hash E5E3D004 - sample 352: - time = 11733333 - flags = 0 - data = length 70, hash E9E98F05 - sample 353: - time = 11700000 - flags = 0 - data = length 68, hash 88475340 - sample 354: - time = 11766666 - flags = 0 - data = length 68, hash 682BED75 - sample 355: - time = 11933333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 356: - time = 11866666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 357: - time = 11833333 - flags = 0 - data = length 68, hash E8DCF97D - sample 358: - time = 11900000 - flags = 0 - data = length 68, hash C8C193B1 - sample 359: - time = 12066666 - flags = 0 - data = length 77, hash 6967C98D - sample 360: - time = 12000000 - flags = 0 - data = length 70, hash 5B33157D - sample 361: - time = 11966666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 362: - time = 12033333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 363: - time = 12200000 - flags = 0 - data = length 77, hash 434E1BAF - sample 364: - time = 12133333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 365: - time = 12100000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 366: - time = 12166666 - flags = 0 - data = length 68, hash 7D7C646A - sample 367: - time = 12333333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 368: - time = 12266666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 369: - time = 12233333 - flags = 0 - data = length 68, hash BFBDD171 - sample 370: - time = 12300000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 371: - time = 12466666 - flags = 0 - data = length 77, hash C6D21537 - sample 372: - time = 12400000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 373: - time = 12366666 - flags = 0 - data = length 68, hash 205377AE - sample 374: - time = 12433333 - flags = 0 - data = length 68, hash 3811E1 - sample 375: - time = 12600000 - flags = 0 - data = length 77, hash B8F74A5C - sample 376: - time = 12533333 - flags = 0 - data = length 70, hash F383DB6D - sample 377: - time = 12500000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 378: - time = 12566666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 379: - time = 12733333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 380: - time = 12666666 - flags = 0 - data = length 70, hash B92866C9 - sample 381: - time = 12633333 - flags = 0 - data = length 68, hash 66214C05 - sample 382: - time = 12700000 - flags = 0 - data = length 68, hash 4605E638 - sample 383: - time = 12866666 - flags = 0 - data = length 77, hash E5E3D004 - sample 384: - time = 12800000 - flags = 0 - data = length 70, hash E9E98F05 - sample 385: - time = 12766666 - flags = 0 - data = length 68, hash 88475340 - sample 386: - time = 12833333 - flags = 0 - data = length 68, hash 682BED75 - sample 387: - time = 13000000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 388: - time = 12933333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 389: - time = 12900000 - flags = 0 - data = length 68, hash E8DCF97D - sample 390: - time = 12966666 - flags = 0 - data = length 68, hash C8C193B1 - sample 391: - time = 13133333 - flags = 0 - data = length 77, hash 6967C98D - sample 392: - time = 13066666 - flags = 0 - data = length 70, hash 5B33157D - sample 393: - time = 13033333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 394: - time = 13100000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 395: - time = 13266666 - flags = 0 - data = length 77, hash 434E1BAF - sample 396: - time = 13200000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 397: - time = 13166666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 398: - time = 13233333 - flags = 0 - data = length 68, hash 7D7C646A - sample 399: - time = 13400000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 400: - time = 13333333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 401: - time = 13300000 - flags = 0 - data = length 68, hash BFBDD171 - sample 402: - time = 13366666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 403: - time = 13533333 - flags = 0 - data = length 77, hash C6D21537 - sample 404: - time = 13466666 - flags = 0 - data = length 70, hash AF7E4C31 - sample 405: - time = 13433333 - flags = 0 - data = length 68, hash 205377AE - sample 406: - time = 13500000 - flags = 0 - data = length 68, hash 3811E1 - sample 407: - time = 13666666 - flags = 0 - data = length 77, hash B8F74A5C - sample 408: - time = 13600000 - flags = 0 - data = length 70, hash F383DB6D - sample 409: - time = 13566666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 410: - time = 13633333 - flags = 0 - data = length 68, hash 6D3E33DD - sample 411: - time = 13800000 - flags = 0 - data = length 77, hash 8E4832C0 - sample 412: - time = 13733333 - flags = 0 - data = length 70, hash B92866C9 - sample 413: - time = 13700000 - flags = 0 - data = length 68, hash 66214C05 - sample 414: - time = 13766666 - flags = 0 - data = length 68, hash 4605E638 - sample 415: - time = 13933333 - flags = 0 - data = length 77, hash E5E3D004 - sample 416: - time = 13866666 - flags = 0 - data = length 70, hash E9E98F05 - sample 417: - time = 13833333 - flags = 0 - data = length 68, hash 88475340 - sample 418: - time = 13900000 - flags = 0 - data = length 68, hash 682BED75 - sample 419: - time = 14066666 - flags = 0 - data = length 77, hash 11CC2C48 - sample 420: - time = 14000000 - flags = 0 - data = length 70, hash 7BAE9641 - sample 421: - time = 13966666 - flags = 0 - data = length 68, hash E8DCF97D - sample 422: - time = 14033333 - flags = 0 - data = length 68, hash C8C193B1 - sample 423: - time = 14200000 - flags = 0 - data = length 77, hash 6967C98D - sample 424: - time = 14133333 - flags = 0 - data = length 70, hash 5B33157D - sample 425: - time = 14100000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 426: - time = 14166666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 427: - time = 14333333 - flags = 0 - data = length 77, hash 434E1BAF - sample 428: - time = 14266666 - flags = 0 - data = length 70, hash ECF81CB9 - sample 429: - time = 14233333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 430: - time = 14300000 - flags = 0 - data = length 68, hash 7D7C646A - sample 431: - time = 14466666 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 432: - time = 14400000 - flags = 0 - data = length 70, hash 1DB944F5 - sample 433: - time = 14366666 - flags = 0 - data = length 68, hash BFBDD171 - sample 434: - time = 14433333 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 435: - time = 14600000 - flags = 0 - data = length 77, hash C6D21537 - sample 436: - time = 14533333 - flags = 0 - data = length 70, hash AF7E4C31 - sample 437: - time = 14500000 - flags = 0 - data = length 68, hash 205377AE - sample 438: - time = 14566666 - flags = 0 - data = length 68, hash 3811E1 - sample 439: - time = 14733333 - flags = 0 - data = length 77, hash B8F74A5C - sample 440: - time = 14666666 - flags = 0 - data = length 70, hash F383DB6D - sample 441: - time = 14633333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 442: - time = 14700000 - flags = 0 - data = length 68, hash 6D3E33DD - sample 443: - time = 14866666 - flags = 0 - data = length 77, hash 8E4832C0 - sample 444: - time = 14800000 - flags = 0 - data = length 70, hash B92866C9 - sample 445: - time = 14766666 - flags = 0 - data = length 68, hash 66214C05 - sample 446: - time = 14833333 - flags = 0 - data = length 68, hash 4605E638 - sample 447: - time = 15000000 - flags = 0 - data = length 77, hash E5E3D004 - sample 448: - time = 14933333 - flags = 0 - data = length 70, hash E9E98F05 - sample 449: - time = 14900000 - flags = 0 - data = length 68, hash 88475340 - sample 450: - time = 14966666 - flags = 0 - data = length 68, hash 682BED75 - sample 451: - time = 15133333 - flags = 0 - data = length 77, hash 11CC2C48 - sample 452: - time = 15066666 - flags = 0 - data = length 70, hash 7BAE9641 - sample 453: - time = 15033333 - flags = 0 - data = length 68, hash E8DCF97D - sample 454: - time = 15100000 - flags = 0 - data = length 68, hash C8C193B1 - sample 455: - time = 15266666 - flags = 0 - data = length 77, hash 6967C98D - sample 456: - time = 15200000 - flags = 0 - data = length 70, hash 5B33157D - sample 457: - time = 15166666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 458: - time = 15233333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 459: - time = 15400000 - flags = 0 - data = length 77, hash 434E1BAF - sample 460: - time = 15333333 - flags = 0 - data = length 70, hash ECF81CB9 - sample 461: - time = 15300000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 462: - time = 15366666 - flags = 0 - data = length 68, hash 7D7C646A - sample 463: - time = 15533333 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 464: - time = 15466666 - flags = 0 - data = length 70, hash 1DB944F5 - sample 465: - time = 15433333 - flags = 0 - data = length 68, hash BFBDD171 - sample 466: - time = 15500000 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 467: - time = 15666666 - flags = 0 - data = length 77, hash C6D21537 - sample 468: - time = 15600000 - flags = 0 - data = length 70, hash AF7E4C31 - sample 469: - time = 15566666 - flags = 0 - data = length 68, hash 205377AE - sample 470: - time = 15633333 - flags = 0 - data = length 68, hash 3811E1 - sample 471: - time = 15800000 - flags = 0 - data = length 77, hash B8F74A5C - sample 472: - time = 15733333 - flags = 0 - data = length 70, hash F383DB6D - sample 473: - time = 15700000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 474: - time = 15766666 - flags = 0 - data = length 68, hash 6D3E33DD - sample 475: - time = 15933333 - flags = 0 - data = length 77, hash 8E4832C0 - sample 476: - time = 15866666 - flags = 0 - data = length 70, hash B92866C9 - sample 477: - time = 15833333 - flags = 0 - data = length 68, hash 66214C05 - sample 478: - time = 15900000 - flags = 0 - data = length 68, hash 4605E638 - sample 479: - time = 16066666 - flags = 0 - data = length 77, hash E5E3D004 - sample 480: - time = 16000000 - flags = 0 - data = length 70, hash E9E98F05 - sample 481: - time = 15966666 - flags = 0 - data = length 68, hash 88475340 - sample 482: - time = 16033333 - flags = 0 - data = length 68, hash 682BED75 - sample 483: - time = 16200000 - flags = 0 - data = length 77, hash 11CC2C48 - sample 484: - time = 16133333 - flags = 0 - data = length 70, hash 7BAE9641 - sample 485: - time = 16100000 - flags = 0 - data = length 68, hash E8DCF97D - sample 486: - time = 16166666 - flags = 0 - data = length 68, hash C8C193B1 - sample 487: - time = 16333333 - flags = 0 - data = length 77, hash 6967C98D - sample 488: - time = 16266666 - flags = 0 - data = length 70, hash 5B33157D - sample 489: - time = 16233333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 490: - time = 16300000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 491: - time = 16466666 - flags = 0 - data = length 77, hash 434E1BAF - sample 492: - time = 16400000 - flags = 0 - data = length 70, hash ECF81CB9 - sample 493: - time = 16366666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 494: - time = 16433333 - flags = 0 - data = length 68, hash 7D7C646A - sample 495: - time = 16600000 - flags = 0 - data = length 77, hash 9AE9B8F4 - sample 496: - time = 16533333 - flags = 0 - data = length 70, hash 1DB944F5 - sample 497: - time = 16500000 - flags = 0 - data = length 68, hash BFBDD171 - sample 498: - time = 16566666 - flags = 0 - data = length 68, hash 9FA26BA6 - sample 499: - time = 16633333 - flags = 0 - data = length 77, hash AB0BE9E8 - sample 500: - time = 16666666 - flags = 1 - data = length 423, hash DDB543E8 - sample 501: - time = 16800000 - flags = 0 - data = length 71, hash E6801694 - sample 502: - time = 16733333 - flags = 0 - data = length 68, hash 83DC1BAA - sample 503: - time = 16700000 - flags = 0 - data = length 68, hash 66214C05 - sample 504: - time = 16766666 - flags = 0 - data = length 68, hash 4605E639 - sample 505: - time = 16933333 - flags = 0 - data = length 77, hash 76DDE100 - sample 506: - time = 16866666 - flags = 0 - data = length 70, hash 98EFD089 - sample 507: - time = 16833333 - flags = 0 - data = length 68, hash 88475340 - sample 508: - time = 16900000 - flags = 0 - data = length 68, hash 682BED75 - sample 509: - time = 17066666 - flags = 0 - data = length 77, hash 11CC2C49 - sample 510: - time = 17000000 - flags = 0 - data = length 70, hash 7BAE9640 - sample 511: - time = 16966666 - flags = 0 - data = length 68, hash E8DCF97C - sample 512: - time = 17033333 - flags = 0 - data = length 68, hash C8C193B1 - sample 513: - time = 17200000 - flags = 0 - data = length 77, hash 6967C98C - sample 514: - time = 17133333 - flags = 0 - data = length 70, hash 5B33157E - sample 515: - time = 17100000 - flags = 0 - data = length 68, hash 3D0223F9 - sample 516: - time = 17166666 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 517: - time = 17333333 - flags = 0 - data = length 77, hash 434E1BB0 - sample 518: - time = 17266666 - flags = 0 - data = length 70, hash ECF81CBA - sample 519: - time = 17233333 - flags = 0 - data = length 68, hash 9D97CA36 - sample 520: - time = 17300000 - flags = 0 - data = length 68, hash 7D7C6469 - sample 521: - time = 17466666 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 522: - time = 17400000 - flags = 0 - data = length 70, hash 1DB944F6 - sample 523: - time = 17366666 - flags = 0 - data = length 68, hash BFBDD171 - sample 524: - time = 17433333 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 525: - time = 17600000 - flags = 0 - data = length 77, hash C6D21538 - sample 526: - time = 17533333 - flags = 0 - data = length 70, hash AF7E4C32 - sample 527: - time = 17500000 - flags = 0 - data = length 68, hash 205377AE - sample 528: - time = 17566666 - flags = 0 - data = length 68, hash 3811E1 - sample 529: - time = 17733333 - flags = 0 - data = length 77, hash B8F74A5B - sample 530: - time = 17666666 - flags = 0 - data = length 70, hash F383DB6D - sample 531: - time = 17633333 - flags = 0 - data = length 68, hash 8D5999A8 - sample 532: - time = 17700000 - flags = 0 - data = length 68, hash 6D3E33DC - sample 533: - time = 17866666 - flags = 0 - data = length 77, hash 8E4832C1 - sample 534: - time = 17800000 - flags = 0 - data = length 70, hash B92866C8 - sample 535: - time = 17766666 - flags = 0 - data = length 68, hash 66214C05 - sample 536: - time = 17833333 - flags = 0 - data = length 68, hash 4605E639 - sample 537: - time = 18000000 - flags = 0 - data = length 77, hash E5E3D004 - sample 538: - time = 17933333 - flags = 0 - data = length 70, hash E9E98F05 - sample 539: - time = 17900000 - flags = 0 - data = length 68, hash 88475340 - sample 540: - time = 17966666 - flags = 0 - data = length 68, hash 682BED75 - sample 541: - time = 18133333 - flags = 0 - data = length 77, hash 11CC2C49 - sample 542: - time = 18066666 - flags = 0 - data = length 70, hash 7BAE9640 - sample 543: - time = 18033333 - flags = 0 - data = length 68, hash E8DCF97C - sample 544: - time = 18100000 - flags = 0 - data = length 68, hash C8C193B1 - sample 545: - time = 18266666 - flags = 0 - data = length 77, hash 6967C98C - sample 546: - time = 18200000 - flags = 0 - data = length 70, hash 5B33157E - sample 547: - time = 18166666 - flags = 0 - data = length 68, hash 3D0223F9 - sample 548: - time = 18233333 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 549: - time = 18400000 - flags = 0 - data = length 77, hash 434E1BB0 - sample 550: - time = 18333333 - flags = 0 - data = length 70, hash ECF81CBA - sample 551: - time = 18300000 - flags = 0 - data = length 68, hash 9D97CA36 - sample 552: - time = 18366666 - flags = 0 - data = length 68, hash 7D7C6469 - sample 553: - time = 18533333 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 554: - time = 18466666 - flags = 0 - data = length 70, hash 1DB944F6 - sample 555: - time = 18433333 - flags = 0 - data = length 68, hash BFBDD171 - sample 556: - time = 18500000 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 557: - time = 18666666 - flags = 0 - data = length 77, hash C6D21538 - sample 558: - time = 18600000 - flags = 0 - data = length 70, hash AF7E4C32 - sample 559: - time = 18566666 - flags = 0 - data = length 68, hash 205377AE - sample 560: - time = 18633333 - flags = 0 - data = length 68, hash 3811E1 - sample 561: - time = 18800000 - flags = 0 - data = length 77, hash B8F74A5B - sample 562: - time = 18733333 - flags = 0 - data = length 70, hash F383DB6D - sample 563: - time = 18700000 - flags = 0 - data = length 68, hash 8D5999A8 - sample 564: - time = 18766666 - flags = 0 - data = length 68, hash 6D3E33DC - sample 565: - time = 18933333 - flags = 0 - data = length 77, hash 8E4832C1 - sample 566: - time = 18866666 - flags = 0 - data = length 70, hash B92866C8 - sample 567: - time = 18833333 - flags = 0 - data = length 68, hash 66214C05 - sample 568: - time = 18900000 - flags = 0 - data = length 68, hash 4605E639 - sample 569: - time = 19066666 - flags = 0 - data = length 77, hash E5E3D004 - sample 570: - time = 19000000 - flags = 0 - data = length 70, hash E9E98F05 - sample 571: - time = 18966666 - flags = 0 - data = length 68, hash 88475340 - sample 572: - time = 19033333 - flags = 0 - data = length 68, hash 682BED75 - sample 573: - time = 19200000 - flags = 0 - data = length 77, hash 11CC2C49 - sample 574: - time = 19133333 - flags = 0 - data = length 70, hash 7BAE9640 - sample 575: - time = 19100000 - flags = 0 - data = length 68, hash E8DCF97C - sample 576: - time = 19166666 - flags = 0 - data = length 68, hash C8C193B1 - sample 577: - time = 19333333 - flags = 0 - data = length 77, hash 6967C98C - sample 578: - time = 19266666 - flags = 0 - data = length 70, hash 5B33157E - sample 579: - time = 19233333 - flags = 0 - data = length 68, hash 3D0223F9 - sample 580: - time = 19300000 - flags = 0 - data = length 68, hash 1CE6BE2E - sample 581: - time = 19466666 - flags = 0 - data = length 77, hash 434E1BB0 - sample 582: - time = 19400000 - flags = 0 - data = length 70, hash ECF81CBA - sample 583: - time = 19366666 - flags = 0 - data = length 68, hash 9D97CA36 - sample 584: - time = 19433333 - flags = 0 - data = length 68, hash 7D7C6469 - sample 585: - time = 19600000 - flags = 0 - data = length 77, hash 9AE9B8F3 - sample 586: - time = 19533333 - flags = 0 - data = length 70, hash 1DB944F6 - sample 587: - time = 19500000 - flags = 0 - data = length 68, hash BFBDD171 - sample 588: - time = 19566666 - flags = 0 - data = length 68, hash 9FA26BA5 - sample 589: - time = 19733333 - flags = 0 - data = length 77, hash 179FCFE9 - sample 590: - time = 19666666 - flags = 0 - data = length 70, hash AF7E4C32 - sample 591: - time = 19633333 - flags = 0 - data = length 68, hash 205377AE - sample 592: - time = 19700000 - flags = 0 - data = length 68, hash 3811E1 - sample 593: - time = 19866666 - flags = 0 - data = length 77, hash BCD7CD76 - sample 594: - time = 19800000 - flags = 0 - data = length 70, hash F383DB6D - sample 595: - time = 19766666 - flags = 0 - data = length 68, hash 8D5999A8 - sample 596: - time = 19833333 - flags = 0 - data = length 68, hash 6D3E33DC - sample 597: - time = 19966666 - flags = 0 - data = length 77, hash 38D96DF4 - sample 598: - time = 19900000 - flags = 0 - data = length 70, hash 3AC762E9 - sample 599: - time = 19933333 - flags = 0 - data = length 68, hash B1C7769A -tracksEnded = true 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.unknown_length.dump similarity index 100% rename from libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.merge-fragmented-sidx.unknown_length.dump rename to libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable_multiple_sidx.mp4.unknown_length.dump diff --git a/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/manifest.mpd b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/manifest.mpd new file mode 100644 index 00000000000..2c69b8cee3b --- /dev/null +++ b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/manifest.mpd @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/segment_1.m4s b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/segment_1.m4s new file mode 100644 index 00000000000..bba8572c5a7 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/segment_1.m4s differ diff --git a/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/segment_2.m4s b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/segment_2.m4s new file mode 100644 index 00000000000..74e53cf0f6d Binary files /dev/null and b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/segment_2.m4s differ diff --git a/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/segment_init.mp4 b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/segment_init.mp4 new file mode 100644 index 00000000000..0b7063de1b6 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg0/segment_init.mp4 differ diff --git a/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg1/manifest.mpd b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg1/manifest.mpd new file mode 100644 index 00000000000..8b6fd3137ca --- /dev/null +++ b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg1/manifest.mpd @@ -0,0 +1,17 @@ + + + + + + + video.mp4 + + + + + + + + + diff --git a/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg1/video.mp4 b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg1/video.mp4 new file mode 100644 index 00000000000..2a9b55c4b4b Binary files /dev/null and b/libraries/test_data/src/test/assets/media/dash/multi-segment-with-emsg1/video.mp4 differ diff --git a/libraries/test_data/src/test/assets/media/flac/bear_with_vorbis_comments_invalid_mutf8.flac b/libraries/test_data/src/test/assets/media/flac/bear_with_vorbis_comments_invalid_mutf8.flac new file mode 100644 index 00000000000..e281fcda9fd Binary files /dev/null and b/libraries/test_data/src/test/assets/media/flac/bear_with_vorbis_comments_invalid_mutf8.flac differ diff --git a/libraries/test_data/src/test/assets/media/mp4/sample_without_stss.mp4 b/libraries/test_data/src/test/assets/media/mp4/sample_without_stss.mp4 new file mode 100644 index 00000000000..26b3bfbdbd1 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/sample_without_stss.mp4 differ diff --git a/libraries/test_data/src/test/assets/media/mpd/sample_mpd_supplemental_codecs b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_supplemental_codecs new file mode 100644 index 00000000000..2cad73510b1 --- /dev/null +++ b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_supplemental_codecs @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/libraries/test_data/src/test/assets/playbackdumps/dash/multi-segment-with-emsg0.dump b/libraries/test_data/src/test/assets/playbackdumps/dash/multi-segment-with-emsg0.dump new file mode 100644 index 00000000000..0e6dfd861b9 --- /dev/null +++ b/libraries/test_data/src/test/assets/playbackdumps/dash/multi-segment-with-emsg0.dump @@ -0,0 +1,226 @@ +MediaCodecAdapter (media3.video.avc): + inputBuffers: + count = 31 + input buffer #0: + timeUs = 1000000000000 + contents = length 4408, hash C5611A85 + input buffer #1: + timeUs = 1000000133333 + contents = length 565, hash A46533DA + input buffer #2: + timeUs = 1000000066666 + contents = length 83, hash B02DDC9F + input buffer #3: + timeUs = 1000000033333 + contents = length 61, hash 293AD9BD + input buffer #4: + timeUs = 1000000100000 + contents = length 33, hash EA06170F + input buffer #5: + timeUs = 1000000266666 + contents = length 538, hash 3FB5D480 + input buffer #6: + timeUs = 1000000200000 + contents = length 71, hash CB618159 + input buffer #7: + timeUs = 1000000166666 + contents = length 54, hash B0C09666 + input buffer #8: + timeUs = 1000000233333 + contents = length 42, hash D16EB023 + input buffer #9: + timeUs = 1000000400000 + contents = length 537, hash 4940F62A + input buffer #10: + timeUs = 1000000333333 + contents = length 65, hash C96E100 + input buffer #11: + timeUs = 1000000300000 + contents = length 41, hash E89E97DA + input buffer #12: + timeUs = 1000000366666 + contents = length 29, hash 5C33A589 + input buffer #13: + timeUs = 1000000466666 + contents = length 279, hash 8E00D894 + input buffer #14: + timeUs = 1000000433333 + contents = length 42, hash 32D9815A + input buffer #15: + timeUs = 1000000500000 + contents = length 4135, hash FF5D2AE1 + input buffer #16: + timeUs = 1000000633333 + contents = length 443, hash E7EE8CC2 + input buffer #17: + timeUs = 1000000566666 + contents = length 67, hash 628D0096 + input buffer #18: + timeUs = 1000000533333 + contents = length 38, hash 752D1ED3 + input buffer #19: + timeUs = 1000000600000 + contents = length 49, hash 2280616C + input buffer #20: + timeUs = 1000000766666 + contents = length 391, hash E0ADE2EF + input buffer #21: + timeUs = 1000000700000 + contents = length 84, hash BF295395 + input buffer #22: + timeUs = 1000000666666 + contents = length 37, hash A2360933 + input buffer #23: + timeUs = 1000000733333 + contents = length 44, hash B9B84A5A + input buffer #24: + timeUs = 1000000900000 + contents = length 420, hash E6E749C4 + input buffer #25: + timeUs = 1000000833333 + contents = length 80, hash CC538CDF + input buffer #26: + timeUs = 1000000800000 + contents = length 38, hash A457FE9F + input buffer #27: + timeUs = 1000000866666 + contents = length 45, hash 25E9FC21 + input buffer #28: + timeUs = 1000000966666 + contents = length 277, hash 931BCD44 + input buffer #29: + timeUs = 1000000933333 + contents = length 46, hash 1FCA3C4 + input buffer #30: + timeUs = 0 + flags = 4 + contents = length 0, hash 1 + outputBuffers: + count = 30 + output buffer #0: + timeUs = 1000000000000 + size = 4408 + rendered = true + output buffer #1: + timeUs = 1000000133333 + size = 565 + rendered = true + output buffer #2: + timeUs = 1000000066666 + size = 83 + rendered = true + output buffer #3: + timeUs = 1000000033333 + size = 61 + rendered = true + output buffer #4: + timeUs = 1000000100000 + size = 33 + rendered = true + output buffer #5: + timeUs = 1000000266666 + size = 538 + rendered = true + output buffer #6: + timeUs = 1000000200000 + size = 71 + rendered = true + output buffer #7: + timeUs = 1000000166666 + size = 54 + rendered = true + output buffer #8: + timeUs = 1000000233333 + size = 42 + rendered = true + output buffer #9: + timeUs = 1000000400000 + size = 537 + rendered = true + output buffer #10: + timeUs = 1000000333333 + size = 65 + rendered = true + output buffer #11: + timeUs = 1000000300000 + size = 41 + rendered = true + output buffer #12: + timeUs = 1000000366666 + size = 29 + rendered = true + output buffer #13: + timeUs = 1000000466666 + size = 279 + rendered = true + output buffer #14: + timeUs = 1000000433333 + size = 42 + rendered = true + output buffer #15: + timeUs = 1000000500000 + size = 4135 + rendered = true + output buffer #16: + timeUs = 1000000633333 + size = 443 + rendered = true + output buffer #17: + timeUs = 1000000566666 + size = 67 + rendered = true + output buffer #18: + timeUs = 1000000533333 + size = 38 + rendered = true + output buffer #19: + timeUs = 1000000600000 + size = 49 + rendered = true + output buffer #20: + timeUs = 1000000766666 + size = 391 + rendered = true + output buffer #21: + timeUs = 1000000700000 + size = 84 + rendered = true + output buffer #22: + timeUs = 1000000666666 + size = 37 + rendered = true + output buffer #23: + timeUs = 1000000733333 + size = 44 + rendered = true + output buffer #24: + timeUs = 1000000900000 + size = 420 + rendered = true + output buffer #25: + timeUs = 1000000833333 + size = 80 + rendered = true + output buffer #26: + timeUs = 1000000800000 + size = 38 + rendered = true + output buffer #27: + timeUs = 1000000866666 + size = 45 + rendered = true + output buffer #28: + timeUs = 1000000966666 + size = 277 + rendered = true + output buffer #29: + timeUs = 1000000933333 + size = 46 + rendered = true +MetadataOutput: + Metadata[0]: + presentationTimeUs = 0 + entry[0] = EMSG: scheme=urn:example:event, id=1, durationMs=0, value=1 + Metadata[1]: + presentationTimeUs = 500000 + entry[0] = EMSG: scheme=urn:example:event, id=1, durationMs=0, value=1 diff --git a/libraries/test_data/src/test/assets/playbackdumps/dash/multi-segment-with-emsg1.dump b/libraries/test_data/src/test/assets/playbackdumps/dash/multi-segment-with-emsg1.dump new file mode 100644 index 00000000000..d5ac8de3dda --- /dev/null +++ b/libraries/test_data/src/test/assets/playbackdumps/dash/multi-segment-with-emsg1.dump @@ -0,0 +1,226 @@ +MediaCodecAdapter (media3.video.avc): + inputBuffers: + count = 31 + input buffer #0: + timeUs = 1000000000000 + contents = length 4408, hash C5611A85 + input buffer #1: + timeUs = 1000000133333 + contents = length 565, hash A46533DA + input buffer #2: + timeUs = 1000000066666 + contents = length 83, hash B02DDC9F + input buffer #3: + timeUs = 1000000033333 + contents = length 61, hash 293AD9BD + input buffer #4: + timeUs = 1000000100000 + contents = length 33, hash EA06170F + input buffer #5: + timeUs = 1000000266666 + contents = length 538, hash 3FB5D480 + input buffer #6: + timeUs = 1000000200000 + contents = length 71, hash CB618159 + input buffer #7: + timeUs = 1000000166666 + contents = length 54, hash B0C09666 + input buffer #8: + timeUs = 1000000233333 + contents = length 42, hash D16EB023 + input buffer #9: + timeUs = 1000000400000 + contents = length 537, hash 4940F62A + input buffer #10: + timeUs = 1000000333333 + contents = length 65, hash C96E100 + input buffer #11: + timeUs = 1000000300000 + contents = length 41, hash E89E97DA + input buffer #12: + timeUs = 1000000366666 + contents = length 29, hash 5C33A589 + input buffer #13: + timeUs = 1000000466666 + contents = length 279, hash 8E00D894 + input buffer #14: + timeUs = 1000000433333 + contents = length 42, hash 32D9815A + input buffer #15: + timeUs = 1000000500000 + contents = length 4135, hash FF5D2AE1 + input buffer #16: + timeUs = 1000000633333 + contents = length 443, hash E7EE8CC2 + input buffer #17: + timeUs = 1000000566666 + contents = length 67, hash 628D0096 + input buffer #18: + timeUs = 1000000533333 + contents = length 38, hash 752D1ED3 + input buffer #19: + timeUs = 1000000600000 + contents = length 49, hash 2280616C + input buffer #20: + timeUs = 1000000766666 + contents = length 391, hash E0ADE2EF + input buffer #21: + timeUs = 1000000700000 + contents = length 84, hash BF295395 + input buffer #22: + timeUs = 1000000666666 + contents = length 37, hash A2360933 + input buffer #23: + timeUs = 1000000733333 + contents = length 44, hash B9B84A5A + input buffer #24: + timeUs = 1000000900000 + contents = length 420, hash E6E749C4 + input buffer #25: + timeUs = 1000000833333 + contents = length 80, hash CC538CDF + input buffer #26: + timeUs = 1000000800000 + contents = length 38, hash A457FE9F + input buffer #27: + timeUs = 1000000866666 + contents = length 45, hash 25E9FC21 + input buffer #28: + timeUs = 1000000966666 + contents = length 277, hash 931BCD44 + input buffer #29: + timeUs = 1000000933333 + contents = length 46, hash 1FCA3C4 + input buffer #30: + timeUs = 0 + flags = 4 + contents = length 0, hash 1 + outputBuffers: + count = 30 + output buffer #0: + timeUs = 1000000000000 + size = 4408 + rendered = true + output buffer #1: + timeUs = 1000000133333 + size = 565 + rendered = true + output buffer #2: + timeUs = 1000000066666 + size = 83 + rendered = true + output buffer #3: + timeUs = 1000000033333 + size = 61 + rendered = true + output buffer #4: + timeUs = 1000000100000 + size = 33 + rendered = true + output buffer #5: + timeUs = 1000000266666 + size = 538 + rendered = true + output buffer #6: + timeUs = 1000000200000 + size = 71 + rendered = true + output buffer #7: + timeUs = 1000000166666 + size = 54 + rendered = true + output buffer #8: + timeUs = 1000000233333 + size = 42 + rendered = true + output buffer #9: + timeUs = 1000000400000 + size = 537 + rendered = true + output buffer #10: + timeUs = 1000000333333 + size = 65 + rendered = true + output buffer #11: + timeUs = 1000000300000 + size = 41 + rendered = true + output buffer #12: + timeUs = 1000000366666 + size = 29 + rendered = true + output buffer #13: + timeUs = 1000000466666 + size = 279 + rendered = true + output buffer #14: + timeUs = 1000000433333 + size = 42 + rendered = true + output buffer #15: + timeUs = 1000000500000 + size = 4135 + rendered = true + output buffer #16: + timeUs = 1000000633333 + size = 443 + rendered = true + output buffer #17: + timeUs = 1000000566666 + size = 67 + rendered = true + output buffer #18: + timeUs = 1000000533333 + size = 38 + rendered = true + output buffer #19: + timeUs = 1000000600000 + size = 49 + rendered = true + output buffer #20: + timeUs = 1000000766666 + size = 391 + rendered = true + output buffer #21: + timeUs = 1000000700000 + size = 84 + rendered = true + output buffer #22: + timeUs = 1000000666666 + size = 37 + rendered = true + output buffer #23: + timeUs = 1000000733333 + size = 44 + rendered = true + output buffer #24: + timeUs = 1000000900000 + size = 420 + rendered = true + output buffer #25: + timeUs = 1000000833333 + size = 80 + rendered = true + output buffer #26: + timeUs = 1000000800000 + size = 38 + rendered = true + output buffer #27: + timeUs = 1000000866666 + size = 45 + rendered = true + output buffer #28: + timeUs = 1000000966666 + size = 277 + rendered = true + output buffer #29: + timeUs = 1000000933333 + size = 46 + rendered = true +MetadataOutput: + Metadata[0]: + presentationTimeUs = 0 + entry[0] = EMSG: scheme=urn:test, id=1, durationMs=2184, value=1 + Metadata[1]: + presentationTimeUs = 500000 + entry[0] = EMSG: scheme=urn:test, id=2, durationMs=2184, value=2 diff --git a/libraries/test_exoplayer_playback/build.gradle b/libraries/test_exoplayer_playback/build.gradle index 85b533380c4..3355935541c 100644 --- a/libraries/test_exoplayer_playback/build.gradle +++ b/libraries/test_exoplayer_playback/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android.namespace 'androidx.media3.test.exoplayer.playback' diff --git a/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DebugRenderersFactory.java b/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DebugRenderersFactory.java index 89189ed2aa0..87decb70074 100644 --- a/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DebugRenderersFactory.java +++ b/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DebugRenderersFactory.java @@ -100,7 +100,9 @@ public DebugMediaCodecVideoRenderer( .setAllowedJoiningTimeMs(allowedJoiningTimeMs) .setEventHandler(eventHandler) .setEventListener(eventListener) - .setMaxDroppedFramesToNotify(maxDroppedFrameCountToNotify)); + .setMaxDroppedFramesToNotify(maxDroppedFrameCountToNotify) + // TODO: b/501235190 - Remove workaround once operating rate is always set. + .setAssumedMinimumCodecOperatingRate(0)); timestampsList = new long[ARRAY_SIZE]; } diff --git a/libraries/test_proguard/build.gradle b/libraries/test_proguard/build.gradle index e29a600c542..1d322cf8133 100644 --- a/libraries/test_proguard/build.gradle +++ b/libraries/test_proguard/build.gradle @@ -11,8 +11,8 @@ // 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: '../../constants.gradle' apply plugin: 'com.android.application' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.test.proguard' @@ -22,20 +22,14 @@ android { test.assets.srcDir '../test_data/src/test/assets/' } - compileSdk project.ext.compileSdkVersion - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 coreLibraryDesugaringEnabled true } defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.appTargetSdkVersion - testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } buildTypes { diff --git a/libraries/test_proguard/src/androidTest/java/androidx/media3/test/proguard/ExtractorModuleProguardTest.java b/libraries/test_proguard/src/androidTest/java/androidx/media3/test/proguard/ExtractorModuleProguardTest.java index 6c6ea89ddc0..37bb28f07b0 100644 --- a/libraries/test_proguard/src/androidTest/java/androidx/media3/test/proguard/ExtractorModuleProguardTest.java +++ b/libraries/test_proguard/src/androidTest/java/androidx/media3/test/proguard/ExtractorModuleProguardTest.java @@ -15,26 +15,24 @@ */ package androidx.media3.test.proguard; -import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Ignore; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; /** Unit test executing methods in {@link ExtractorModuleProguard}. */ @RunWith(AndroidJUnit4.class) -@Ignore("Fails on gradle: b/463675073") public final class ExtractorModuleProguardTest { @Test public void defaultExtractorFactory_createExtensionFlacExtractor_succeeds() throws Exception { ExtractorModuleProguard.createLibFlacExtractorWithDefaultExtractorsFactory( - ApplicationProvider.getApplicationContext()); + InstrumentationRegistry.getInstrumentation().getContext()); } @Test public void defaultExtractorFactory_createMidiExtractor_succeeds() throws Exception { ExtractorModuleProguard.createMidiExtractorWithDefaultExtractorsFactory( - ApplicationProvider.getApplicationContext()); + InstrumentationRegistry.getInstrumentation().getContext()); } } diff --git a/libraries/test_proguard/src/androidTest/java/androidx/media3/test/proguard/UiModuleProguardTest.java b/libraries/test_proguard/src/androidTest/java/androidx/media3/test/proguard/UiModuleProguardTest.java index dd64ba07f2c..d7942926d65 100644 --- a/libraries/test_proguard/src/androidTest/java/androidx/media3/test/proguard/UiModuleProguardTest.java +++ b/libraries/test_proguard/src/androidTest/java/androidx/media3/test/proguard/UiModuleProguardTest.java @@ -19,7 +19,7 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Ignore; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; @@ -46,10 +46,9 @@ public void playerView_inflateVideoDecoderGLSurfaceView_succeeds() { } @Test - @Ignore("Fails on gradle: b/463675073") public void playerControlView_scrubbingWithExoPlayer_succeeds() throws Exception { UiModuleProguard.scrubOnTimeBarWithExoPlayerAndCheckThatSuppressionReasonChangesAndSeeksHappen( - getApplicationContext()); + InstrumentationRegistry.getInstrumentation().getContext(), getApplicationContext()); } @Test diff --git a/libraries/test_proguard/src/main/java/androidx/media3/test/proguard/UiModuleProguard.java b/libraries/test_proguard/src/main/java/androidx/media3/test/proguard/UiModuleProguard.java index dd5acf7b14d..561d5502f2e 100644 --- a/libraries/test_proguard/src/main/java/androidx/media3/test/proguard/UiModuleProguard.java +++ b/libraries/test_proguard/src/main/java/androidx/media3/test/proguard/UiModuleProguard.java @@ -37,10 +37,12 @@ import androidx.media3.common.TrackGroup; import androidx.media3.common.Tracks; import androidx.media3.common.util.ConditionVariable; +import androidx.media3.datasource.AssetDataSource; import androidx.media3.exoplayer.BaseRenderer; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.Renderer; import androidx.media3.exoplayer.RendererCapabilities; +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.video.VideoDecoderGLSurfaceView; import androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView; import androidx.media3.transformer.Composition; @@ -87,7 +89,7 @@ public static void inflatePlayerViewWithVideoDecoderGLSurfaceView(Context contex * the suppression reason change and seeks happen. */ public static void scrubOnTimeBarWithExoPlayerAndCheckThatSuppressionReasonChangesAndSeeksHappen( - Context context) throws InterruptedException { + Context instrumentationContext, Context applicationContext) throws InterruptedException { ConditionVariable playerReady = new ConditionVariable(); List<@Player.PlaybackSuppressionReason Integer> playbackSuppressionReasons = Collections.synchronizedList(new ArrayList<>()); @@ -119,11 +121,20 @@ public void onPositionDiscontinuity( Handler mainHandler = new Handler(Looper.getMainLooper()); mainHandler.post( () -> { - ExoPlayer player = new ExoPlayer.Builder(context).build(); + ExoPlayer player = + new ExoPlayer.Builder(applicationContext) + .setMediaSourceFactory( + new DefaultMediaSourceFactory( + // The asset media URI used below is accessible to the instrumentation + // context, not the app context (which needs to be used for all other + // purposes). + /* dataSourceFactory= */ () -> + new AssetDataSource(instrumentationContext))) + .build(); player.addListener(listener); PlayerControlView playerControlView = (PlayerControlView) - LayoutInflater.from(context) + LayoutInflater.from(applicationContext) .inflate( R.layout.player_control_view_with_scrubbable_timebar, /* root= */ null); playerControlView.setPlayer(player); diff --git a/libraries/test_session_common/build.gradle b/libraries/test_session_common/build.gradle index 9ca25bd7b60..1d5e0a2934f 100644 --- a/libraries/test_session_common/build.gradle +++ b/libraries/test_session_common/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.test.session.common' diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl index 497f04fd9f3..e333852677e 100644 --- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl +++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl @@ -96,6 +96,8 @@ interface IRemoteMediaController { void setMediaItemsPreparePlayAddItemsSeek(String controllerId, in List initialMediaItems, in List addedMediaItems, int seekIndex); void executeCommandButtonAction(String controllerId, int buttonIndex); void executeCustomLayoutAction(String controllerId, int buttonIndex); + int getTimelineWindowCount(String controllerId); + boolean hasQueueCommandsSupport(String controllerId); // MediaBrowser methods Bundle getLibraryRoot(String controllerId, in Bundle libraryParams); diff --git a/libraries/test_session_current/build.gradle b/libraries/test_session_current/build.gradle index bf8815308ed..64aad057a34 100644 --- a/libraries/test_session_current/build.gradle +++ b/libraries/test_session_current/build.gradle @@ -11,25 +11,21 @@ // 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/constants.gradle" apply plugin: 'com.android.application' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.test.session' - compileSdk project.ext.compileSdkVersion - defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.appTargetSdkVersion - testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + lintOptions { + // TODO: b/353490583 - Enable this once the violations are fixed. + checkTestSources false } sourceSets { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java index de0b8f40e1c..54e395065fd 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java @@ -84,6 +84,7 @@ import org.junit.After; import org.junit.Before; import org.junit.ClassRule; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.RuleChain; @@ -2346,6 +2347,7 @@ public void getVolume_returnsVolumeOfPlayerInSession() throws Exception { } @Test + @Ignore("flaky: b/398226223") public void getUnmuteVolume_returnsUnmuteVolumeOfPlayerInSession_roundTrip() throws Exception { float testVolume = .5f; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java index fc5d0c86046..2b20b07c7a2 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java @@ -212,6 +212,29 @@ public void setPlaybackSpeed_actionSetPlaybackSpeedNotAvailable_commandNotAvaila .isFalse()); } + @Test + public void getAvailableCommands_withSkipToQueueItemAction_containsSeekToMediaItemCommand() + throws Exception { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setActions(PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM) + .build(); + session.setPlaybackState(playbackStateCompat); + // sessionFlags = 0 (doesn't contain FLAG_HANDLES_QUEUE_COMMANDS) + session.setFlags(0); + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + + threadTestRule + .getHandler() + .postAndSync( + () -> + assertThat( + controller + .getAvailableCommands() + .contains(Player.COMMAND_SEEK_TO_MEDIA_ITEM)) + .isTrue()); + } + @Test public void createController_alreadyReleasedSession_throwsSecurityExceptionWithoutCallingOnDisconnected() @@ -2120,6 +2143,9 @@ public void prepare_withMetadata_callsPrepareFromMediaId() throws Exception { session.setPlaybackState( new PlaybackStateCompat.Builder() .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .setActions( + PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID) .build()); session.setMetadata( new MediaMetadataCompat.Builder() @@ -2163,6 +2189,9 @@ public void prepare_withMetadataAndActiveQueueItemId_callsPrepareFromMediaId() t new PlaybackStateCompat.Builder() .setActiveQueueItemId(4) .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .setActions( + PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID) .build()); session.setMetadata( new MediaMetadataCompat.Builder() @@ -2208,6 +2237,7 @@ public void prepare_withQueue_callsPrepare() throws Exception { session.setPlaybackState( new PlaybackStateCompat.Builder() .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .setActions(PlaybackStateCompat.ACTION_PREPARE | PlaybackStateCompat.ACTION_PLAY) .build()); session.setQueue(testQueue); MediaController controller = controllerTestRule.createController(session.getSessionToken()); @@ -2247,6 +2277,7 @@ public void prepare_withQueueAndActiveQueueItemId_callsPrepare() throws Exceptio new PlaybackStateCompat.Builder() .setActiveQueueItemId(5) .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .setActions(PlaybackStateCompat.ACTION_PREPARE | PlaybackStateCompat.ACTION_PLAY) .build()); session.setQueue(testQueue); MediaController controller = controllerTestRule.createController(session.getSessionToken()); @@ -2285,6 +2316,9 @@ public void prepare_withQueueAndMetadata_callsPrepareFromMediaId() throws Except session.setPlaybackState( new PlaybackStateCompat.Builder() .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .setActions( + PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID) .build()); session.setMetadata( new MediaMetadataCompat.Builder() @@ -2332,6 +2366,7 @@ public void prepare_withQueueAndMetadataAndActiveQueueItemId_callsPrepare() thro new PlaybackStateCompat.Builder() .setActiveQueueItemId(4) .setState(PlaybackStateCompat.STATE_NONE, /* position= */ 0, /* playbackSpeed= */ 0.0f) + .setActions(PlaybackStateCompat.ACTION_PREPARE | PlaybackStateCompat.ACTION_PLAY) .build()); session.setMetadata( new MediaMetadataCompat.Builder() diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java index 06918e11b97..a7621ccd72a 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java @@ -53,7 +53,9 @@ import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeoutException; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; @@ -68,8 +70,8 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { private static final String TAG = "MediaControllerTest"; - // The maximum time to wait for an operation. private static final long TIMEOUT_MS = 3000L; + private static final long NO_OP_TIMEOUT_MS = 100L; @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); @@ -120,7 +122,7 @@ public void play() throws Exception { List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); - setPlaybackState(PlaybackStateCompat.STATE_PAUSED); + setPlaybackStateAndActions(PlaybackStateCompat.STATE_PAUSED, PlaybackStateCompat.ACTION_PLAY); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(1); @@ -136,7 +138,7 @@ public void pause() throws Exception { List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); - setPlaybackState(PlaybackStateCompat.STATE_PLAYING); + setPlaybackStateAndActions(PlaybackStateCompat.STATE_PLAYING, PlaybackStateCompat.ACTION_PAUSE); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(1); @@ -152,6 +154,7 @@ public void prepare() throws Exception { List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions(PlaybackStateCompat.STATE_NONE, PlaybackStateCompat.ACTION_PREPARE); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(1); @@ -168,6 +171,9 @@ public void stop() throws Exception { List testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_NONE, + PlaybackStateCompat.ACTION_PREPARE | PlaybackStateCompat.ACTION_STOP); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(/* count= */ 2); @@ -179,6 +185,32 @@ public void stop() throws Exception { assertThat(sessionCallback.onStopCalled).isTrue(); } + @Test + public void stop_resetsInitializationState() throws Exception { + List testList = MediaTestUtils.createMediaItemsWithArtworkData(2); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PAUSED, + PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_STOP); + RemoteMediaController controller = createControllerAndWaitConnection(); + controller.setMediaItems(testList); + sessionCallback.reset(2); + controller.play(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + + sessionCallback.reset(1); + controller.stop(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + sessionCallback.reset(2); + controller.prepare(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue(); + assertThat(sessionCallback.onAddQueueItemAtCalledCount).isEqualTo(1); + } + @Test public void seekToDefaultPosition() throws Exception { RemoteMediaController controller = createControllerAndWaitConnection(); @@ -333,7 +365,7 @@ public void addMediaItems() throws Exception { session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); - setPlaybackState(PlaybackStateCompat.STATE_PLAYING); + setPlaybackStateAndActions(PlaybackStateCompat.STATE_PLAYING, /* actions= */ 0); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(size); @@ -360,7 +392,7 @@ public void removeMediaItems() throws Exception { session.setQueue(MediaTestUtils.convertToQueueItemsWithoutBitmap(testList)); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); - setPlaybackState(PlaybackStateCompat.STATE_BUFFERING); + setPlaybackStateAndActions(PlaybackStateCompat.STATE_BUFFERING, /* actions= */ 0); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(count); @@ -400,7 +432,8 @@ public void setMediaItems_nonEmptyList_startFromFirstMediaItem() throws Exceptio List testList = MediaTestUtils.createMediaItemsWithArtworkData(size); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); - setPlaybackState(PlaybackStateCompat.STATE_PLAYING); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PLAYING, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(size); @@ -423,7 +456,8 @@ public void setMediaItems_nonEmptyList_startFromNonFirstMediaItem() throws Excep List testList = MediaTestUtils.createMediaItemsWithArtworkData(size); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); - setPlaybackState(PlaybackStateCompat.STATE_PLAYING); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PLAYING, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(size); int testStartIndex = 2; @@ -450,7 +484,8 @@ public void setMediaItems_emptyList() throws Exception { session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); - setPlaybackState(PlaybackStateCompat.STATE_PLAYING); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PLAYING, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(size); @@ -466,6 +501,8 @@ public void setMediaItems_emptyList() throws Exception { @Test public void setShuffleMode() throws Exception { session.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_NONE, PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(1); @@ -806,22 +843,291 @@ public void seekToPrevious_callsOnSkipToPrevious() throws Exception { assertThat(sessionCallback.onSkipToPreviousCalled).isTrue(); } - private void setPlaybackState(int state) { - long allActions = - PlaybackStateCompat.ACTION_PLAY - | PlaybackStateCompat.ACTION_PAUSE - | PlaybackStateCompat.ACTION_PLAY_PAUSE - | PlaybackStateCompat.ACTION_STOP - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT - | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - | PlaybackStateCompat.ACTION_FAST_FORWARD - | PlaybackStateCompat.ACTION_REWIND; + @Test + public void setMediaItems_paused_doesNotInitializeUntilPlay() throws Exception { + List testList = MediaTestUtils.createMediaItemsWithArtworkData(3); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PAUSED, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaItems(testList); + + assertThat(sessionCallback.await(NO_OP_TIMEOUT_MS)).isFalse(); + assertThat(sessionCallback.onPrepareFromMediaIdCalled).isFalse(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isFalse(); + + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue(); + } + + @Test + public void setMediaItems_thenAddBeforeInitialize_doesNotInitializeUntilPlay() throws Exception { + List testList = MediaTestUtils.createMediaItemsWithArtworkData(3); + List addedList = MediaTestUtils.createMediaItemsWithArtworkData(1); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PAUSED, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(4); + + controller.setMediaItems(testList); + assertThat(sessionCallback.await(NO_OP_TIMEOUT_MS)).isFalse(); + controller.addMediaItems(addedList); + assertThat(sessionCallback.await(NO_OP_TIMEOUT_MS)).isFalse(); + assertThat(sessionCallback.onPrepareFromMediaIdCalled).isFalse(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isFalse(); + assertThat(sessionCallback.onAddQueueItemAtCalledCount).isEqualTo(0); + + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue(); + assertThat(sessionCallback.onAddQueueItemAtCalledCount).isEqualTo(3); + } + + @Test + public void setMediaItems_thenRemoveBeforeInitialize_doesNotInitializeUntilPlay() + throws Exception { + List testList = MediaTestUtils.createMediaItemsWithArtworkData(3); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PAUSED, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaItems(testList); + assertThat(sessionCallback.await(NO_OP_TIMEOUT_MS)).isFalse(); + controller.removeMediaItem(2); + assertThat(sessionCallback.await(NO_OP_TIMEOUT_MS)).isFalse(); + assertThat(sessionCallback.onPrepareFromMediaIdCalled).isFalse(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isFalse(); + assertThat(sessionCallback.onRemoveQueueItemCalledCount).isEqualTo(0); + + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue(); + assertThat(sessionCallback.onRemoveQueueItemCalledCount).isEqualTo(0); + } + + @Test + public void setMediaItems_pausedAndSupportPrepare_initializesImmediately() throws Exception { + List testList = MediaTestUtils.createMediaItemsWithArtworkData(3); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PAUSED, + PlaybackStateCompat.ACTION_PREPARE + | PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID); + RemoteMediaController controller = createControllerAndWaitConnection(); + waitForCondition(() -> controller.hasQueueCommandsSupport(), TIMEOUT_MS); + sessionCallback.reset(1); + + controller.setMediaItems(testList); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPrepareFromMediaIdCalled).isTrue(); + } + + @Test + public void setMediaItems_thenPrepare_initializes() throws Exception { + List testList = MediaTestUtils.createMediaItems(3, /* buildWithUri= */ false); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_NONE, PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaItems(testList); + + assertThat(sessionCallback.await(NO_OP_TIMEOUT_MS)).isFalse(); + + controller.prepare(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPrepareFromMediaIdCalled).isTrue(); + } + + @Test + public void setMediaItems_withMediaUri_initializesWithPlayFromUri() throws Exception { + Uri testUri = Uri.parse("http://test.com"); + MediaItem testItem = + new MediaItem.Builder() + .setMediaId("id") + .setRequestMetadata( + new MediaItem.RequestMetadata.Builder().setMediaUri(testUri).build()) + .build(); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PAUSED, PlaybackStateCompat.ACTION_PLAY_FROM_URI); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaItem(testItem); + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromUriCalled).isTrue(); + assertThat(sessionCallback.uri).isEqualTo(testUri); + } + + @Test + public void setMediaItems_withSearchQuery_initializesWithPlayFromSearch() throws Exception { + String testQuery = "test query"; + MediaItem testItem = + new MediaItem.Builder() + .setMediaId("id") + .setRequestMetadata( + new MediaItem.RequestMetadata.Builder().setSearchQuery(testQuery).build()) + .build(); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PAUSED, PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaItem(testItem); + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromSearchCalled).isTrue(); + assertThat(sessionCallback.query).isEqualTo(testQuery); + } + + @Test + public void setMediaItems_withStartPosition_seeksAfterPlayFromMediaId() throws Exception { + long startPositionMs = 1000L; + List testList = MediaTestUtils.createMediaItemsWithArtworkData(1); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PAUSED, + PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID | PlaybackStateCompat.ACTION_SEEK_TO); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(2); + + controller.setMediaItems(testList, /* startIndex= */ 0, startPositionMs); + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue(); + assertThat(sessionCallback.onSeekToCalled).isTrue(); + assertThat(sessionCallback.seekPosition).isEqualTo(startPositionMs); + } + + @Test + public void setMediaItems_replacementBeforeInitialize_initializesWithSecondList() + throws Exception { + List testList1 = MediaTestUtils.createMediaItems(/* buildWithUri= */ false, "id1"); + List testList2 = + MediaTestUtils.createMediaItems(/* buildWithUri= */ false, "id2", "id3"); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PAUSED, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaItems(testList1); + assertThat(sessionCallback.await(NO_OP_TIMEOUT_MS)).isFalse(); + controller.setMediaItems(testList2); + assertThat(sessionCallback.await(NO_OP_TIMEOUT_MS)).isFalse(); + sessionCallback.reset(2); + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.mediaId).isEqualTo(testList2.get(0).mediaId); + assertThat(sessionCallback.onAddQueueItemAtCalledCount).isEqualTo(1); + assertThat(sessionCallback.queueDescriptionListForAdd.get(0).getMediaId()) + .isEqualTo(testList2.get(1).mediaId); + } + + @Test + public void setMediaItems_withoutChangeMediaItemsCommand_initializesOnlyFirstItem() + throws Exception { + List testList = MediaTestUtils.createMediaItemsWithArtworkData(2); + session.setFlags(0); // No FLAG_HANDLES_QUEUE_COMMANDS + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PAUSED, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(1); + + controller.setMediaItems(testList); + controller.play(); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue(); + assertThat(sessionCallback.onAddQueueItemAtCalledCount).isEqualTo(0); + } + + @Test + public void addMediaItems_afterInitialize_addsImmediately() throws Exception { + List testList = MediaTestUtils.createMediaItemsWithArtworkData(2); + List addedList = MediaTestUtils.createMediaItemsWithArtworkData(1); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PAUSED, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID); + RemoteMediaController controller = createControllerAndWaitConnection(); + controller.setMediaItems(testList); + sessionCallback.reset(2); + controller.play(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + session.setQueue(MediaTestUtils.convertToQueueItemsWithoutBitmap(testList)); + // Wait until the controller accepts the new platform state for further operations. + Thread.sleep(2 * MediaController.DEFAULT_PLATFORM_CALLBACK_AGGREGATION_TIMEOUT_MS); + + sessionCallback.reset(1); + controller.addMediaItems(addedList); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onAddQueueItemAtCalledCount).isEqualTo(1); + } + + @Test + public void removeMediaItem_afterInitialize_removesImmediately() throws Exception { + List testList = MediaTestUtils.createMediaItemsWithArtworkData(2); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackStateAndActions( + PlaybackStateCompat.STATE_PLAYING, PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID); + RemoteMediaController controller = createControllerAndWaitConnection(); + sessionCallback.reset(2); + controller.setMediaItems(testList); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + session.setQueue(MediaTestUtils.convertToQueueItemsWithoutBitmap(testList)); + // Wait until the controller accepts the new platform state for further operations. + Thread.sleep(2 * MediaController.DEFAULT_PLATFORM_CALLBACK_AGGREGATION_TIMEOUT_MS); + + sessionCallback.reset(1); + controller.removeMediaItem(1); + + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); + assertThat(sessionCallback.onRemoveQueueItemCalledCount).isEqualTo(1); + } + + private void setPlaybackStateAndActions(int state, long actions) { PlaybackStateCompat playbackState = - new PlaybackStateCompat.Builder().setActions(allActions).setState(state, 0L, 0.0f).build(); + new PlaybackStateCompat.Builder() + .setActions(actions) + .setState(state, /* position= */ 0, /* playbackSpeed= */ 1.0f) + .build(); session.setPlaybackState(playbackState); } - class TestVolumeProvider extends VolumeProviderCompat { + private static void waitForCondition(Callable condition, long timeoutMs) + throws Exception { + long startTime = System.currentTimeMillis(); + while (!condition.call()) { + if (System.currentTimeMillis() - startTime > timeoutMs) { + throw new TimeoutException("Condition not met within " + timeoutMs + " ms"); + } + Thread.sleep(100); + } + } + + private static class TestVolumeProvider extends VolumeProviderCompat { CountDownLatch latch = new CountDownLatch(1); boolean setVolumeToCalled; boolean adjustVolumeCalled; @@ -847,52 +1153,46 @@ public void onAdjustVolume(int direction) { } } - private class MediaSessionCallback extends MediaSessionCompat.Callback { - public CountDownLatch latch = new CountDownLatch(1); - public long seekPosition; - public float speed; - public long queueItemId; - public RatingCompat rating; - public String mediaId; - public String query; - public Uri uri; - public String action; - public String command; - public Bundle extras; - public boolean captioningEnabled; - @RepeatMode public int repeatMode; - @ShuffleMode public int shuffleMode; - public final List queueIndices = new ArrayList<>(); - public final List queueDescriptionListForAdd = new ArrayList<>(); - public final List queueDescriptionListForRemove = new ArrayList<>(); - - public int onPlayCalledCount; - public boolean onPauseCalled; - public boolean onStopCalled; - public boolean onSkipToPreviousCalled; - public boolean onSkipToNextCalled; - public boolean onSeekToCalled; - public boolean onFastForwardCalled; - public boolean onRewindCalled; - public boolean onSetPlaybackSpeedCalled; - public boolean onSkipToQueueItemCalled; - public boolean onSetRatingCalled; - public boolean onPlayFromMediaIdCalled; - public boolean onPlayFromSearchCalled; - public boolean onPlayFromUriCalled; - public boolean onCustomActionCalled; - public boolean onPrepareCalled; - public boolean onPrepareFromMediaIdCalled; - public boolean onPrepareFromSearchCalled; - public boolean onPrepareFromUriCalled; - public boolean onSetCaptioningEnabledCalled; - public boolean onSetRepeatModeCalled; - public boolean onSetShuffleModeCalled; - public boolean onAddQueueItemCalled; - public int onAddQueueItemAtCalledCount; - public int onRemoveQueueItemCalledCount; - - public void reset(int count) { + private static class MediaSessionCallback extends MediaSessionCompat.Callback { + private CountDownLatch latch = new CountDownLatch(1); + private long seekPosition; + private float speed; + private long queueItemId; + private RatingCompat rating; + private String mediaId; + private String query; + private Uri uri; + private String action; + private Bundle extras; + @RepeatMode private int repeatMode; + @ShuffleMode private int shuffleMode; + private final List queueIndices = new ArrayList<>(); + private final List queueDescriptionListForAdd = new ArrayList<>(); + private final List queueDescriptionListForRemove = new ArrayList<>(); + + private int onPlayCalledCount; + private boolean onPauseCalled; + private boolean onStopCalled; + private boolean onSkipToPreviousCalled; + private boolean onSkipToNextCalled; + private boolean onSeekToCalled; + private boolean onFastForwardCalled; + private boolean onRewindCalled; + private boolean onSetPlaybackSpeedCalled; + private boolean onSkipToQueueItemCalled; + private boolean onSetRatingCalled; + private boolean onPlayFromMediaIdCalled; + private boolean onPlayFromSearchCalled; + private boolean onPlayFromUriCalled; + private boolean onCustomActionCalled; + private boolean onPrepareCalled; + private boolean onPrepareFromMediaIdCalled; + private boolean onSetRepeatModeCalled; + private boolean onSetShuffleModeCalled; + private int onAddQueueItemAtCalledCount; + private int onRemoveQueueItemCalledCount; + + private void reset(int count) { latch = new CountDownLatch(count); seekPosition = -1; speed = -1.0f; @@ -903,8 +1203,6 @@ public void reset(int count) { uri = null; action = null; extras = null; - command = null; - captioningEnabled = false; repeatMode = PlaybackStateCompat.REPEAT_MODE_NONE; shuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE; queueIndices.clear(); @@ -928,17 +1226,13 @@ public void reset(int count) { onCustomActionCalled = false; onPrepareCalled = false; onPrepareFromMediaIdCalled = false; - onPrepareFromSearchCalled = false; - onPrepareFromUriCalled = false; - onSetCaptioningEnabledCalled = false; onSetRepeatModeCalled = false; onSetShuffleModeCalled = false; - onAddQueueItemCalled = false; onAddQueueItemAtCalledCount = 0; onRemoveQueueItemCalledCount = 0; } - public boolean await(long timeoutMs) { + private boolean await(long timeoutMs) { try { return latch.await(timeoutMs, MILLISECONDS); } catch (InterruptedException e) { @@ -949,21 +1243,18 @@ public boolean await(long timeoutMs) { @Override public void onPlay() { onPlayCalledCount++; - setPlaybackState(PlaybackStateCompat.STATE_PLAYING); latch.countDown(); } @Override public void onPause() { onPauseCalled = true; - setPlaybackState(PlaybackStateCompat.STATE_PAUSED); latch.countDown(); } @Override public void onStop() { onStopCalled = true; - setPlaybackState(PlaybackStateCompat.STATE_STOPPED); latch.countDown(); } @@ -1067,7 +1358,6 @@ public void onPrepareFromMediaId(String mediaId, Bundle extras) { @Override public void onPrepareFromSearch(String query, Bundle extras) { - onPrepareFromSearchCalled = true; this.query = query; this.extras = extras; latch.countDown(); @@ -1075,7 +1365,6 @@ public void onPrepareFromSearch(String query, Bundle extras) { @Override public void onPrepareFromUri(Uri uri, Bundle extras) { - onPrepareFromUriCalled = true; this.uri = uri; this.extras = extras; latch.countDown(); @@ -1085,13 +1374,11 @@ public void onPrepareFromUri(Uri uri, Bundle extras) { public void onSetRepeatMode(@RepeatMode int repeatMode) { onSetRepeatModeCalled = true; this.repeatMode = repeatMode; - session.setRepeatMode(repeatMode); latch.countDown(); } @Override public void onAddQueueItem(MediaDescriptionCompat description) { - onAddQueueItemCalled = true; queueDescriptionListForAdd.add(description); latch.countDown(); } @@ -1113,8 +1400,6 @@ public void onRemoveQueueItem(MediaDescriptionCompat description) { @Override public void onSetCaptioningEnabled(boolean enabled) { - onSetCaptioningEnabledCalled = true; - captioningEnabled = enabled; latch.countDown(); } @@ -1122,7 +1407,6 @@ public void onSetCaptioningEnabled(boolean enabled) { public void onSetShuffleMode(@ShuffleMode int shuffleMode) { onSetShuffleModeCalled = true; this.shuffleMode = shuffleMode; - session.setShuffleMode(shuffleMode); latch.countDown(); } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java index 243ceea20ee..18c9bc2e3d9 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java @@ -23,24 +23,33 @@ import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.Futures.immediateFuture; +import static java.lang.Math.max; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.Assert.assertThrows; import android.app.Notification; import android.app.NotificationManager; +import android.app.UiAutomation; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Point; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.os.ParcelFileDescriptor; import android.service.notification.StatusBarNotification; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; +import android.view.WindowManager; import androidx.annotation.Nullable; import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; import androidx.media3.common.Player; import androidx.media3.common.Player.Listener; import androidx.media3.common.Player.PositionInfo; @@ -60,10 +69,15 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -1110,6 +1124,65 @@ public void pauseAllPlayersAndStopSelf_stopsForegroundService() throws Exception assertThat(notificationFlags & Notification.FLAG_FOREGROUND_SERVICE).isEqualTo(0); } + /** Regression test for https://github.com/androidx/media/issues/3118. */ + @Test + public void playbackWithArtworkOneSixthOfDisplaySize_doesNotCrashDevice() throws Exception { + // Create compressed Bitmap of exactly 1/6 of the larger display size. + Point displaySize = new Point(); + context.getSystemService(WindowManager.class).getDefaultDisplay().getSize(displaySize); + int oneSixthDisplaySize = max(displaySize.x / 6, displaySize.y / 6); + Bitmap artworkBitmap = + Bitmap.createBitmap(oneSixthDisplaySize, oneSixthDisplaySize, Bitmap.Config.ARGB_8888); + artworkBitmap.eraseColor(Color.BLUE); + ByteArrayOutputStream bitmapOutputStream = new ByteArrayOutputStream(); + artworkBitmap.compress(Bitmap.CompressFormat.PNG, 100, bitmapOutputStream); + byte[] artworkBytes = bitmapOutputStream.toByteArray(); + TestHandler handler = new TestHandler(Looper.getMainLooper()); + ExoPlayer player = + handler.postAndSync( + () -> { + ExoPlayer exoPlayer = new TestExoPlayerBuilder(context).build(); + exoPlayer.setMediaItem( + new MediaItem.Builder() + .setUri("asset:///media/mp4/sample.mp4") + .setMediaMetadata( + new MediaMetadata.Builder() + .setArtworkData(artworkBytes, MediaMetadata.PICTURE_TYPE_FRONT_COVER) + .build()) + .build()); + exoPlayer.setRepeatMode(Player.REPEAT_MODE_ALL); + exoPlayer.prepare(); + return exoPlayer; + }); + MediaSession mediaSession = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player).build(); + TestServiceRegistry.getInstance().setOnGetSessionHandler(controllerInfo -> mediaSession); + // Start the service by creating a remote controller. + controllerTestRule.createRemoteController( + token, /* waitForConnection= */ true, /* connectionHints= */ Bundle.EMPTY); + MockMediaSessionService service = + (MockMediaSessionService) TestServiceRegistry.getInstance().getServiceInstance(); + runPendingMainThreadMessages(); + UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); + uiAutomation.executeShellCommand("logcat -c"); // Ensure logcat is clean. + + MainLooperTestRule.runOnMainSync(player::play); + // Sleep for 2 seconds to give SysUI sufficient time to receive and use the Bitmap. + Thread.sleep(2000); + mediaSession.release(); + service.blockUntilAllControllersUnbind(TIMEOUT_MS); + + // Check if there was a recent SysUI crash in logcat. + try (ParcelFileDescriptor command = + uiAutomation.executeShellCommand("logcat -d -t 500 AndroidRuntime:E *:S"); + FileInputStream fileInputStream = new FileInputStream(command.getFileDescriptor()); + InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, UTF_8); + BufferedReader reader = new BufferedReader(inputStreamReader)) { + assertThat(reader.lines().anyMatch(line -> line.contains("Process: com.android.systemui"))) + .isFalse(); + } + } + private void runPendingMainThreadMessages() throws Exception { MainLooperTestRule.runOnMainSync(() -> {}); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java index f2fc1743690..b1f71a010d3 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java @@ -65,6 +65,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.After; @@ -387,15 +388,10 @@ public void deadlock() throws Exception { @Test public void creatingTwoSessionWithSameId() { String sessionId = "testSessionId"; - MediaSession session = - new MediaSession.Builder( - context, new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build()) - .setId(sessionId) - .build(); - - MediaSession.Builder builderWithSameId = - new MediaSession.Builder( - context, new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build()); + Player player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); + MediaSession session = new MediaSession.Builder(context, player).setId(sessionId).build(); + + MediaSession.Builder builderWithSameId = new MediaSession.Builder(context, player); try { builderWithSameId.setId(sessionId).build(); assertWithMessage( @@ -407,7 +403,8 @@ public void creatingTwoSessionWithSameId() { session.release(); // Creating a new session with ID of the closed session is okay. - MediaSession sessionWithSameId = builderWithSameId.build(); + MediaSession sessionWithSameId = + new MediaSession.Builder(context, player).setId(sessionId).build(); sessionWithSameId.release(); } @@ -484,6 +481,33 @@ public MediaSession.ConnectionResult onConnect( assertThat(player.seekPositionMs).isEqualTo(testSeekPositionMs); } + @Test + public void getPlatformToken_inApp_ensuresControllerInfoIsTrusted() throws Exception { + AtomicBoolean controllerIsTrusted = new AtomicBoolean(); + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player) + .setId("getPlatformToken_inApp_ensuresControllerInfoIsTrusted") + .setCallback( + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, ControllerInfo controller) { + controllerIsTrusted.set(controller.isTrusted()); + return MediaSession.Callback.super.onConnect(session, controller); + } + }) + .build()); + android.media.session.MediaSession.Token token = session.getPlatformToken(); + + android.media.session.MediaController platformController = + new android.media.session.MediaController(context, token); + platformController.getTransportControls().seekTo(100); + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO, TIMEOUT_MS); + + assertThat(controllerIsTrusted.get()).isTrue(); + } + @Test public void getControllerVersion() throws Exception { CountDownLatch connectedLatch = new CountDownLatch(1); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java index ce43861e572..1230155975d 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java @@ -34,6 +34,7 @@ import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.MediaMetadata; import androidx.media3.common.PlaybackParameters; +import androidx.media3.common.Player; import androidx.media3.common.Rating; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.util.BundleCollectionUtil; @@ -842,6 +843,24 @@ public void executeCustomLayoutAction(String controllerId, int buttonIndex) }); } + @Override + public int getTimelineWindowCount(String controllerId) throws RemoteException { + return runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + return controller.getCurrentTimeline().getWindowCount(); + }); + } + + @Override + public boolean hasQueueCommandsSupport(String controllerId) throws RemoteException { + return runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + return controller.getAvailableCommands().contains(Player.COMMAND_CHANGE_MEDIA_ITEMS); + }); + } + //////////////////////////////////////////////////////////////////////////////// // MediaBrowser methods //////////////////////////////////////////////////////////////////////////////// diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java index 8393ab54f3a..1b7410deb4c 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java @@ -460,6 +460,14 @@ public void executeCustomLayoutAction(int buttonIndex) throws RemoteException { binder.executeCustomLayoutAction(controllerId, buttonIndex); } + public int getTimelineWindowCount() throws RemoteException { + return binder.getTimelineWindowCount(controllerId); + } + + public boolean hasQueueCommandsSupport() throws RemoteException { + return binder.hasQueueCommandsSupport(controllerId); + } + //////////////////////////////////////////////////////////////////////////////// // Non-public methods //////////////////////////////////////////////////////////////////////////////// diff --git a/libraries/test_utils/build.gradle b/libraries/test_utils/build.gradle index 6051e60eea8..524bacaf3e3 100644 --- a/libraries/test_utils/build.gradle +++ b/libraries/test_utils/build.gradle @@ -14,7 +14,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget -apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" +apply plugin: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" apply plugin: 'kotlin-android' android { diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeChunkSource.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeChunkSource.java index 5e055c15cb9..079c18be413 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeChunkSource.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeChunkSource.java @@ -140,7 +140,7 @@ public void getNextChunk( new DataSpec(Uri.parse(uri), fakeDataChunk.byteOffset, fakeDataChunk.length); int trackType = MimeTypes.getTrackType(selectedFormat.sampleMimeType); out.chunk = - new SingleSampleMediaChunk( + createMediaChunk( dataSource, dataSpec, selectedFormat, @@ -154,6 +154,35 @@ public void getNextChunk( } } + /** + * Creates a {@link MediaChunk} for the given parameters. + * + *

Subclasses can override this method to return custom {@link MediaChunk} implementations. + */ + protected MediaChunk createMediaChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + @C.SelectionReason int trackSelectionReason, + @Nullable Object trackSelectionData, + long startTimeUs, + long endTimeUs, + long chunkIndex, + @C.TrackType int trackType, + Format sampleFormat) { + return new SingleSampleMediaChunk( + dataSource, + dataSpec, + trackFormat, + trackSelectionReason, + trackSelectionData, + startTimeUs, + endTimeUs, + chunkIndex, + trackType, + sampleFormat); + } + @Override public void onChunkLoadCompleted(Chunk chunk) { // Do nothing. diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelection.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelection.java index b392a02e1e7..bb92d3360c2 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelection.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelection.java @@ -29,26 +29,47 @@ import java.util.Objects; /** - * A fake {@link ExoTrackSelection} that only returns 1 fixed track, and allows querying the number - * of calls to its methods. + * A fake {@link ExoTrackSelection} that supports selecting a subset of tracks within a {@link + * TrackGroup}, returns 1 fixed track, and tracks the number of calls to its methods. */ @UnstableApi public class FakeTrackSelection extends BaseTrackSelection { - private final TrackGroup rendererTrackGroup; private final int selectedIndex; public int enableCount; public int releaseCount; public boolean isEnabled; + /** + * Creates an instance that selects all tracks in the group and defaults the selected index to + * {@code 0}. + * + * @param rendererTrackGroup The {@link TrackGroup} to select from. + */ public FakeTrackSelection(TrackGroup rendererTrackGroup) { this(rendererTrackGroup, /* selectedIndex= */ 0); } + /** + * Creates an instance that selects all tracks in the group and sets the selected index. + * + * @param rendererTrackGroup The {@link TrackGroup} to select from. + * @param selectedIndex The index of the selected track within the group. + */ public FakeTrackSelection(TrackGroup rendererTrackGroup, int selectedIndex) { - super(rendererTrackGroup, getAllTrackIndices(rendererTrackGroup)); - this.rendererTrackGroup = rendererTrackGroup; + this(rendererTrackGroup, getAllTrackIndices(rendererTrackGroup), selectedIndex); + } + + /** + * Creates an instance that selects the specified tracks in the group and sets the selected index. + * + * @param rendererTrackGroup The {@link TrackGroup} to select from. + * @param tracks The indices of the selected tracks in the group. + * @param selectedIndex The index of the selected track within the {@code tracks} array. + */ + public FakeTrackSelection(TrackGroup rendererTrackGroup, int[] tracks, int selectedIndex) { + super(rendererTrackGroup, tracks); this.selectedIndex = selectedIndex; } @@ -105,13 +126,13 @@ public int evaluateQueueSize(long playbackPositionUs, List @Override public boolean excludeTrack(int index, long exclusionDurationMs) { assertThat(isEnabled).isTrue(); - return false; + return super.excludeTrack(index, exclusionDurationMs); } @Override public boolean isTrackExcluded(int index, long nowMs) { assertThat(isEnabled).isTrue(); - return false; + return super.isTrackExcluded(index, nowMs); } @Override @@ -119,20 +140,19 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof FakeTrackSelection)) { + if (!(o instanceof FakeTrackSelection) || !super.equals(o)) { return false; } FakeTrackSelection that = (FakeTrackSelection) o; return enableCount == that.enableCount && releaseCount == that.releaseCount && isEnabled == that.isEnabled - && selectedIndex == that.selectedIndex - && Objects.equals(rendererTrackGroup, that.rendererTrackGroup); + && selectedIndex == that.selectedIndex; } @Override public int hashCode() { - return Objects.hash(rendererTrackGroup, enableCount, releaseCount, isEnabled, selectedIndex); + return Objects.hash(super.hashCode(), enableCount, releaseCount, isEnabled, selectedIndex); } private static int[] getAllTrackIndices(TrackGroup trackGroup) { diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/MediaPeriodAsserts.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/MediaPeriodAsserts.java index 452a8557548..479a5e1ac59 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/MediaPeriodAsserts.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/MediaPeriodAsserts.java @@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat; import androidx.annotation.Nullable; -import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.StreamKey; import androidx.media3.common.TrackGroup; @@ -28,13 +27,10 @@ import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaPeriod.Callback; import androidx.media3.exoplayer.source.TrackGroupArray; -import androidx.media3.exoplayer.source.chunk.MediaChunk; -import androidx.media3.exoplayer.source.chunk.MediaChunkIterator; -import androidx.media3.exoplayer.trackselection.BaseTrackSelection; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -108,23 +104,21 @@ void assertGetStreamKeysAndManifestFilterIntegration( for (int i = 0; i < trackGroupArray.length; i++) { TrackGroup trackGroup = trackGroupArray.get(i); for (int j = 0; j < trackGroup.length; j++) { - testSelections.add(Collections.singletonList(new TestTrackSelection(trackGroup, j))); + testSelections.add( + ImmutableList.of( + new FakeTrackSelection(trackGroup, new int[] {j}, /* selectedIndex= */ 0))); } if (trackGroup.length > 1) { - testSelections.add(Collections.singletonList(new TestTrackSelection(trackGroup, 0, 1))); + testSelections.add( + ImmutableList.of( + new FakeTrackSelection(trackGroup, new int[] {0, 1}, /* selectedIndex= */ 0))); testSelections.add( Arrays.asList( - new ExoTrackSelection[] { - new TestTrackSelection(trackGroup, 0), new TestTrackSelection(trackGroup, 1) - })); + new FakeTrackSelection(trackGroup, new int[] {0}, /* selectedIndex= */ 0), + new FakeTrackSelection(trackGroup, new int[] {1}, /* selectedIndex= */ 0))); } if (trackGroup.length > 2) { - int[] allTracks = new int[trackGroup.length]; - for (int j = 0; j < trackGroup.length; j++) { - allTracks[j] = j; - } - testSelections.add( - Collections.singletonList(new TestTrackSelection(trackGroup, allTracks))); + testSelections.add(ImmutableList.of(new FakeTrackSelection(trackGroup))); } } if (trackGroupArray.length > 1) { @@ -132,17 +126,18 @@ void assertGetStreamKeysAndManifestFilterIntegration( for (int j = i + 1; j < trackGroupArray.length; j++) { testSelections.add( Arrays.asList( - new ExoTrackSelection[] { - new TestTrackSelection(trackGroupArray.get(i), 0), - new TestTrackSelection(trackGroupArray.get(j), 0) - })); + new FakeTrackSelection( + trackGroupArray.get(i), new int[] {0}, /* selectedIndex= */ 0), + new FakeTrackSelection( + trackGroupArray.get(j), new int[] {0}, /* selectedIndex= */ 0))); } } } if (trackGroupArray.length > 2) { List selectionsFromAllGroups = new ArrayList<>(); for (int i = 0; i < trackGroupArray.length; i++) { - selectionsFromAllGroups.add(new TestTrackSelection(trackGroupArray.get(i), 0)); + selectionsFromAllGroups.add( + new FakeTrackSelection(trackGroupArray.get(i), new int[] {0}, /* selectedIndex= */ 0)); } testSelections.add(selectionsFromAllGroups); } @@ -227,37 +222,4 @@ public void onContinueLoadingRequested(MediaPeriod source) { testThread.release(); return trackGroupArray.get(); } - - private static final class TestTrackSelection extends BaseTrackSelection { - - public TestTrackSelection(TrackGroup trackGroup, int... tracks) { - super(trackGroup, tracks); - } - - @Override - public int getSelectedIndex() { - return 0; - } - - @Override - public @C.SelectionReason int getSelectionReason() { - return C.SELECTION_REASON_UNKNOWN; - } - - @Override - @Nullable - public Object getSelectionData() { - return null; - } - - @Override - public void updateSelectedTrack( - long playbackPositionUs, - long bufferedDurationUs, - long availableDurationUs, - List queue, - MediaChunkIterator[] mediaChunkIterators) { - // Do nothing. - } - } } diff --git a/libraries/test_utils_robolectric/build.gradle b/libraries/test_utils_robolectric/build.gradle index 5d737f72f15..0360fa77048 100644 --- a/libraries/test_utils_robolectric/build.gradle +++ b/libraries/test_utils_robolectric/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { diff --git a/libraries/transformer/build.gradle b/libraries/transformer/build.gradle index d35c8c81d12..45d70506994 100644 --- a/libraries/transformer/build.gradle +++ b/libraries/transformer/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.transformer' @@ -64,7 +65,6 @@ dependencies { androidTestImplementation 'com.google.testparameterinjector:test-parameter-injector:' + testParameterInjectorVersion androidTestImplementation project(modulePrefix + 'lib-inspector') androidTestImplementation project(modulePrefix + 'lib-inspector-frame') - androidTestImplementation project(modulePrefix + 'lib-effect-ndk') androidTestImplementation project(modulePrefix + 'test-utils') } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java index bf9f2007723..172aa02d154 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java @@ -63,6 +63,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -72,6 +73,7 @@ * seeking}. */ @RunWith(AndroidJUnit4.class) +@Ignore("Flaky: b/362905994") public class CompositionPlayerSeekTest { private static final long TEST_TIMEOUT_MS = isRunningOnEmulator() ? 20_000 : 10_000; diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java index e22377347e2..267b5937e72 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -108,7 +108,6 @@ import androidx.media3.effect.ScaleAndRotateTransformation; import androidx.media3.effect.SpeedChangeEffect; import androidx.media3.effect.TimestampWrapper; -import androidx.media3.effect.ndk.NdkTransformerBuilder; import androidx.media3.exoplayer.audio.TeeAudioProcessor; import androidx.media3.extractor.mp4.Mp4Extractor; import androidx.media3.extractor.text.DefaultSubtitleParserFactory; @@ -2314,42 +2313,6 @@ public void transcode_withOutputAudioMimeTypeOpus_completesSuccessfully() throws assertThat(audioBytesSeen.get()).isEqualTo(101_760); } - @Test - @SdkSuppress(minSdkVersion = 34) - public void transcode_withOutputVideoMimeTypeAv1_andPacketConsumer_completesSuccessfully() - throws Exception { - assumeFormatsSupported( - context, - testId, - /* inputFormat= */ MP4_ASSET.videoFormat, - /* outputFormat= */ MP4_ASSET - .videoFormat - .buildUpon() - .setSampleMimeType(MimeTypes.VIDEO_AV1) - .setCodecs(null) - .build()); - MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET.uri)); - EditedMediaItem editedMediaItem = new EditedMediaItem.Builder(mediaItem).build(); - Transformer transformer = - NdkTransformerBuilder.create(context) - .setHardwareBufferEffectsPipeline(new DefaultHardwareBufferEffectsPipeline()) - .setVideoMimeType(MimeTypes.VIDEO_AV1) - .build(); - - ExportTestResult exportTestResult = - new TransformerAndroidTestRunner.Builder(context, transformer) - .build() - .run(testId, editedMediaItem); - ExportResult exportResult = exportTestResult.exportResult; - - String actualMimeType = - retrieveTrackFormat(context, exportTestResult.filePath, C.TRACK_TYPE_VIDEO).sampleMimeType; - assertThat(actualMimeType).isEqualTo(MimeTypes.VIDEO_AV1); - assertThat(exportResult.exportException).isNull(); - assertThat(exportResult.approximateDurationMs).isGreaterThan(0); - assertThat(exportResult.videoMimeType).isEqualTo(MimeTypes.VIDEO_AV1); - } - @Test public void transmux_audioWithEditList_preservesDuration() throws Exception { Context context = ApplicationProvider.getApplicationContext(); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerParameterizedPacketConsumerAndroidTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerParameterizedPacketConsumerAndroidTest.java deleted file mode 100644 index acabe233818..00000000000 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerParameterizedPacketConsumerAndroidTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2026 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.transformer; - -import static androidx.media3.test.utils.FormatSupportAssumptions.assumeAllFormatsSupported; -import static androidx.media3.transformer.CompositionAssetInfo.MULTI_SEQUENCE_IMAGE_CONFIGS; -import static androidx.media3.transformer.CompositionAssetInfo.MULTI_SEQUENCE_MISMATCHED_DURATION_CONFIGS; -import static androidx.media3.transformer.CompositionAssetInfo.MULTI_SEQUENCE_VIDEO_CONFIGS; -import static androidx.media3.transformer.CompositionAssetInfo.SINGLE_SEQUENCE_CONFIGS; -import static com.google.common.truth.Truth.assertThat; - -import android.content.Context; -import androidx.media3.common.C; -import androidx.media3.common.util.Util; -import androidx.media3.effect.DefaultHardwareBufferEffectsPipeline; -import androidx.media3.effect.ndk.NdkTransformerBuilder; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.filters.SdkSuppress; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.testing.junit.testparameterinjector.TestParameter; -import com.google.testing.junit.testparameterinjector.TestParameterInjector; -import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; -import java.io.File; -import java.util.List; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Parameterized end-to-end export tests for {@link Transformer}. */ -@RunWith(TestParameterInjector.class) -public final class TransformerParameterizedPacketConsumerAndroidTest { - - private final Context context = ApplicationProvider.getApplicationContext(); - private @MonotonicNonNull ListeningExecutorService glExecutorService; - - private static class TestConfigProvider extends TestParameterValuesProvider { - @Override - protected List provideValues( - TestParameterValuesProvider.Context context) { - return new ImmutableList.Builder() - .addAll(SINGLE_SEQUENCE_CONFIGS) - .addAll(MULTI_SEQUENCE_IMAGE_CONFIGS) - .addAll(MULTI_SEQUENCE_VIDEO_CONFIGS) - .addAll(MULTI_SEQUENCE_MISMATCHED_DURATION_CONFIGS) - .build(); - } - } - - @Before - public void setUp() { - glExecutorService = - MoreExecutors.listeningDecorator(Util.newSingleThreadExecutor("PacketProcessor:Effect")); - } - - @After - public void tearDown() { - if (glExecutorService != null) { - glExecutorService.shutdown(); - } - } - - // TODO: b/479415308 - Expand API versions below 34 once supported. - @Test - @SdkSuppress(minSdkVersion = 34) - public void export_withPacketProcessor_completesSuccessfully( - @TestParameter(valuesProvider = TestConfigProvider.class) CompositionAssetInfo testConfig) - throws Exception { - String testId = "export_withPacketProcessor_" + testConfig; - Composition composition = testConfig.getComposition(); - assumeAllFormatsSupported( - context, - testId, - /* inputFormats= */ testConfig.getAllVideoFormats(), - /* outputFormat= */ testConfig.getVideoEncoderInputFormat()); - Transformer transformer = - NdkTransformerBuilder.create(context) - .setHardwareBufferEffectsPipeline(new DefaultHardwareBufferEffectsPipeline()) - .build(); - - ExportTestResult result = - new TransformerAndroidTestRunner.Builder(context, transformer) - .build() - .run(testId, composition); - - assertThat(result.exportResult.exportException).isNull(); - assertThat(new File(result.filePath).length()).isGreaterThan(0); - assertThat(result.exportResult.fileSizeBytes).isGreaterThan(0); - if (result.exportResult.approximateDurationMs != C.TIME_UNSET) { - assertThat(result.exportResult.approximateDurationMs).isGreaterThan(0); - } - } -} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/CompositionPlayerPacketConsumerSurfaceViewPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/CompositionPlayerPacketConsumerSurfaceViewPixelTest.java index 4828aee44bc..6e8f785fe30 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/CompositionPlayerPacketConsumerSurfaceViewPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/CompositionPlayerPacketConsumerSurfaceViewPixelTest.java @@ -54,6 +54,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -64,6 +65,7 @@ * CompositionPlayer.Builder#setPacketConsumerFactory(PacketConsumer.Factory)} when outputting to a * {@link android.view.SurfaceView}. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class CompositionPlayerPacketConsumerSurfaceViewPixelTest { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorMultipleTextureOutputPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorMultipleTextureOutputPixelTest.java index 2fab8f4ccab..5ecaa4d741b 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorMultipleTextureOutputPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorMultipleTextureOutputPixelTest.java @@ -38,6 +38,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -49,6 +50,7 @@ *

Confirms that the output timestamps are correct for each frame, and that the output pixels are * correct for the first frame of each bitmap. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class DefaultVideoFrameProcessorMultipleTextureOutputPixelTest { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorParametrizedSurfaceOutputTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorParametrizedSurfaceOutputTest.java index 9ce20cd56d4..a124284cbd1 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorParametrizedSurfaceOutputTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorParametrizedSurfaceOutputTest.java @@ -35,6 +35,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -49,6 +50,7 @@ * succeeds. */ // TODO: b/263395272 - Move this test to effects/mh tests +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(Parameterized.class) public class DefaultVideoFrameProcessorParametrizedSurfaceOutputTest { @Rule public final TestName testName = new TestName(); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorTextureOutputPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorTextureOutputPixelTest.java index 763d35ca27a..a9a3af950e6 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorTextureOutputPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/DefaultVideoFrameProcessorTextureOutputPixelTest.java @@ -84,6 +84,7 @@ * output matches expected output, either from a golden file or from another edit. */ // TODO: b/263395272 - Move this test to effects/mh tests, and remove @TestOnly dependencies. +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public final class DefaultVideoFrameProcessorTextureOutputPixelTest { private static final String ORIGINAL_PNG_ASSET_PATH = diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ExportTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ExportTest.java index fc3ed4d5bdf..fc552b3f217 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ExportTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ExportTest.java @@ -64,12 +64,14 @@ import java.io.File; import org.junit.AssumptionViolatedException; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; /** {@link Transformer} instrumentation tests. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class ExportTest { private static final String TAG = "ExportTest"; diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ForceInterpretHdrVideoAsSdrTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ForceInterpretHdrVideoAsSdrTest.java index f3f784f046d..f002da64f13 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ForceInterpretHdrVideoAsSdrTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ForceInterpretHdrVideoAsSdrTest.java @@ -39,6 +39,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -49,6 +50,7 @@ * Composition#HDR_MODE_EXPERIMENTAL_FORCE_INTERPRET_HDR_AS_SDR forcing HDR contents to be * interpreted as SDR}. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class ForceInterpretHdrVideoAsSdrTest { @Rule public final TestName testName = new TestName(); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrEditingTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrEditingTest.java index a1e378def89..4592c2476c7 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrEditingTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrEditingTest.java @@ -65,6 +65,7 @@ import org.junit.After; import org.junit.AssumptionViolatedException; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -74,6 +75,7 @@ * {@link Transformer} instrumentation test for applying an {@linkplain * Composition#HDR_MODE_KEEP_HDR HDR frame edit}. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public final class HdrEditingTest { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/PlaybackVideoGraphWrapperTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/PlaybackVideoGraphWrapperTest.java index 8967f7c3116..4785b7bc006 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/PlaybackVideoGraphWrapperTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/PlaybackVideoGraphWrapperTest.java @@ -38,12 +38,14 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; /** Instrumentation tests for {@link PlaybackVideoGraphWrapper}. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class PlaybackVideoGraphWrapperTest { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ReplayCacheTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ReplayCacheTest.java index 094caf277d6..d2574f6171c 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ReplayCacheTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ReplayCacheTest.java @@ -68,6 +68,7 @@ import org.junit.runner.RunWith; /** Instrumentation tests for frame replaying (dynamic effect update). */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class ReplayCacheTest { private static final long TEST_TIMEOUT_MS = isRunningOnEmulator() ? 20_000 : 10_000; diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingMediaCodecTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingMediaCodecTest.java index 4c076f81b6a..7c465661bc1 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingMediaCodecTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingMediaCodecTest.java @@ -39,6 +39,7 @@ import com.google.common.collect.ImmutableList; import java.util.Objects; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -48,6 +49,7 @@ * {@link Transformer} instrumentation test for applying an {@linkplain * Composition#HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC HDR to SDR tone mapping edit}. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class ToneMapHdrToSdrUsingMediaCodecTest { @Rule public final TestName testName = new TestName(); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingOpenGlPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingOpenGlPixelTest.java index a4ac5f430a8..cf71139d1f1 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingOpenGlPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingOpenGlPixelTest.java @@ -37,6 +37,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -49,6 +50,7 @@ * output matches expected output, either from a golden file or from another edit. */ // TODO: b/263395272 - Move this test to effects/mh tests. +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public final class ToneMapHdrToSdrUsingOpenGlPixelTest { private static final String TAG = "ToneMapHdrToSdrGl"; diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingOpenGlTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingOpenGlTest.java index 320fd411d8a..0f821743895 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingOpenGlTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ToneMapHdrToSdrUsingOpenGlTest.java @@ -35,6 +35,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -44,6 +45,7 @@ * {@link Transformer} instrumentation test for applying an {@linkplain * Composition#HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL HDR to SDR tone mapping edit}. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class ToneMapHdrToSdrUsingOpenGlTest { @Rule public final TestName testName = new TestName(); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeForegroundSpeedTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeForegroundSpeedTest.java index dd1324794a2..a7f24d66586 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeForegroundSpeedTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeForegroundSpeedTest.java @@ -47,12 +47,14 @@ import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; /** Checks transcoding speed when running in foreground. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class TranscodeForegroundSpeedTest { private final Context context = ApplicationProvider.getApplicationContext(); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeQualityTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeQualityTest.java index 3d50fcf8284..80023660eca 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeQualityTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeQualityTest.java @@ -41,7 +41,6 @@ import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import androidx.media3.effect.DefaultHardwareBufferEffectsPipeline; -import androidx.media3.effect.ndk.NdkTransformerBuilder; import androidx.media3.inspector.frame.FrameExtractor; import androidx.media3.transformer.AndroidTestUtil; import androidx.media3.transformer.EditedMediaItem; @@ -56,6 +55,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -65,6 +65,7 @@ import org.junit.runners.Parameterized.Parameters; /** Checks transcoding quality. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(Parameterized.class) public final class TranscodeQualityTest { private static final String TAG = "TranscodeQualityTest"; @@ -73,7 +74,6 @@ public final class TranscodeQualityTest { "test-generated-goldens/FrameExtractorTest/hlg10-color-test_0.000.png"; private static final String LEGACY = "legacy"; - private static final String PACKET_CONSUMER_NDK = "packet_consumer_ndk"; private static final String PACKET_CONSUMER = "packet_consumer"; @Rule public final TestName testName = new TestName(); @@ -81,7 +81,7 @@ public final class TranscodeQualityTest { @Parameters(name = "{0}") public static ImmutableList params() { if (SDK_INT >= 34) { - return ImmutableList.of(LEGACY, PACKET_CONSUMER_NDK, PACKET_CONSUMER); + return ImmutableList.of(LEGACY, PACKET_CONSUMER); } return ImmutableList.of(LEGACY); } @@ -164,7 +164,6 @@ public void transcodeAvcToAvc320x240_ssimIsGreaterThan90Percent() throws Excepti @SdkSuppress(minSdkVersion = 34) // HDR Bitmap extraction requires API 34+. public void transcode_hlg10_outputsHlg() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - assumeFalse(mode.equals(PACKET_CONSUMER_NDK)); assumeDeviceSupportsHdrEditing(testId, MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat); assumeFormatsSupported( context, @@ -201,10 +200,6 @@ public void transcode_hlg10_outputsHlg() throws Exception { } private static Transformer.Builder createBuilder(Context context, String mode) { - if (mode.equals(PACKET_CONSUMER_NDK)) { - return NdkTransformerBuilder.create(context) - .setHardwareBufferEffectsPipeline(new DefaultHardwareBufferEffectsPipeline()); - } if (mode.equals(PACKET_CONSUMER)) { return new Transformer.Builder(context) .setHardwareBufferEffectsPipeline(new DefaultHardwareBufferEffectsPipeline()); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeSpeedTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeSpeedTest.java index 98883b47dfa..0750cf877d3 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeSpeedTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeSpeedTest.java @@ -45,12 +45,14 @@ import com.google.common.collect.ImmutableList; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; /** Checks transcoding speed. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class TranscodeSpeedTest { private final Context context = ApplicationProvider.getApplicationContext(); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerHdrSequenceEffectTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerHdrSequenceEffectTest.java index 4b7f5bc909e..050dd0c8f74 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerHdrSequenceEffectTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerHdrSequenceEffectTest.java @@ -59,6 +59,7 @@ import java.io.File; import java.util.List; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -68,6 +69,7 @@ * Tests for using different {@linkplain Effect effects} for {@link MediaItem MediaItems} in one * {@link EditedMediaItemSequence}, with HDR assets. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public final class TransformerHdrSequenceEffectTest { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerMhUltraHdrTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerMhUltraHdrTest.java index c68ef022993..c0cbf10e407 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerMhUltraHdrTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerMhUltraHdrTest.java @@ -50,12 +50,14 @@ import org.json.JSONException; import org.junit.AssumptionViolatedException; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; /** Tests for Ultra HDR support in Transformer that should only run in mobile harness. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public final class TransformerMhUltraHdrTest { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerWithInAppMp4MuxerEndToEndMhTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerWithInAppMp4MuxerEndToEndMhTest.java index 2ba4b8e0fe1..3f38b819de3 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerWithInAppMp4MuxerEndToEndMhTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformerWithInAppMp4MuxerEndToEndMhTest.java @@ -35,12 +35,14 @@ import com.google.common.collect.ImmutableList; import java.io.File; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; /** End-to-end instrumentation test for {@link Transformer} with {@link InAppMp4Muxer}. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class TransformerWithInAppMp4MuxerEndToEndMhTest { @Rule public final TestName testName = new TestName(); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/VideoTimestampConsistencyTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/VideoTimestampConsistencyTest.java index 9eaf30f4f16..ea5bc535997 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/VideoTimestampConsistencyTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/VideoTimestampConsistencyTest.java @@ -50,6 +50,7 @@ import java.util.List; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -59,6 +60,7 @@ * A test that guarantees the timestamp is handled identically between {@link CompositionPlayer} and * {@link Transformer}. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class VideoTimestampConsistencyTest { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerPixelTest.java index 27ec6c0f01b..337ec339f9d 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerPixelTest.java @@ -58,6 +58,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -67,6 +68,7 @@ // These tests are in the performance package even though they are not performance tests so that // they are not run on all devices. This is because they use ImageReader, which has a tendency to // drop frames. +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class CompositionPlayerPixelTest { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/EffectPlaybackPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/EffectPlaybackPixelTest.java index 69569335e0c..a50a52e7ec9 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/EffectPlaybackPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/EffectPlaybackPixelTest.java @@ -71,6 +71,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -80,6 +81,7 @@ // These tests are in the performance package even though they are not performance tests so that // they are not run on all devices. This is because they use ImageReader, which has a tendency to // drop frames. +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class EffectPlaybackPixelTest { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/VideoCompositionPreviewPerformanceTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/VideoCompositionPreviewPerformanceTest.java index 5bc1a28c09a..dd807403d52 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/VideoCompositionPreviewPerformanceTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/VideoCompositionPreviewPerformanceTest.java @@ -52,6 +52,7 @@ import org.junit.runner.RunWith; /** Performance tests for the composition previewing pipeline in {@link CompositionPlayer}. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class VideoCompositionPreviewPerformanceTest { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/VideoEffectsPreviewPerformanceTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/VideoEffectsPreviewPerformanceTest.java index 4e1a1d5481d..385787fef7b 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/VideoEffectsPreviewPerformanceTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/VideoEffectsPreviewPerformanceTest.java @@ -38,10 +38,12 @@ import java.util.concurrent.atomic.AtomicLong; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.junit.After; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; /** Performance tests for the effects previewing pipeline in ExoPlayer. */ +@Ignore("Only intended to run on internal infra: b/396671260") @RunWith(AndroidJUnit4.class) public class VideoEffectsPreviewPerformanceTest { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java index 2f2ebfd66b8..b765cc7a1d7 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java @@ -51,6 +51,7 @@ import androidx.media3.exoplayer.audio.AudioSink; import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer; import androidx.media3.exoplayer.image.ImageDecoder; +import androidx.media3.exoplayer.image.ImageMetadataListener; import androidx.media3.exoplayer.image.ImageOutput; import androidx.media3.exoplayer.image.ImageRenderer; import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; @@ -735,6 +736,9 @@ private static final class SequenceImageRenderer extends ImageRenderer { private boolean mayRenderStartOfStream; private @VideoSink.FirstFrameReleaseInstruction int nextFirstFrameReleaseInstruction; private @MonotonicNonNull WakeupListener wakeupListener; + @Nullable private ImageMetadataListener imageMetadataListener; + private @MonotonicNonNull Format outputFormat; + private long streamOffsetUs; public SequenceImageRenderer(ImageDecoder.Factory imageDecoderFactory, VideoSink videoSink) { super(imageDecoderFactory, ImageOutput.NO_OP); @@ -834,6 +838,7 @@ protected void onStreamChanged( timestampIterator = createTimestampIterator(/* positionUs= */ startPositionUs); videoEffects = editedMediaItem.effects.videoEffects; inputStreamPending = true; + streamOffsetUs = offsetUs; super.onStreamChanged(formats, startPositionUs, offsetUs, mediaPeriodId); } @@ -858,15 +863,17 @@ protected boolean processOutputBuffer( long positionUs, long elapsedRealtimeUs, Bitmap outputImage, long timeUs) { if (inputStreamPending) { checkState(streamStartPositionUs != C.TIME_UNSET); - videoSink.onInputStreamChanged( - VideoSink.INPUT_TYPE_BITMAP, + outputFormat = new Format.Builder() .setSampleMimeType(MimeTypes.IMAGE_RAW) .setWidth(outputImage.getWidth()) .setHeight(outputImage.getHeight()) .setColorInfo(ColorInfo.SRGB_BT709_FULL) .setFrameRate(/* frameRate= */ DEFAULT_FRAME_RATE) - .build(), + .build(); + videoSink.onInputStreamChanged( + VideoSink.INPUT_TYPE_BITMAP, + outputFormat, streamStartPositionUs, nextFirstFrameReleaseInstruction, videoEffects); @@ -880,6 +887,10 @@ protected boolean processOutputBuffer( if (isLastInSequence(getTimeline(), checkNotNull(getMediaPeriodId()))) { videoSink.signalEndOfInput(); } + if (imageMetadataListener != null) { + imageMetadataListener.onImageAboutToBeAvailable( + timeUs - streamOffsetUs, checkNotNull(outputFormat)); + } return true; } @@ -894,6 +905,9 @@ public void handleMessage(@MessageType int messageType, @Nullable Object message videoSink.setVideoFrameMetadataListener( (VideoFrameMetadataListener) checkNotNull(message)); break; + case Renderer.MSG_SET_IMAGE_METADATA_LISTENER: + imageMetadataListener = (ImageMetadataListener) message; + break; default: super.handleMessage(messageType, message); } @@ -1085,6 +1099,9 @@ private static final class HardwareBufferImageRenderer extends ImageRenderer { private MediaSource.@MonotonicNonNull MediaPeriodId mediaPeriodId; private long streamStartPositionUs; private long offsetToCompositionTimeUs; + @Nullable private ImageMetadataListener imageMetadataListener; + private @MonotonicNonNull Format outputFormat; + private long streamOffsetUs; HardwareBufferImageRenderer( ImageDecoder.Factory imageDecoderFactory, @@ -1096,6 +1113,16 @@ private static final class HardwareBufferImageRenderer extends ImageRenderer { streamStartPositionUs = C.TIME_UNSET; } + @Override + public void handleMessage(@Renderer.MessageType int messageType, @Nullable Object message) + throws ExoPlaybackException { + if (messageType == Renderer.MSG_SET_IMAGE_METADATA_LISTENER) { + imageMetadataListener = (ImageMetadataListener) message; + return; + } + super.handleMessage(messageType, message); + } + // ImageRenderer methods @Override @@ -1125,6 +1152,7 @@ protected void onStreamChanged( offsetToCompositionTimeUs = getOffsetToCompositionTimeUs(getTimeline(), mediaPeriodId, offsetUs); timestampIterator = createTimestampIterator(/* positionUs= */ startPositionUs); + streamOffsetUs = offsetUs; super.onStreamChanged(formats, startPositionUs, offsetUs, mediaPeriodId); } @@ -1153,6 +1181,22 @@ protected boolean processOutputBuffer( if (isLastInSequence(getTimeline(), checkNotNull(mediaPeriodId))) { hardwareBufferFrameReader.queueEndOfStream(); } + if (imageMetadataListener != null) { + if (outputFormat == null + || outputFormat.width != outputImage.getWidth() + || outputFormat.height != outputImage.getHeight()) { + outputFormat = + new Format.Builder() + .setSampleMimeType(MimeTypes.IMAGE_RAW) + .setWidth(outputImage.getWidth()) + .setHeight(outputImage.getHeight()) + .setColorInfo(ColorInfo.SRGB_BT709_FULL) + .setFrameRate(DEFAULT_FRAME_RATE) + .build(); + } + checkNotNull(imageMetadataListener) + .onImageAboutToBeAvailable(timeUs - streamOffsetUs, outputFormat); + } return true; } diff --git a/libraries/ui/build.gradle b/libraries/ui/build.gradle index 3f50b69d3c1..2a265fdaace 100644 --- a/libraries/ui/build.gradle +++ b/libraries/ui/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android.buildTypes.debug.testCoverageEnabled true diff --git a/libraries/ui/src/main/res/layout/exo_player_control_view.xml b/libraries/ui/src/main/res/layout/exo_player_control_view.xml index 6fa397964ce..8d8c9f303b2 100644 --- a/libraries/ui/src/main/res/layout/exo_player_control_view.xml +++ b/libraries/ui/src/main/res/layout/exo_player_control_view.xml @@ -41,7 +41,8 @@ android:paddingLeft="@dimen/exo_styled_bottom_bar_time_padding" android:paddingRight="@dimen/exo_styled_bottom_bar_time_padding" android:layout_gravity="center_vertical|start" - android:layoutDirection="ltr"> + android:layoutDirection="ltr" + android:importantForAccessibility="noHideDescendants"> diff --git a/libraries/ui/src/main/res/values-b+sr+Latn/strings.xml b/libraries/ui/src/main/res/values-b+sr+Latn/strings.xml index 1f7efab29b8..0935a0d1ad6 100644 --- a/libraries/ui/src/main/res/values-b+sr+Latn/strings.xml +++ b/libraries/ui/src/main/res/values-b+sr+Latn/strings.xml @@ -8,8 +8,8 @@ Prikaži dodatna podešavanja Pređi na ceo ekran Izađi iz celog ekrana - Prethodna - Sledeće + Nazad + Dalje Pauziraj Pusti Zaustavi diff --git a/libraries/ui/src/main/res/values-sr/strings.xml b/libraries/ui/src/main/res/values-sr/strings.xml index 50eb292e4cd..a02a5917f10 100644 --- a/libraries/ui/src/main/res/values-sr/strings.xml +++ b/libraries/ui/src/main/res/values-sr/strings.xml @@ -8,8 +8,8 @@ Прикажи додатна подешавања Пређи на цео екран Изађи из целог екрана - Претходна - Следеће + Назад + Даље Паузирај Пусти Заустави diff --git a/libraries/ui_compose/build.gradle b/libraries/ui_compose/build.gradle index 68abcba07e4..5ca5217b749 100644 --- a/libraries/ui_compose/build.gradle +++ b/libraries/ui_compose/build.gradle @@ -14,7 +14,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget -apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" +apply plugin: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" apply plugin: 'kotlin-android' apply plugin: 'org.jetbrains.kotlin.plugin.compose' diff --git a/libraries/ui_compose_material3/build.gradle b/libraries/ui_compose_material3/build.gradle index fccafba9e5d..57a0cbdec54 100644 --- a/libraries/ui_compose_material3/build.gradle +++ b/libraries/ui_compose_material3/build.gradle @@ -14,7 +14,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget -apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" +apply plugin: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" apply plugin: 'kotlin-android' apply plugin: 'org.jetbrains.kotlin.plugin.compose' diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/Player.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/Player.kt index a8926cca02b..0ab4ec0a918 100644 --- a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/Player.kt +++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/Player.kt @@ -17,8 +17,6 @@ package androidx.media3.ui.compose.material3 import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxWidth @@ -27,22 +25,43 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.media3.common.Player +import androidx.media3.common.util.ExperimentalApi import androidx.media3.common.util.UnstableApi import androidx.media3.ui.compose.ContentFrame import androidx.media3.ui.compose.SURFACE_TYPE_SURFACE_VIEW import androidx.media3.ui.compose.SurfaceType -import kotlinx.coroutines.delay + +/** + * A composable that provides a basic player UI layout, combining a [ContentFrame] for displaying + * player content with customizable controls and a shutter. + * + * This composable consists of a [ContentFrame] that handles the rendering of the player's video, + * overlaid with some default controls and a shutter. + * + * @param player The [Player] instance to be controlled and whose content is displayed. + * @param modifier The [Modifier] to be applied to the outer [Box]. + */ +@UnstableApi +@Composable +fun Player(player: Player?, modifier: Modifier = Modifier) { + Player( + player, + modifier, + surfaceType = SURFACE_TYPE_SURFACE_VIEW, + contentScale = ContentScale.Fit, + keepContentOnReset = false, + shutter = { PlayerDefaults.Shutter() }, + showControls = true, + topControls = defaultTopControls, + centerControls = defaultCenterControls, + bottomControls = defaultBottomControls, + ) +} /** * A composable that provides a basic player UI layout, combining a [ContentFrame] for displaying @@ -59,14 +78,15 @@ import kotlinx.coroutines.delay * @param keepContentOnReset Whether to keep the content visible when the player is reset. * @param shutter A composable to be displayed as a shutter over the content. The default shutter is * a black [Box]. - * @param controlsTimeoutMs Delay amount in milliseconds for keeping the controls visible on every - * interaction with the screen. A non-positive value will cause the controls to remain visible - * indefinitely, unless visibility is toggled by clicking on the component. - * @param topControls A composable aligned with [Alignment.TopCenter], receiving the [player]. - * @param centerControls A composable aligned with [Alignment.Center], receiving the [player]. - * @param bottomControls A composable aligned with [Alignment.BottomCenter], receiving the [player]. + * @param showControls Whether the controls should be visible. + * @param topControls A composable aligned with [Alignment.TopCenter], receiving the [player] and + * [showControls]. + * @param centerControls A composable aligned with [Alignment.Center], receiving the [player] and + * [showControls]. + * @param bottomControls A composable aligned with [Alignment.BottomCenter], receiving the [player] + * and [showControls]. */ -@UnstableApi +@ExperimentalApi // TODO: b/490015547 - Move to stable/unstable @Composable fun Player( player: Player?, @@ -77,30 +97,12 @@ fun Player( shutter: @Composable () -> Unit = { PlayerDefaults.Shutter() }, // TODO: b/305035807 change to PlayerDefaults::Shutter with Kotlin 2.2 - controlsTimeoutMs: Long = 3000L, + showControls: Boolean = true, topControls: (@Composable BoxScope.(Player?, Boolean) -> Unit)? = defaultTopControls, centerControls: (@Composable BoxScope.(Player?, Boolean) -> Unit)? = defaultCenterControls, bottomControls: (@Composable BoxScope.(Player?, Boolean) -> Unit)? = defaultBottomControls, ) { - var showControls by remember { mutableStateOf(true) } - var anyPointerDown by remember { mutableStateOf(false) } // pointer pressed, interacting - - if (controlsTimeoutMs > 0) { - LaunchedEffect(showControls, anyPointerDown) { - // Timer only runs when controls are shown AND user is NOT currently touching the screen - if (showControls && !anyPointerDown) { - delay(controlsTimeoutMs) - showControls = false - } - } - } - - Box( - modifier.withControlsVisibility( - onToggleShowControls = { showControls = !showControls }, - onPressedPointerEvent = { anyPointerDown = it }, - ) - ) { + Box(modifier) { ContentFrame( player = player, surfaceType = surfaceType, @@ -117,28 +119,6 @@ fun Player( } } -@Composable -private fun Modifier.withControlsVisibility( - onToggleShowControls: () -> Unit, - onPressedPointerEvent: (Boolean) -> Unit, -): Modifier = - this.clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, // to prevent the ripple from the tap - ) { - onToggleShowControls() - } - .pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val event = awaitPointerEvent() - // Check if any pointers are currently down - // Useful to prevent recomposition mid-drag (pointer will remain pressed) - onPressedPointerEvent(event.changes.any { it.pressed }) - } - } - } - private val defaultTopControls: @Composable BoxScope.(player: Player?, showControls: Boolean) -> Unit = diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/indicator/ProgressSlider.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/indicator/ProgressSlider.kt index 96fa1688ea5..7597d73d1bd 100644 --- a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/indicator/ProgressSlider.kt +++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/indicator/ProgressSlider.kt @@ -31,10 +31,30 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.media3.common.Player +import androidx.media3.common.util.ExperimentalApi import androidx.media3.common.util.UnstableApi import androidx.media3.ui.compose.indicators.ProgressIndicator import kotlinx.coroutines.CoroutineScope +/** + * A Material3 [Slider] that displays the current position of the player. + * + * @param player The [Player] to get the progress from. + * @param modifier The [Modifier] to be applied to the slider. + */ +@UnstableApi +@Composable +fun ProgressSlider(player: Player?, modifier: Modifier = Modifier) { + ProgressSlider( + player, + modifier, + onValueChangeFinished = null, + scope = rememberCoroutineScope(), + colors = SliderDefaults.colors(), + interactionSource = remember { MutableInteractionSource() }, + ) +} + /** * A Material3 [Slider] that displays the current position of the player. * @@ -55,7 +75,7 @@ import kotlinx.coroutines.CoroutineScope * create and pass in your own `remember`ed instance to observe `Interactions` and customize the * appearance / behavior of this slider in different states. */ -@UnstableApi +@ExperimentalApi // TODO: b/490019662 - Move to stable/unstable @Composable fun ProgressSlider( player: Player?, diff --git a/libraries/ui_compose_material3/src/main/res/values-b+sr+Latn/strings.xml b/libraries/ui_compose_material3/src/main/res/values-b+sr+Latn/strings.xml index 7c060bce15e..98c702124e1 100755 --- a/libraries/ui_compose_material3/src/main/res/values-b+sr+Latn/strings.xml +++ b/libraries/ui_compose_material3/src/main/res/values-b+sr+Latn/strings.xml @@ -2,8 +2,8 @@ Pusti Pauziraj - Sledeće - Prethodno + Dalje + Nazad Premotaj unazad Premotajte %d sekundu unazad diff --git a/libraries/ui_compose_material3/src/main/res/values-sr/strings.xml b/libraries/ui_compose_material3/src/main/res/values-sr/strings.xml index 03d4d0fe4f0..d54b7b9b555 100755 --- a/libraries/ui_compose_material3/src/main/res/values-sr/strings.xml +++ b/libraries/ui_compose_material3/src/main/res/values-sr/strings.xml @@ -2,8 +2,8 @@ Пусти Паузирај - Следеће - Претходно + Даље + Назад Премотај уназад Премотајте %d секунду уназад diff --git a/libraries/ui_compose_material3/src/test/java/androidx/media3/ui/compose/material3/PlayerTest.kt b/libraries/ui_compose_material3/src/test/java/androidx/media3/ui/compose/material3/PlayerTest.kt index 8b513f58e67..e89a52b0b04 100644 --- a/libraries/ui_compose_material3/src/test/java/androidx/media3/ui/compose/material3/PlayerTest.kt +++ b/libraries/ui_compose_material3/src/test/java/androidx/media3/ui/compose/material3/PlayerTest.kt @@ -17,14 +17,16 @@ package androidx.media3.ui.compose.material3 import android.content.Context +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.assertIsDisplayed @@ -33,7 +35,6 @@ import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.unit.dp import androidx.media3.test.utils.FakePlayer import androidx.test.core.app.ApplicationProvider @@ -70,8 +71,8 @@ class PlayerTest { ) @Test - fun player_controlsVisibleInitially() { - composeTestRule.setContent { Player(player = FakePlayer()) } + fun player_controlsVisible() { + composeTestRule.setContent { Player(player = FakePlayer(), showControls = true) } findTopControls().assertIsDisplayed() findCenterControls().assertIsDisplayed() @@ -79,20 +80,8 @@ class PlayerTest { } @Test - fun player_controlsHideAfterTimeout() { - composeTestRule.setContent { Player(player = FakePlayer(), controlsTimeoutMs = 3000) } - - findTopControls().assertIsDisplayed() - findCenterControls().assertIsDisplayed() - findBottomControls().assertIsDisplayed() - - composeTestRule.mainClock.advanceTimeBy(500) - - findTopControls().assertIsDisplayed() - findCenterControls().assertIsDisplayed() - findBottomControls().assertIsDisplayed() - - composeTestRule.mainClock.advanceTimeBy(2600) + fun player_controlsHidden() { + composeTestRule.setContent { Player(player = FakePlayer(), showControls = false) } findTopControls().assertDoesNotExist() findCenterControls().assertDoesNotExist() @@ -102,10 +91,11 @@ class PlayerTest { @Test fun player_tapShowsHiddenControls() { composeTestRule.setContent { + var showControls by remember { mutableStateOf(false) } Player( player = FakePlayer(), - modifier = Modifier.testTag(playerTestTag), - controlsTimeoutMs = 3000, + showControls = showControls, + modifier = Modifier.testTag(playerTestTag).clickable { showControls = !showControls }, ) } composeTestRule.mainClock.advanceTimeBy(3100) @@ -120,46 +110,6 @@ class PlayerTest { findBottomControls().assertIsDisplayed() } - @Test - fun player_interactionPreventsHiding() { - val timeoutMs = 1000L - composeTestRule.setContent { - Player( - player = FakePlayer(), - modifier = Modifier.testTag(playerTestTag), - controlsTimeoutMs = timeoutMs, - ) - } - - composeTestRule.onNodeWithTag(playerTestTag).performTouchInput { down(Offset(0f, 0f)) } - composeTestRule.mainClock.advanceTimeBy(timeoutMs + 500) - - findTopControls().assertIsDisplayed() - findCenterControls().assertIsDisplayed() - findBottomControls().assertIsDisplayed() - - composeTestRule.onNodeWithTag(playerTestTag).performTouchInput { up() } - composeTestRule.mainClock.advanceTimeBy(timeoutMs + 500) - - findTopControls().assertDoesNotExist() - findCenterControls().assertDoesNotExist() - findBottomControls().assertDoesNotExist() - } - - @Test - fun player_controlsTimeoutZero_controlsAlwaysVisible() { - composeTestRule.setContent { Player(player = FakePlayer(), controlsTimeoutMs = 0L) } - findTopControls().assertIsDisplayed() - findCenterControls().assertIsDisplayed() - findBottomControls().assertIsDisplayed() - - composeTestRule.mainClock.advanceTimeBy(5000) - - findTopControls().assertIsDisplayed() - findCenterControls().assertIsDisplayed() - findBottomControls().assertIsDisplayed() - } - @Test fun player_customControls_areDisplayedAndAlignedCorrectly() { val player = FakePlayer() @@ -169,7 +119,6 @@ class PlayerTest { Player( player, Modifier.testTag(playerTestTag), - controlsTimeoutMs = 1000000, topControls = { _, _ -> Box(Modifier.testTag("topControls")) { BasicText("Top") } }, centerControls = { _, _ -> Box(Modifier.testTag("centerControls")) { BasicText("Center") } diff --git a/libraries/ui_leanback/build.gradle b/libraries/ui_leanback/build.gradle index 9626b20a381..4f905c9fe44 100644 --- a/libraries/ui_leanback/build.gradle +++ b/libraries/ui_leanback/build.gradle @@ -11,7 +11,8 @@ // 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: 'com.android.library' +apply from: "$gradle.ext.androidxMediaSettingsDir/common_config.gradle" android { namespace 'androidx.media3.ui.leanback'