diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index 25383cd8dd..0000000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: Bug report -about: Issue template for a bug report. -title: '' -labels: bug, needs triage -assignees: '' ---- - -We can only process bug reports that are actionable. Unclear bug reports or -reports with insufficient information may not get attention. - -Before filing a bug: -------------------------- - -- Search existing issues, including issues that are closed: - https://github.com/androidx/media/issues?q=is%3Aissue -- For ExoPlayer-related bugs, please also check the ExoPlayer tracker: - https://github.com/google/ExoPlayer/issues?q=is%3Aissue - -When reporting a bug: -------------------------- - -Describe how the issue can be reproduced, ideally using one of the demo apps -or a small sample app that you’re able to share as source code on GitHub. To -increase the chance of your issue getting attention, please also include: - -- Clear reproduction steps including observed and expected behavior -- Output of running "adb bugreport" in the console shortly after encountering - the issue -- URI to test content for reproduction -- For protected content: - - DRM scheme and license server URL - - Authentication HTTP headers - -- AndroidX Media version number -- Android version -- Android device - -If there's something you don't want to post publicly, please submit the issue, -then email the link/bug report to dev.exoplayer@gmail.com using a subject in the -format "Issue #1234", where #1234 is your issue number (we don't reply to -emails). diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000..41d4528ced --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,99 @@ +name: Bug Report +description: Report a bug in the Media3 library +labels: ["bug", "needs triage"] +body: + - type: markdown + attributes: + value: | + We can only process bug reports that are actionable. Unclear bug reports or reports with insufficient information may not get attention. + + Before filing a bug: + ------------------------- + + - Search existing issues, including issues that are closed: https://github.com/androidx/media/issues?q=is%3Aissue + - For ExoPlayer-related bugs, please also check the ExoPlayer tracker: https://github.com/google/ExoPlayer/issues?q=is%3Aissue + - type: dropdown + attributes: + label: Media3 Version + description: What version of Media3 are you using? + options: + - 1.0.0-alpha03 + - 1.0.0-alpha02 + - 1.0.0-alpha01 + validations: + required: true + - type: textarea + attributes: + label: Devices that reproduce the issue + placeholder: | + Example: + * Pixel 4 running Android 12 + * Samsung S21 running Android 11 + validations: + required: true + - type: textarea + attributes: + label: Devices that do not reproduce the issue + placeholder: | + Example: + * Pixel 3 running Android Pie + - type: dropdown + attributes: + label: Reproducible in the demo app? + description: Please try and reproduce the issue in the [Media3 demo app](https://github.com/androidx/media/tree/release/demos/main). + options: + - "Yes" + - "No" + - Not tested + validations: + required: true + - type: textarea + attributes: + label: Reproduction steps + description: Clear and complete steps we can use to reproduce the problem + placeholder: | + Example: + 1. Play the attached media in the demo app + 2. Seek forward 10s + validations: + required: true + - type: textarea + attributes: + label: Expected result + placeholder: | + Example: + The media plays successfully + validations: + required: true + - type: textarea + attributes: + label: Actual result + placeholder: | + Example: + Playback crashes with the following stack trace: + ... + validations: + required: true + - type: textarea + attributes: + label: Media + description: | + Media we can use to reproduce the problem. Either: + * Attach a file here + * Include a media URL + * Refer to a piece of media from the demo app (e.g. `Misc > Dizzy (MP4)`) + * If you don't want to post media publicly please email the info to dev.exoplayer@gmail.com with subject 'Issue #\' after filing this issue, and note that you will do this here. + * If you are certain the issue does not depend on the media being played, enter "Not applicable" here. + + For DRM-protected media please also include the scheme and license server URL. + validations: + required: true + - type: checkboxes + attributes: + label: Bug Report + description: | + After filing this issue please run `adb bugreport` shortly after reproducing the problem (ideally in the [demo app](https://github.com/androidx/media/tree/release/demos/main)) to capture a zip file, and email this to dev.exoplayer@gmail.com with subject 'Issue #\'. + + **Note:** Logcat output is **not** the same as a full bug report, and is often missing information that's useful for diagnosing issues. Please ensure you're sending a full bug report zip file. + options: + - label: You will email the zip file produced by `adb bugreport` to dev.exoplayer@gmail.com after filing this issue. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..3ba13e0cec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/README.md b/README.md index cf07b97ad8..7ea16a8f42 100644 --- a/README.md +++ b/README.md @@ -94,15 +94,13 @@ to prevent build errors. Cloning the repository and depending on the modules locally is required when using some libraries. It's also a suitable approach if you want to make local -changes, or if you want to use the main branch. +changes, or if you want to use the `main` branch. -First, clone the repository into a local directory and checkout the desired -branch: +First, clone the repository into a local directory: ```sh git clone https://github.com/androidx/media.git cd media -git checkout main ``` Next, add the following to your project's `settings.gradle` file, replacing @@ -129,7 +127,7 @@ implementation project(':media-lib-ui') Development work happens on the `main` branch. Pull requests should normally be made to this branch. -We plan to add a release branch soon. +The `release` branch holds the most recent stable release. #### Using Android Studio diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3fa5013eae..4e0e3c5aeb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,398 @@ # Release notes +### Unreleased changes + +* Core library: + * Enable support for Android platform diagnostics via + `MediaMetricsManager`. ExoPlayer will forward playback events and + performance data to the platform, which helps to provide system + performance and debugging information on the device. This data may also + be collected by Google + [if sharing usage and diagnostics data is enabled](https://support.google.com/accounts/answer/6078260) + by the user of the device. Apps can opt-out of contributing to platform + diagnostics for ExoPlayer with + `ExoPlayer.Builder.setUsePlatformDiagnostics(false)`. +* Track selection: + * Flatten `TrackSelectionOverrides` class into `TrackSelectionParameters`, + and promote `TrackSelectionOverride` to a top level class. +* Video: + * Rename `DummySurface` to `PlaceHolderSurface`. +* Audio: + * Use LG AC3 audio decoder advertising non-standard MIME type. +* Ad playback / IMA: + * Decrease ad polling rate from every 100ms to every 200ms, to line up with + Media Rating Council (MRC) recommendations. +* Extractors: + * Matroska: Parse `DiscardPadding` for Opus tracks. + * Parse bitrates from `esds` boxes. + * MP4: Parse initialization data from AV1 tracks. +* UI: + * Fix delivery of events to `OnClickListener`s set on `PlayerView` and + `LegacyPlayerView`, in the case that `useController=false` + ([#9605](https://github.com/google/ExoPlayer/issues/9605)). Also fix + delivery of events to `OnLongClickListener` for all view configurations. + * Fix incorrectly treating a sequence of touch events that exit the bounds + of `PlayerView` and `LegacyPlayerView` before `ACTION_UP` as a click + ([#9861](https://github.com/google/ExoPlayer/issues/9861)). + * Fix `PlayerView` accessibility issue where it was not possible to + tapping would toggle playback rather than hiding the controls + ([#8627](https://github.com/google/ExoPlayer/issues/8627)). + * Rewrite `TrackSelectionView` and `TrackSelectionDialogBuilder` to work + with the `Player` interface rather than `ExoPlayer`. This allows the + views to be used with other `Player` implementations, and removes the + dependency from the UI module to the ExoPlayer module. This is a + breaking change. + * Don't show forced text tracks in the `PlayerView` track selector, and + keep a suitable forced text track selected if "None" is selected + ([#9432](https://github.com/google/ExoPlayer/issues/9432)). +* HLS: + * Fallback to chunkful preparation if the playlist CODECS attribute + does not contain the audio codec + ([#10065](https://github.com/google/ExoPlayer/issues/10065)). +* RTSP: + * Add RTP reader for MPEG4 + ([#35](https://github.com/androidx/media/pull/35)) + * Add RTP reader for HEVC + ([#36](https://github.com/androidx/media/pull/36)). + * Add RTP reader for AMR. Currently only mono-channel, non-interleaved + AMR streams are supported. Compound AMR RTP payload is not supported. + ([#46](https://github.com/androidx/media/pull/46)) + * Add RTP reader for VP8 + ([#47](https://github.com/androidx/media/pull/47)). + * Add RTP reader for WAV + ([#56](https://github.com/androidx/media/pull/56)). +* Data sources: + * Rename `DummyDataSource` to `PlaceHolderDataSource`. +* Remove deprecated symbols: + * Remove `Player.Listener.onTracksChanged`. Use + `Player.Listener.onTracksInfoChanged` instead. + * Remove `Player.getCurrentTrackGroups` and + `Player.getCurrentTrackSelections`. Use `Player.getCurrentTracksInfo` + instead. You can also continue to use `ExoPlayer.getCurrentTrackGroups` + and `ExoPlayer.getCurrentTrackSelections`, although these methods remain + deprecated. + * Remove `DownloadHelper` + `DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT` and + `DEFAULT_TRACK_SELECTOR_PARAMETERS` constants. Use + `getDefaultTrackSelectorParameters(Context)` instead when possible, and + `DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT` otherwise. + * FFmpeg extension: + * Update CMake version to `3.21.0+` to avoid a CMake bug causing + AndroidStudio's gradle sync to fail + ([#9933](https://github.com/google/ExoPlayer/issues/9933)). + +### 1.0.0-alpha03 (2022-03-14) + +This release corresponds to the +[ExoPlayer 2.17.1 release](https://github.com/google/ExoPlayer/releases/tag/r2.17.1). + +* Audio: + * Fix error checking audio capabilities for Dolby Atmos (E-AC3-JOC) in + HLS. +* Extractors: + * FMP4: Fix issue where emsg sample metadata could be output in the wrong + order for streams containing both v0 and v1 emsg atoms + ([#9996](https://github.com/google/ExoPlayer/issues/9996)). +* Text: + * Fix the interaction of `SingleSampleMediaSource.Factory.setTrackId` and + `MediaItem.SubtitleConfiguration.Builder.setId` to prioritise the + `SubtitleConfiguration` field and fall back to the `Factory` value if + it's not set + ([#10016](https://github.com/google/ExoPlayer/issues/10016)). +* Ad playback: + * Fix audio underruns between ad periods in live HLS SSAI streams. + +### 1.0.0-alpha02 (2022-03-02) + +This release corresponds to the +[ExoPlayer 2.17.0 release](https://github.com/google/ExoPlayer/releases/tag/r2.17.0). + +* Core Library: + * Add protected method `DefaultRenderersFactory.getCodecAdapterFactory()` + so that subclasses of `DefaultRenderersFactory` that override + `buildVideoRenderers()` or `buildAudioRenderers()` can access the codec + adapter factory and pass it to `MediaCodecRenderer` instances they + create. + * Propagate ICY header fields `name` and `genre` to + `MediaMetadata.station` and `MediaMetadata.genre` respectively so that + they reach the app via `Player.Listener.onMediaMetadataChanged()` + ([#9677](https://github.com/google/ExoPlayer/issues/9677)). + * Remove null keys from `DefaultHttpDataSource#getResponseHeaders`. + * Sleep and retry when creating a `MediaCodec` instance fails. This works + around an issue that occurs on some devices when switching a surface + from a secure codec to another codec + ([#8696](https://github.com/google/ExoPlayer/issues/8696)). + * Add `MediaCodecAdapter.getMetrics()` to allow users obtain metrics data + from `MediaCodec` + ([#9766](https://github.com/google/ExoPlayer/issues/9766)). + * Fix Maven dependency resolution + ([#8353](https://github.com/google/ExoPlayer/issues/8353)). + * Disable automatic speed adjustment for live streams that neither have + low-latency features nor a user request setting the speed + ([#9329](https://github.com/google/ExoPlayer/issues/9329)). + * Rename `DecoderCounters#inputBufferCount` to `queuedInputBufferCount`. + * Make `SimpleExoPlayer.renderers` private. Renderers can be accessed via + `ExoPlayer.getRenderer`. + * Updated some `AnalyticsListener.EventFlags` constant values to match + values in `Player.EventFlags`. + * Split `AnalyticsCollector` into an interface and default implementation + to allow it to be stripped by R8 if an app doesn't need it. +* Track selection: + * Support preferred video role flags in track selection + ([#9402](https://github.com/google/ExoPlayer/issues/9402)). + * Update video track selection logic to take preferred MIME types and role + flags into account when selecting multiple video tracks for adaptation + ([#9519](https://github.com/google/ExoPlayer/issues/9519)). + * Update video and audio track selection logic to only choose formats for + adaptive selections that have the same level of decoder and hardware + support ([#9565](https://github.com/google/ExoPlayer/issues/9565)). + * Update video track selection logic to prefer more efficient codecs if + multiple codecs are supported by primary, hardware-accelerated decoders + ([#4835](https://github.com/google/ExoPlayer/issues/4835)). + * Prefer audio content preferences (for example, the "default" audio track + or a track matching the system locale language) over technical track + selection constraints (for example, preferred MIME type, or maximum + channel count). + * Fix track selection issue where overriding one track group did not + disable other track groups of the same type + ([#9675](https://github.com/google/ExoPlayer/issues/9675)). + * Fix track selection issue where a mixture of non-empty and empty track + overrides is not applied correctly + ([#9649](https://github.com/google/ExoPlayer/issues/9649)). + * Prohibit duplicate `TrackGroup`s in a `TrackGroupArray`. `TrackGroup`s + can always be made distinguishable by setting an `id` in the + `TrackGroup` constructor. This fixes a crash when resuming playback + after backgrounding the app with an active track override + ([#9718](https://github.com/google/ExoPlayer/issues/9718)). + * Amend logic in `AdaptiveTrackSelection` to allow a quality increase + under sufficient network bandwidth even if playback is very close to the + live edge ([#9784](https://github.com/google/ExoPlayer/issues/9784)). +* Video: + * Fix decoder fallback logic for Dolby Vision to use a compatible + H264/H265 decoder if needed. +* Audio: + * Fix decoder fallback logic for Dolby Atmos (E-AC3-JOC) to use a + compatible E-AC3 decoder if needed. + * Change `AudioCapabilities` APIs to require passing explicitly + `AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES` instead of `null`. + * Allow customization of the `AudioTrack` buffer size calculation by + injecting an `AudioTrackBufferSizeProvider` to `DefaultAudioSink` + ([#8891](https://github.com/google/ExoPlayer/issues/8891)). + * Retry `AudioTrack` creation if the requested buffer size was > 1MB + ([#9712](https://github.com/google/ExoPlayer/issues/9712)). +* Extractors: + * WAV: Add support for RF64 streams + ([#9543](https://github.com/google/ExoPlayer/issues/9543)). + * Fix incorrect parsing of H.265 SPS NAL units + ([#9719](https://github.com/google/ExoPlayer/issues/9719)). + * Parse Vorbis Comments (including `METADATA_BLOCK_PICTURE`) in Ogg Opus + and Ogg Vorbis files. +* Text: + * Add a `MediaItem.SubtitleConfiguration.id` field which is propagated to + the `Format.id` field of the subtitle track created from the + configuration + ([#9673](https://github.com/google/ExoPlayer/issues/9673)). + * Add basic support for WebVTT subtitles in Matroska containers + ([#9886](https://github.com/google/ExoPlayer/issues/9886)). + * Prevent `Cea708Decoder` from reading more than the declared size of a + service block. +* DRM: + * Remove `playbackLooper` from `DrmSessionManager.(pre)acquireSession`. + When a `DrmSessionManager` is used by an app in a custom `MediaSource`, + the `playbackLooper` needs to be passed to `DrmSessionManager.setPlayer` + instead. +* Ad playback / IMA: + * Add support for + [IMA Dynamic Ad Insertion (DAI)](https://support.google.com/admanager/answer/6147120) + ([#8213](https://github.com/google/ExoPlayer/issues/8213)). + * Add a method to `AdPlaybackState` to allow resetting an ad group so that + it can be played again + ([#9615](https://github.com/google/ExoPlayer/issues/9615)). + * Enforce playback speed of 1.0 during ad playback + ([#9018](https://github.com/google/ExoPlayer/issues/9018)). + * Fix issue where an ad group that failed to load caused an immediate + playback reset + ([#9929](https://github.com/google/ExoPlayer/issues/9929)). +* UI: + * Fix the color of the numbers in `StyledPlayerView` rewind and + fastforward buttons when using certain themes + ([#9765](https://github.com/google/ExoPlayer/issues/9765)). + * Correctly translate playback speed strings + ([#9811](https://github.com/google/ExoPlayer/issues/9811)). +* DASH: + * Add parsed essential and supplemental properties to the `Representation` + ([#9579](https://github.com/google/ExoPlayer/issues/9579)). + * Support the `forced-subtitle` track role + ([#9727](https://github.com/google/ExoPlayer/issues/9727)). + * Stop interpreting the `main` track role as `C.SELECTION_FLAG_DEFAULT`. + * Fix base URL exclusion logic for manifests that do not declare the DVB + namespace ([#9856](https://github.com/google/ExoPlayer/issues/9856)). + * Support relative `MPD.Location` URLs + ([#9939](https://github.com/google/ExoPlayer/issues/9939)). +* HLS: + * Correctly populate `Format.label` for audio only HLS streams + ([#9608](https://github.com/google/ExoPlayer/issues/9608)). + * Use chunkless preparation by default to improve start up time. If your + renditions contain muxed closed-caption tracks that are **not** declared + in the master playlist, you should add them to the master playlist to be + available for playback, or turn off chunkless preparation with + `HlsMediaSource.Factory.setAllowChunklessPreparation(false)`. + * Support key-frame accurate seeking in HLS + ([#2882](https://github.com/google/ExoPlayer/issues/2882)). +* RTSP: + * Provide a client API to override the `SocketFactory` used for any server + connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)). + * Prefer DIGEST authentication method over BASIC if both are present + ([#9800](https://github.com/google/ExoPlayer/issues/9800)). + * Handle when RTSP track timing is not available + ([#9775](https://github.com/google/ExoPlayer/issues/9775)). + * Ignore invalid RTP-Info header values + ([#9619](https://github.com/google/ExoPlayer/issues/9619)). +* Transformer: + * Increase required min API version to 21. + * `TransformationException` is now used to describe errors that occur + during a transformation. + * Add `TransformationRequest` for specifying the transformation options. + * Allow multiple listeners to be registered. + * Fix Transformer being stuck when the codec output is partially read. + * Fix potential NPE in `Transformer.getProgress` when releasing the muxer + throws. + * Add a demo app for applying transformations. +* MediaSession extension: + * By default, `MediaSessionConnector` now clears the playlist on stop. + Apps that want the playlist to be retained can call + `setClearMediaItemsOnStop(false)` on the connector. +* Cast extension: + * Fix bug that prevented `CastPlayer` from calling `onIsPlayingChanged` + correctly ([#9792](https://github.com/google/ExoPlayer/issues/9792)). + * Support audio metadata including artwork with + `DefaultMediaItemConverter` + ([#9663](https://github.com/google/ExoPlayer/issues/9663)). +* FFmpeg extension: + * Make `build_ffmpeg.sh` depend on LLVM's bin utils instead of GNU's + ([#9933](https://github.com/google/ExoPlayer/issues/9933)). +* Android 12 compatibility: + * Upgrade the Cast extension to depend on + `com.google.android.gms:play-services-cast-framework:20.1.0`. Earlier + versions of `play-services-cast-framework` are not compatible with apps + targeting Android 12, and will crash with an `IllegalArgumentException` + when creating `PendingIntent`s + ([#9528](https://github.com/google/ExoPlayer/issues/9528)). +* Remove deprecated symbols: + * Remove `Player.EventLister`. Use `Player.Listener` instead. + * Remove `MediaSourceFactory.setDrmSessionManager`, + `MediaSourceFactory.setDrmHttpDataSourceFactory`, and + `MediaSourceFactory.setDrmUserAgent`. Use + `MediaSourceFactory.setDrmSessionManagerProvider` instead. + * Remove `MediaSourceFactory.setStreamKeys`. Use + `MediaItem.Builder.setStreamKeys` instead. + * Remove `MediaSourceFactory.createMediaSource(Uri)`. Use + `MediaSourceFactory.createMediaSource(MediaItem)` instead. + * Remove `setTag` from `DashMediaSource`, `HlsMediaSource` and + `SsMediaSource`. Use `MediaItem.Builder.setTag` instead. + * Remove `DashMediaSource.setLivePresentationDelayMs(long, boolean)`. Use + `MediaItem.Builder.setLiveConfiguration` and + `MediaItem.LiveConfiguration.Builder.setTargetOffsetMs` to override the + manifest, or `DashMediaSource.setFallbackTargetLiveOffsetMs` to provide + a fallback value. + * Remove `(Simple)ExoPlayer.setThrowsWhenUsingWrongThread`. Opting out of + the thread enforcement is no longer possible. + * Remove `ActionFile` and `ActionFileUpgradeUtil`. Use ExoPlayer 2.16.1 or + before to use `ActionFileUpgradeUtil` to merge legacy action files into + `DefaultDownloadIndex`. + * Remove `ProgressiveMediaSource.setExtractorsFactory`. Use + `ProgressiveMediaSource.Factory(DataSource.Factory, ExtractorsFactory)` + constructor instead. + * Remove `ProgressiveMediaSource.Factory.setTag` and + `ProgressiveMediaSource.Factory.setCustomCacheKey`. Use + `MediaItem.Builder.setTag` and `MediaItem.Builder.setCustomCacheKey` + instead. + * Remove `DefaultRenderersFactory(Context, @ExtensionRendererMode int)` + and `DefaultRenderersFactory(Context, @ExtensionRendererMode int, long)` + constructors. Use the `DefaultRenderersFactory(Context)` constructor, + `DefaultRenderersFactory.setExtensionRendererMode`, and + `DefaultRenderersFactory.setAllowedVideoJoiningTimeMs` instead. + * Remove all public `CronetDataSource` constructors. Use + `CronetDataSource.Factory` instead. +* Change the following `IntDefs` to `@Target(TYPE_USE)` only. This may break + the compilation of usages in Kotlin, which can be fixed by moving the + annotation to annotate the type (`Int`). + * `@AacAudioObjectType` + * `@Ac3Util.SyncFrameInfo.StreamType` + * `@AdLoadException.Type` + * `@AdtsExtractor.Flags` + * `@AmrExtractor.Flags` + * `@AspectRatioFrameLayout.ResizeMode` + * `@AudioFocusManager.PlayerCommand` + * `@AudioSink.SinkFormatSupport` + * `@BinarySearchSeeker.TimestampSearchResult.Type` + * `@BufferReplacementMode` + * `@C.BufferFlags` + * `@C.ColorRange` + * `@C.ColorSpace` + * `@C.ColorTransfer` + * `@C.CryptoMode` + * `@C.Encoding` + * `@C.PcmEncoding` + * `@C.Projection` + * `@C.SelectionReason` + * `@C.StereoMode` + * `@C.VideoOutputMode` + * `@CacheDataSource.Flags` + * `@CaptionStyleCompat.EdgeType` + * `@DataSpec.Flags` + * `@DataSpec.HttpMethods` + * `@DecoderDiscardReasons` + * `@DecoderReuseResult` + * `@DefaultAudioSink.OutputMode` + * `@DefaultDrmSessionManager.Mode` + * `@DefaultTrackSelector.SelectionEligibility` + * `@DefaultTsPayloadReaderFactory.Flags` + * `@EGLSurfaceTexture.SecureMode` + * `@EbmlProcessor.ElementType` + * `@ExoMediaDrm.KeyRequest.RequestType` + * `@ExtensionRendererMode` + * `@Extractor.ReadResult` + * `@FileTypes.Type` + * `@FlacExtractor.Flags` (in `com.google.android.exoplayer2.ext.flac` + package) + * `@FlacExtractor.Flags` (in + `com.google.android.exoplayer2.extractor.flac` package) + * `@FragmentedMp4Extractor.Flags` + * `@HlsMediaPlaylist.PlaylistType` + * `@HttpDataSourceException.Type` + * `@IllegalClippingException.Reason` + * `@IllegalMergeException.Reason` + * `@LoadErrorHandlingPolicy.FallbackType` + * `@MatroskaExtractor.Flags` + * `@Mp3Extractor.Flags` + * `@Mp4Extractor.Flags` + * `@NotificationUtil.Importance` + * `@PlaybackException.FieldNumber` + * `@PlayerNotificationManager.Priority` + * `@PlayerNotificationManager.Visibility` + * `@PlayerView.ShowBuffering` + * `@Renderer.State` + * `@RendererCapabilities.AdaptiveSupport` + * `@RendererCapabilities.Capabilities` + * `@RendererCapabilities.DecoderSupport` + * `@RendererCapabilities.FormatSupport` + * `@RendererCapabilities.HardwareAccelerationSupport` + * `@RendererCapabilities.TunnelingSupport` + * `@SampleStream.ReadDataResult` + * `@SampleStream.ReadFlags` + * `@StyledPlayerView.ShowBuffering` + * `@SubtitleView.ViewType` + * `@TextAnnotation.Position` + * `@TextEmphasisSpan.MarkFill` + * `@TextEmphasisSpan.MarkShape` + * `@Track.Transformation` + * `@TrackOutput.SampleDataPart` + * `@Transformer.ProgressState` + * `@TsExtractor.Mode` + * `@TsPayloadReader.Flags` + * `@WebvttCssStyle.FontSizeUnit` + ### 1.0.0-alpha01 AndroidX Media is the new home for media support libraries, including ExoPlayer. diff --git a/constants.gradle b/constants.gradle index 7805a8dfb9..d016c32571 100644 --- a/constants.gradle +++ b/constants.gradle @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. project.ext { - releaseVersion = '1.0.0-alpha01' - releaseVersionCode = 1000000 + releaseVersion = '1.0.0-alpha03' + releaseVersionCode = 1_000_000_0_03 minSdkVersion = 16 appTargetSdkVersion = 29 // Upgrading this requires [Internal ref: b/193254928] to be fixed, or some @@ -26,7 +26,7 @@ project.ext { // https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA guavaVersion = '31.0.1-android' mockitoVersion = '3.12.4' - robolectricVersion = '4.6.1' + robolectricVersion = '4.8-alpha-1' // Keep this in sync with Google's internal Checker Framework version. checkerframeworkVersion = '3.13.0' checkerframeworkCompatVersion = '2.5.5' diff --git a/core_settings.gradle b/core_settings.gradle index d8760a1709..baca421753 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -87,3 +87,7 @@ include modulePrefix + 'test-data' project(modulePrefix + 'test-data').projectDir = new File(rootDir, 'libraries/test_data') include modulePrefix + 'test-utils' project(modulePrefix + 'test-utils').projectDir = new File(rootDir, 'libraries/test_utils') +include modulePrefix + 'test-session-common' +project(modulePrefix + 'test-session-common').projectDir = new File(rootDir, 'libraries/test_session_common') +include modulePrefix + 'test-session-current' +project(modulePrefix + 'test-session-current').projectDir = new File(rootDir, 'libraries/test_session_current') diff --git a/demos/gl/src/main/java/androidx/media3/demo/gl/BitmapOverlayVideoProcessor.java b/demos/gl/src/main/java/androidx/media3/demo/gl/BitmapOverlayVideoProcessor.java index 33e44323a3..a9329b5343 100644 --- a/demos/gl/src/main/java/androidx/media3/demo/gl/BitmapOverlayVideoProcessor.java +++ b/demos/gl/src/main/java/androidx/media3/demo/gl/BitmapOverlayVideoProcessor.java @@ -119,8 +119,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Run the shader program. GlProgram program = checkNotNull(this.program); - program.setSamplerTexIdUniform("uTexSampler0", frameTexture, /* unit= */ 0); - program.setSamplerTexIdUniform("uTexSampler1", textures[0], /* unit= */ 1); + program.setSamplerTexIdUniform("uTexSampler0", frameTexture, /* texUnitIndex= */ 0); + program.setSamplerTexIdUniform("uTexSampler1", textures[0], /* texUnitIndex= */ 1); program.setFloatUniform("uScaleX", bitmapScaleX); program.setFloatUniform("uScaleY", bitmapScaleY); program.setFloatsUniform("uTexTransform", transformMatrix); diff --git a/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java b/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java index 9a4e5c5dc3..bb43d08ced 100644 --- a/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java +++ b/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java @@ -32,7 +32,6 @@ import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.DefaultHttpDataSource; -import androidx.media3.datasource.HttpDataSource; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.dash.DashMediaSource; import androidx.media3.exoplayer.drm.DefaultDrmSessionManager; @@ -144,7 +143,7 @@ public final class MainActivity extends Activity { String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); - HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory(); + DataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory(); HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); drmSessionManager = diff --git a/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java b/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java index 21078d8545..c14bfc4ba9 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java @@ -20,6 +20,7 @@ import static androidx.media3.demo.main.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_I import android.app.Notification; import android.content.Context; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.media3.common.util.NotificationUtil; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.offline.Download; @@ -32,6 +33,7 @@ import androidx.media3.exoplayer.scheduler.Scheduler; import java.util.List; /** A service for downloading media. */ +@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public class DemoDownloadService extends DownloadService { private static final int JOB_ID = 1; diff --git a/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java b/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java index 2e3424697a..2a5a4bfbbe 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java @@ -16,12 +16,12 @@ package androidx.media3.demo.main; import android.content.Context; +import androidx.annotation.OptIn; import androidx.media3.database.DatabaseProvider; import androidx.media3.database.StandaloneDatabaseProvider; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.DefaultHttpDataSource; -import androidx.media3.datasource.HttpDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.NoOpCacheEvictor; @@ -59,7 +59,7 @@ public final class DemoUtil { private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; private static DataSource.@MonotonicNonNull Factory dataSourceFactory; - private static HttpDataSource.@MonotonicNonNull Factory httpDataSourceFactory; + private static DataSource.@MonotonicNonNull Factory httpDataSourceFactory; private static @MonotonicNonNull DatabaseProvider databaseProvider; private static @MonotonicNonNull File downloadDirectory; private static @MonotonicNonNull Cache downloadCache; @@ -72,6 +72,7 @@ public final class DemoUtil { return BuildConfig.USE_DECODER_EXTENSIONS; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public static RenderersFactory buildRenderersFactory( Context context, boolean preferExtensionRenderer) { @DefaultRenderersFactory.ExtensionRendererMode @@ -85,7 +86,7 @@ public final class DemoUtil { .setExtensionRendererMode(extensionRendererMode); } - public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) { + public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) { if (httpDataSourceFactory == null) { if (USE_CRONET_FOR_NETWORKING) { context = context.getApplicationContext(); @@ -117,6 +118,7 @@ public final class DemoUtil { return dataSourceFactory; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public static synchronized DownloadNotificationHelper getDownloadNotificationHelper( Context context) { if (downloadNotificationHelper == null) { @@ -136,6 +138,7 @@ public final class DemoUtil { return downloadTracker; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static synchronized Cache getDownloadCache(Context context) { if (downloadCache == null) { File downloadContentDirectory = @@ -147,6 +150,7 @@ public final class DemoUtil { return downloadCache; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static synchronized void ensureDownloadManagerInitialized(Context context) { if (downloadManager == null) { downloadManager = @@ -161,6 +165,7 @@ public final class DemoUtil { } } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static synchronized DatabaseProvider getDatabaseProvider(Context context) { if (databaseProvider == null) { databaseProvider = new StandaloneDatabaseProvider(context); @@ -178,6 +183,7 @@ public final class DemoUtil { return downloadDirectory; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static CacheDataSource.Factory buildReadOnlyCacheDataSource( DataSource.Factory upstreamFactory, Cache cache) { return new CacheDataSource.Factory() diff --git a/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java b/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java index a27991519c..a303815125 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java @@ -15,8 +15,7 @@ */ package androidx.media3.demo.main; -import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static com.google.common.base.Preconditions.checkNotNull; import android.content.Context; import android.content.DialogInterface; @@ -24,16 +23,18 @@ import android.net.Uri; import android.os.AsyncTask; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.annotation.RequiresApi; import androidx.fragment.app.FragmentManager; import androidx.media3.common.DrmInitData; import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; +import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.TracksInfo; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; -import androidx.media3.datasource.HttpDataSource; +import androidx.media3.datasource.DataSource; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.drm.DrmSession; import androidx.media3.exoplayer.drm.DrmSessionEventListener; @@ -46,13 +47,14 @@ import androidx.media3.exoplayer.offline.DownloadIndex; import androidx.media3.exoplayer.offline.DownloadManager; import androidx.media3.exoplayer.offline.DownloadRequest; import androidx.media3.exoplayer.offline.DownloadService; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo; import java.io.IOException; import java.util.HashMap; import java.util.concurrent.CopyOnWriteArraySet; /** Tracks media that has been downloaded. */ +@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public class DownloadTracker { /** Listens for changes in the tracked downloads. */ @@ -65,31 +67,26 @@ public class DownloadTracker { private static final String TAG = "DownloadTracker"; private final Context context; - private final HttpDataSource.Factory httpDataSourceFactory; + private final DataSource.Factory dataSourceFactory; private final CopyOnWriteArraySet listeners; private final HashMap downloads; private final DownloadIndex downloadIndex; - private final DefaultTrackSelector.Parameters trackSelectorParameters; @Nullable private StartDownloadDialogHelper startDownloadDialogHelper; public DownloadTracker( - Context context, - HttpDataSource.Factory httpDataSourceFactory, - DownloadManager downloadManager) { + Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) { this.context = context.getApplicationContext(); - this.httpDataSourceFactory = httpDataSourceFactory; + this.dataSourceFactory = dataSourceFactory; listeners = new CopyOnWriteArraySet<>(); downloads = new HashMap<>(); downloadIndex = downloadManager.getDownloadIndex(); - trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context); downloadManager.addListener(new DownloadManagerListener()); loadDownloads(); } public void addListener(Listener listener) { - checkNotNull(listener); - listeners.add(listener); + listeners.add(checkNotNull(listener)); } public void removeListener(Listener listener) { @@ -120,8 +117,7 @@ public class DownloadTracker { startDownloadDialogHelper = new StartDownloadDialogHelper( fragmentManager, - DownloadHelper.forMediaItem( - context, mediaItem, renderersFactory, httpDataSourceFactory), + DownloadHelper.forMediaItem(context, mediaItem, renderersFactory, dataSourceFactory), mediaItem); } } @@ -159,7 +155,7 @@ public class DownloadTracker { private final class StartDownloadDialogHelper implements DownloadHelper.Callback, - DialogInterface.OnClickListener, + TrackSelectionDialog.TrackSelectionListener, DialogInterface.OnDismissListener { private final FragmentManager fragmentManager; @@ -167,7 +163,6 @@ public class DownloadTracker { private final MediaItem mediaItem; private TrackSelectionDialog trackSelectionDialog; - private MappedTrackInfo mappedTrackInfo; private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask; @Nullable private byte[] keySetId; @@ -220,7 +215,7 @@ public class DownloadTracker { new WidevineOfflineLicenseFetchTask( format, mediaItem.localConfiguration.drmConfiguration, - httpDataSourceFactory, + dataSourceFactory, /* dialogHelper= */ this, helper); widevineOfflineLicenseFetchTask.execute(); @@ -237,21 +232,13 @@ public class DownloadTracker { Log.e(TAG, logMessage, e); } - // DialogInterface.OnClickListener implementation. + // TrackSelectionListener implementation. @Override - public void onClick(DialogInterface dialog, int which) { + public void onTracksSelected(TrackSelectionParameters trackSelectionParameters) { for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) { downloadHelper.clearTrackSelections(periodIndex); - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) { - downloadHelper.addTrackSelectionForSingleRenderer( - periodIndex, - /* rendererIndex= */ i, - trackSelectorParameters, - trackSelectionDialog.getOverrides(/* rendererIndex= */ i)); - } - } + downloadHelper.addTrackSelection(periodIndex, trackSelectionParameters); } DownloadRequest downloadRequest = buildDownloadRequest(); if (downloadRequest.streamKeys.isEmpty()) { @@ -316,21 +303,21 @@ public class DownloadTracker { return; } - mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); - if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { + TracksInfo tracksInfo = downloadHelper.getTracksInfo(/* periodIndex= */ 0); + if (!TrackSelectionDialog.willHaveContent(tracksInfo)) { Log.d(TAG, "No dialog content. Downloading entire stream."); startDownload(); downloadHelper.release(); return; } trackSelectionDialog = - TrackSelectionDialog.createForMappedTrackInfoAndParameters( + TrackSelectionDialog.createForTracksInfoAndParameters( /* titleId= */ R.string.exo_download_description, - mappedTrackInfo, - trackSelectorParameters, + tracksInfo, + DownloadHelper.getDefaultTrackSelectorParameters(context), /* allowAdaptiveSelections= */ false, /* allowMultipleOverrides= */ true, - /* onClickListener= */ this, + /* onTracksSelectedListener= */ this, /* onDismissListener= */ this); trackSelectionDialog.show(fragmentManager, /* tag= */ null); } @@ -371,7 +358,7 @@ public class DownloadTracker { private final Format format; private final MediaItem.DrmConfiguration drmConfiguration; - private final HttpDataSource.Factory httpDataSourceFactory; + private final DataSource.Factory dataSourceFactory; private final StartDownloadDialogHelper dialogHelper; private final DownloadHelper downloadHelper; @@ -381,12 +368,12 @@ public class DownloadTracker { public WidevineOfflineLicenseFetchTask( Format format, MediaItem.DrmConfiguration drmConfiguration, - HttpDataSource.Factory httpDataSourceFactory, + DataSource.Factory dataSourceFactory, StartDownloadDialogHelper dialogHelper, DownloadHelper downloadHelper) { this.format = format; this.drmConfiguration = drmConfiguration; - this.httpDataSourceFactory = httpDataSourceFactory; + this.dataSourceFactory = dataSourceFactory; this.dialogHelper = dialogHelper; this.downloadHelper = downloadHelper; } @@ -397,7 +384,7 @@ public class DownloadTracker { OfflineLicenseHelper.newWidevineInstance( drmConfiguration.licenseUri.toString(), drmConfiguration.forceDefaultLicenseUri, - httpDataSourceFactory, + dataSourceFactory, drmConfiguration.licenseRequestHeaders, new DrmSessionEventListener.EventDispatcher()); try { @@ -415,7 +402,7 @@ public class DownloadTracker { if (drmSessionException != null) { dialogHelper.onOfflineLicenseFetchedError(drmSessionException); } else { - dialogHelper.onOfflineLicenseFetched(downloadHelper, checkStateNotNull(keySetId)); + dialogHelper.onOfflineLicenseFetched(downloadHelper, checkNotNull(keySetId)); } } } diff --git a/demos/main/src/main/java/androidx/media3/demo/main/IntentUtil.java b/demos/main/src/main/java/androidx/media3/demo/main/IntentUtil.java index 911dac1ff5..cf1fa329ec 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/IntentUtil.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/IntentUtil.java @@ -15,8 +15,9 @@ */ package androidx.media3.demo.main; -import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.common.util.Assertions.checkState; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import android.content.Intent; import android.net.Uri; @@ -26,7 +27,6 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem.ClippingConfiguration; import androidx.media3.common.MediaItem.SubtitleConfiguration; import androidx.media3.common.MediaMetadata; -import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; import java.util.ArrayList; @@ -86,7 +86,7 @@ public class IntentUtil { /** Populates the intent with the given list of {@link MediaItem media items}. */ public static void addToIntent(List mediaItems, Intent intent) { - Assertions.checkArgument(!mediaItems.isEmpty()); + checkArgument(!mediaItems.isEmpty()); if (mediaItems.size() == 1) { MediaItem mediaItem = mediaItems.get(0); MediaItem.LocalConfiguration localConfiguration = checkNotNull(mediaItem.localConfiguration); @@ -241,7 +241,7 @@ public class IntentUtil { drmConfiguration.forcedSessionTrackTypes; if (!forcedDrmSessionTrackTypes.isEmpty()) { // Only video and audio together are supported. - Assertions.checkState( + checkState( forcedDrmSessionTrackTypes.size() == 2 && forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_VIDEO) && forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_AUDIO)); diff --git a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java index 365bf5422c..c2ebd97480 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java @@ -17,6 +17,7 @@ package androidx.media3.demo.main; import android.content.Intent; import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; import android.util.Pair; import android.view.KeyEvent; @@ -27,6 +28,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; @@ -34,6 +36,7 @@ import androidx.media3.common.ErrorMessageProvider; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; +import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; @@ -48,7 +51,6 @@ import androidx.media3.exoplayer.offline.DownloadRequest; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.ads.AdsLoader; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.util.DebugTextViewHelper; import androidx.media3.exoplayer.util.EventLogger; import androidx.media3.ui.PlayerControlView; @@ -79,8 +81,7 @@ public class PlayerActivity extends AppCompatActivity private Button selectTracksButton; private DataSource.Factory dataSourceFactory; private List mediaItems; - private DefaultTrackSelector trackSelector; - private DefaultTrackSelector.Parameters trackSelectionParameters; + private TrackSelectionParameters trackSelectionParameters; private DebugTextViewHelper debugViewHelper; private TracksInfo lastSeenTracksInfo; private boolean startAutoPlay; @@ -113,9 +114,8 @@ public class PlayerActivity extends AppCompatActivity playerView.requestFocus(); if (savedInstanceState != null) { - // Restore as DefaultTrackSelector.Parameters in case ExoPlayer specific parameters were set. trackSelectionParameters = - DefaultTrackSelector.Parameters.CREATOR.fromBundle( + TrackSelectionParameters.CREATOR.fromBundle( savedInstanceState.getBundle(KEY_TRACK_SELECTION_PARAMETERS)); startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY); startItemIndex = savedInstanceState.getInt(KEY_ITEM_INDEX); @@ -127,8 +127,7 @@ public class PlayerActivity extends AppCompatActivity adsLoaderStateBundle); } } else { - trackSelectionParameters = - new DefaultTrackSelector.ParametersBuilder(/* context= */ this).build(); + trackSelectionParameters = new TrackSelectionParameters.Builder(/* context= */ this).build(); clearStartPosition(); } } @@ -145,7 +144,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onStart() { super.onStart(); - if (Util.SDK_INT > 23) { + if (Build.VERSION.SDK_INT > 23) { initializePlayer(); if (playerView != null) { playerView.onResume(); @@ -156,7 +155,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onResume() { super.onResume(); - if (Util.SDK_INT <= 23 || player == null) { + if (Build.VERSION.SDK_INT <= 23 || player == null) { initializePlayer(); if (playerView != null) { playerView.onResume(); @@ -167,7 +166,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onPause() { super.onPause(); - if (Util.SDK_INT <= 23) { + if (Build.VERSION.SDK_INT <= 23) { if (playerView != null) { playerView.onPause(); } @@ -178,7 +177,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onStop() { super.onStop(); - if (Util.SDK_INT > 23) { + if (Build.VERSION.SDK_INT > 23) { if (playerView != null) { playerView.onPause(); } @@ -237,11 +236,11 @@ public class PlayerActivity extends AppCompatActivity public void onClick(View view) { if (view == selectTracksButton && !isShowingTrackSelectionDialog - && TrackSelectionDialog.willHaveContent(trackSelector)) { + && TrackSelectionDialog.willHaveContent(player)) { isShowingTrackSelectionDialog = true; TrackSelectionDialog trackSelectionDialog = - TrackSelectionDialog.createForTrackSelector( - trackSelector, + TrackSelectionDialog.createForPlayer( + player, /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false); trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null); } @@ -277,13 +276,11 @@ public class PlayerActivity extends AppCompatActivity RenderersFactory renderersFactory = DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders); - trackSelector = new DefaultTrackSelector(/* context= */ this); lastSeenTracksInfo = TracksInfo.EMPTY; player = new ExoPlayer.Builder(/* context= */ this) .setRenderersFactory(renderersFactory) .setMediaSourceFactory(createMediaSourceFactory()) - .setTrackSelector(trackSelector) .build(); player.setTrackSelectionParameters(trackSelectionParameters); player.addListener(new PlayerEventListener()); @@ -347,7 +344,7 @@ public class PlayerActivity extends AppCompatActivity MediaItem.DrmConfiguration drmConfiguration = mediaItem.localConfiguration.drmConfiguration; if (drmConfiguration != null) { - if (Util.SDK_INT < 18) { + if (Build.VERSION.SDK_INT < 18) { showToast(R.string.error_drm_unsupported_before_api_18); finish(); return Collections.emptyList(); @@ -400,10 +397,7 @@ public class PlayerActivity extends AppCompatActivity private void updateTrackSelectorParameters() { if (player != null) { - // Until the demo app is fully migrated to TrackSelectionParameters, rely on ExoPlayer to use - // DefaultTrackSelector by default. - trackSelectionParameters = - (DefaultTrackSelector.Parameters) player.getTrackSelectionParameters(); + trackSelectionParameters = player.getTrackSelectionParameters(); } } @@ -424,8 +418,7 @@ public class PlayerActivity extends AppCompatActivity // User controls private void updateButtonVisibility() { - selectTracksButton.setEnabled( - player != null && TrackSelectionDialog.willHaveContent(trackSelector)); + selectTracksButton.setEnabled(player != null && TrackSelectionDialog.willHaveContent(player)); } private void showControls() { @@ -517,29 +510,32 @@ public class PlayerActivity extends AppCompatActivity private static List createMediaItems(Intent intent, DownloadTracker downloadTracker) { List mediaItems = new ArrayList<>(); for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) { - @Nullable - DownloadRequest downloadRequest = - downloadTracker.getDownloadRequest(item.localConfiguration.uri); - if (downloadRequest != null) { - MediaItem.Builder builder = item.buildUpon(); - builder - .setMediaId(downloadRequest.id) - .setUri(downloadRequest.uri) - .setCustomCacheKey(downloadRequest.customCacheKey) - .setMimeType(downloadRequest.mimeType) - .setStreamKeys(downloadRequest.streamKeys); - @Nullable - MediaItem.DrmConfiguration drmConfiguration = item.localConfiguration.drmConfiguration; - if (drmConfiguration != null) { - builder.setDrmConfiguration( - drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build()); - } - - mediaItems.add(builder.build()); - } else { - mediaItems.add(item); - } + mediaItems.add( + maybeSetDownloadProperties( + item, downloadTracker.getDownloadRequest(item.localConfiguration.uri))); } return mediaItems; } + + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) + private static MediaItem maybeSetDownloadProperties( + MediaItem item, @Nullable DownloadRequest downloadRequest) { + if (downloadRequest == null) { + return item; + } + MediaItem.Builder builder = item.buildUpon(); + builder + .setMediaId(downloadRequest.id) + .setUri(downloadRequest.uri) + .setCustomCacheKey(downloadRequest.customCacheKey) + .setMimeType(downloadRequest.mimeType) + .setStreamKeys(downloadRequest.streamKeys); + @Nullable + MediaItem.DrmConfiguration drmConfiguration = item.localConfiguration.drmConfiguration; + if (drmConfiguration != null) { + builder.setDrmConfiguration( + drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build()); + } + return builder.build(); + } } diff --git a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java index 058fcec3b0..fa61a3ba44 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java @@ -15,9 +15,9 @@ */ package androidx.media3.demo.main; -import static androidx.media3.common.util.Assertions.checkArgument; -import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.common.util.Assertions.checkState; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import android.content.Context; import android.content.Intent; @@ -41,6 +41,7 @@ import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem.ClippingConfiguration; @@ -53,6 +54,7 @@ import androidx.media3.datasource.DataSourceUtil; import androidx.media3.datasource.DataSpec; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.offline.DownloadService; +import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; @@ -116,8 +118,12 @@ public class SampleChooserActivity extends AppCompatActivity useExtensionRenderers = DemoUtil.useExtensionRenderers(); downloadTracker = DemoUtil.getDownloadTracker(/* context= */ this); loadSample(); + startDownloadService(); + } - // Start the download service if it should be running but it's not currently. + /** Start the download service if it should be running but it's not currently. */ + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) + private void startDownloadService() { // Starting the service in the foreground causes notification flicker if there is no scheduled // action. Starting it in the background throws an exception if the app is in the background too // (e.g. if device screen is locked). @@ -271,6 +277,7 @@ public class SampleChooserActivity extends AppCompatActivity private boolean sawError; + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) @Override protected List doInBackground(String... uris) { List result = new ArrayList<>(); @@ -481,7 +488,7 @@ public class SampleChooserActivity extends AppCompatActivity private PlaylistGroup getGroup(String groupName, List groups) { for (int i = 0; i < groups.size(); i++) { - if (Util.areEqual(groupName, groups.get(i).title)) { + if (Objects.equal(groupName, groups.get(i).title)) { return groups.get(i); } } diff --git a/demos/main/src/main/java/androidx/media3/demo/main/TrackSelectionDialog.java b/demos/main/src/main/java/androidx/media3/demo/main/TrackSelectionDialog.java index d4df2a1745..8dbb003104 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/TrackSelectionDialog.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/TrackSelectionDialog.java @@ -31,21 +31,42 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.media3.common.C; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.util.Assertions; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride; -import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo; +import androidx.media3.common.Player; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.TrackSelectionOverride; +import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.TracksInfo; +import androidx.media3.common.TracksInfo.TrackGroupInfo; import androidx.media3.ui.TrackSelectionView; import androidx.viewpager.widget.ViewPager; import com.google.android.material.tabs.TabLayout; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** Dialog to select tracks. */ public final class TrackSelectionDialog extends DialogFragment { + /** Called when tracks are selected. */ + public interface TrackSelectionListener { + + /** + * Called when tracks are selected. + * + * @param trackSelectionParameters A {@link TrackSelectionParameters} representing the selected + * tracks. Any manual selections are defined by {@link + * TrackSelectionParameters#disabledTrackTypes} and {@link + * TrackSelectionParameters#overrides}. + */ + void onTracksSelected(TrackSelectionParameters trackSelectionParameters); + } + + public static final ImmutableList SUPPORTED_TRACK_TYPES = + ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT); + private final SparseArray tabFragments; private final ArrayList tabTrackTypes; @@ -55,20 +76,19 @@ public final class TrackSelectionDialog extends DialogFragment { /** * Returns whether a track selection dialog will have content to display if initialized with the - * specified {@link DefaultTrackSelector} in its current state. + * specified {@link Player}. */ - public static boolean willHaveContent(DefaultTrackSelector trackSelector) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - return mappedTrackInfo != null && willHaveContent(mappedTrackInfo); + public static boolean willHaveContent(Player player) { + return willHaveContent(player.getCurrentTracksInfo()); } /** * Returns whether a track selection dialog will have content to display if initialized with the - * specified {@link MappedTrackInfo}. + * specified {@link TracksInfo}. */ - public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) { - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - if (showTabForRenderer(mappedTrackInfo, i)) { + public static boolean willHaveContent(TracksInfo tracksInfo) { + for (TrackGroupInfo trackGroupInfo : tracksInfo.getTrackGroupInfos()) { + if (SUPPORTED_TRACK_TYPES.contains(trackGroupInfo.getTrackType())) { return true; } } @@ -76,78 +96,67 @@ public final class TrackSelectionDialog extends DialogFragment { } /** - * Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be - * automatically updated when tracks are selected. + * Creates a dialog for a given {@link Player}, whose parameters will be automatically updated + * when tracks are selected. * - * @param trackSelector The {@link DefaultTrackSelector}. + * @param player The {@link Player}. * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is * dismissed. */ - public static TrackSelectionDialog createForTrackSelector( - DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) { - MappedTrackInfo mappedTrackInfo = - Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); - TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); - DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); - trackSelectionDialog.init( - /* titleId= */ R.string.track_selection_title, - mappedTrackInfo, - /* initialParameters = */ parameters, + public static TrackSelectionDialog createForPlayer( + Player player, DialogInterface.OnDismissListener onDismissListener) { + return createForTracksInfoAndParameters( + R.string.track_selection_title, + player.getCurrentTracksInfo(), + player.getTrackSelectionParameters(), /* allowAdaptiveSelections= */ true, /* allowMultipleOverrides= */ false, - /* onClickListener= */ (dialog, which) -> { - DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon(); - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - builder - .clearSelectionOverrides(/* rendererIndex= */ i) - .setRendererDisabled( - /* rendererIndex= */ i, - trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)); - List overrides = - trackSelectionDialog.getOverrides(/* rendererIndex= */ i); - if (!overrides.isEmpty()) { - builder.setSelectionOverride( - /* rendererIndex= */ i, - mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i), - overrides.get(0)); - } - } - trackSelector.setParameters(builder); - }, + player::setTrackSelectionParameters, onDismissListener); - return trackSelectionDialog; } /** - * Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}. + * Creates a dialog for given {@link TracksInfo} and {@link TrackSelectionParameters}. * * @param titleId The resource id of the dialog title. - * @param mappedTrackInfo The {@link MappedTrackInfo} to display. - * @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial - * track selection. + * @param tracksInfo The {@link TracksInfo} describing the tracks to display. + * @param trackSelectionParameters The initial {@link TrackSelectionParameters}. * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track) * can be made. * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected. - * @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected. + * @param trackSelectionListener Called when tracks are selected. * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is * dismissed. */ - public static TrackSelectionDialog createForMappedTrackInfoAndParameters( + public static TrackSelectionDialog createForTracksInfoAndParameters( int titleId, - MappedTrackInfo mappedTrackInfo, - DefaultTrackSelector.Parameters initialParameters, + TracksInfo tracksInfo, + TrackSelectionParameters trackSelectionParameters, boolean allowAdaptiveSelections, boolean allowMultipleOverrides, - DialogInterface.OnClickListener onClickListener, + TrackSelectionListener trackSelectionListener, DialogInterface.OnDismissListener onDismissListener) { TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); trackSelectionDialog.init( + tracksInfo, + trackSelectionParameters, titleId, - mappedTrackInfo, - initialParameters, allowAdaptiveSelections, allowMultipleOverrides, - onClickListener, + /* onClickListener= */ (dialog, which) -> { + TrackSelectionParameters.Builder builder = trackSelectionParameters.buildUpon(); + for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) { + int trackType = SUPPORTED_TRACK_TYPES.get(i); + builder.setTrackTypeDisabled(trackType, trackSelectionDialog.getIsDisabled(trackType)); + builder.clearOverridesOfType(trackType); + Map overrides = + trackSelectionDialog.getOverrides(trackType); + for (TrackSelectionOverride override : overrides.values()) { + builder.addOverride(override); + } + } + trackSelectionListener.onTracksSelected(builder.build()); + }, onDismissListener); return trackSelectionDialog; } @@ -160,9 +169,9 @@ public final class TrackSelectionDialog extends DialogFragment { } private void init( + TracksInfo tracksInfo, + TrackSelectionParameters trackSelectionParameters, int titleId, - MappedTrackInfo mappedTrackInfo, - DefaultTrackSelector.Parameters initialParameters, boolean allowAdaptiveSelections, boolean allowMultipleOverrides, DialogInterface.OnClickListener onClickListener, @@ -170,45 +179,49 @@ public final class TrackSelectionDialog extends DialogFragment { this.titleId = titleId; this.onClickListener = onClickListener; this.onDismissListener = onDismissListener; - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - if (showTabForRenderer(mappedTrackInfo, i)) { - int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i); - TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); + + for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) { + @C.TrackType int trackType = SUPPORTED_TRACK_TYPES.get(i); + ArrayList trackGroupInfos = new ArrayList<>(); + for (TrackGroupInfo trackGroupInfo : tracksInfo.getTrackGroupInfos()) { + if (trackGroupInfo.getTrackType() == trackType) { + trackGroupInfos.add(trackGroupInfo); + } + } + if (!trackGroupInfos.isEmpty()) { TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment(); tabFragment.init( - mappedTrackInfo, - /* rendererIndex= */ i, - initialParameters.getRendererDisabled(/* rendererIndex= */ i), - initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray), + trackGroupInfos, + trackSelectionParameters.disabledTrackTypes.contains(trackType), + trackSelectionParameters.overrides, allowAdaptiveSelections, allowMultipleOverrides); - tabFragments.put(i, tabFragment); + tabFragments.put(trackType, tabFragment); tabTrackTypes.add(trackType); } } } /** - * Returns whether a renderer is disabled. + * Returns whether the disabled option is selected for the specified track type. * - * @param rendererIndex Renderer index. - * @return Whether the renderer is disabled. + * @param trackType The track type. + * @return Whether the disabled option is selected for the track type. */ - public boolean getIsDisabled(int rendererIndex) { - TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); - return rendererView != null && rendererView.isDisabled; + public boolean getIsDisabled(int trackType) { + TrackSelectionViewFragment trackView = tabFragments.get(trackType); + return trackView != null && trackView.isDisabled; } /** - * Returns the list of selected track selection overrides for the specified renderer. There will - * be at most one override for each track group. + * Returns the selected track overrides for the specified track type. * - * @param rendererIndex Renderer index. - * @return The list of track selection overrides for this renderer. + * @param trackType The track type. + * @return The track overrides for the track type. */ - public List getOverrides(int rendererIndex) { - TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); - return rendererView == null ? Collections.emptyList() : rendererView.overrides; + public Map getOverrides(int trackType) { + TrackSelectionViewFragment trackView = tabFragments.get(trackType); + return trackView == null ? Collections.emptyMap() : trackView.overrides; } @Override @@ -248,27 +261,7 @@ public final class TrackSelectionDialog extends DialogFragment { return dialogView; } - private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) { - TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); - if (trackGroupArray.length == 0) { - return false; - } - int trackType = mappedTrackInfo.getRendererType(rendererIndex); - return isSupportedTrackType(trackType); - } - - private static boolean isSupportedTrackType(int trackType) { - switch (trackType) { - case C.TRACK_TYPE_VIDEO: - case C.TRACK_TYPE_AUDIO: - case C.TRACK_TYPE_TEXT: - return true; - default: - return false; - } - } - - private static String getTrackTypeString(Resources resources, int trackType) { + private static String getTrackTypeString(Resources resources, @C.TrackType int trackType) { switch (trackType) { case C.TRACK_TYPE_VIDEO: return resources.getString(R.string.exo_track_selection_title_video); @@ -289,12 +282,12 @@ public final class TrackSelectionDialog extends DialogFragment { @Override public Fragment getItem(int position) { - return tabFragments.valueAt(position); + return tabFragments.get(tabTrackTypes.get(position)); } @Override public int getCount() { - return tabFragments.size(); + return tabTrackTypes.size(); } @Override @@ -307,13 +300,12 @@ public final class TrackSelectionDialog extends DialogFragment { public static final class TrackSelectionViewFragment extends Fragment implements TrackSelectionView.TrackSelectionListener { - private MappedTrackInfo mappedTrackInfo; - private int rendererIndex; + private List trackGroupInfos; private boolean allowAdaptiveSelections; private boolean allowMultipleOverrides; /* package */ boolean isDisabled; - /* package */ List overrides; + /* package */ Map overrides; public TrackSelectionViewFragment() { // Retain instance across activity re-creation to prevent losing access to init data. @@ -321,21 +313,21 @@ public final class TrackSelectionDialog extends DialogFragment { } public void init( - MappedTrackInfo mappedTrackInfo, - int rendererIndex, - boolean initialIsDisabled, - @Nullable SelectionOverride initialOverride, + List trackGroupInfos, + boolean isDisabled, + Map overrides, boolean allowAdaptiveSelections, boolean allowMultipleOverrides) { - this.mappedTrackInfo = mappedTrackInfo; - this.rendererIndex = rendererIndex; - this.isDisabled = initialIsDisabled; - this.overrides = - initialOverride == null - ? Collections.emptyList() - : Collections.singletonList(initialOverride); + this.trackGroupInfos = trackGroupInfos; + this.isDisabled = isDisabled; this.allowAdaptiveSelections = allowAdaptiveSelections; this.allowMultipleOverrides = allowMultipleOverrides; + // TrackSelectionView does this filtering internally, but we need to do it here as well to + // handle the case where the TrackSelectionView is never created. + this.overrides = + new HashMap<>( + TrackSelectionView.filterOverrides( + overrides, trackGroupInfos, allowMultipleOverrides)); } @Override @@ -351,8 +343,7 @@ public final class TrackSelectionDialog extends DialogFragment { trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); trackSelectionView.init( - mappedTrackInfo, - rendererIndex, + trackGroupInfos, isDisabled, overrides, /* trackFormatComparator= */ null, @@ -361,7 +352,8 @@ public final class TrackSelectionDialog extends DialogFragment { } @Override - public void onTrackSelectionChanged(boolean isDisabled, List overrides) { + public void onTrackSelectionChanged( + boolean isDisabled, Map overrides) { this.isDisabled = isDisabled; this.overrides = overrides; } diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlayableFolderActivity.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlayableFolderActivity.kt index 7a910fd867..f1c1631d45 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlayableFolderActivity.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlayableFolderActivity.kt @@ -30,6 +30,7 @@ import android.widget.ListView import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.media3.common.MediaItem +import androidx.media3.common.Player import androidx.media3.session.MediaBrowser import androidx.media3.session.SessionToken import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton @@ -179,6 +180,9 @@ class PlayableFolderActivity : AppCompatActivity() { returnConvertView.findViewById(R.id.add_button).setOnClickListener { val browser = this@PlayableFolderActivity.browser ?: return@setOnClickListener browser.addMediaItem(mediaItem) + if (browser.playbackState == Player.STATE_IDLE) { + browser.prepare() + } Snackbar.make( findViewById(R.id.linear_layout), getString(R.string.added_media_item_format, mediaItem.mediaMetadata.title), diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt index 6efe76bccc..b4ec76ee5a 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt @@ -96,7 +96,6 @@ class PlaybackService : MediaLibraryService() { val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() player.setMediaItem(item) - player.prepare() } override fun onSetMediaUri( diff --git a/demos/surface/src/main/java/androidx/media3/demo/surface/MainActivity.java b/demos/surface/src/main/java/androidx/media3/demo/surface/MainActivity.java index 649a38dd95..405f3b4df2 100644 --- a/demos/surface/src/main/java/androidx/media3/demo/surface/MainActivity.java +++ b/demos/surface/src/main/java/androidx/media3/demo/surface/MainActivity.java @@ -35,7 +35,6 @@ import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.DefaultHttpDataSource; -import androidx.media3.datasource.HttpDataSource; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.dash.DashMediaSource; import androidx.media3.exoplayer.drm.DefaultDrmSessionManager; @@ -189,7 +188,7 @@ public final class MainActivity extends Activity { String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); - HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory(); + DataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory(); HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); drmSessionManager = 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 b6a447768c..0245b9cc60 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 @@ -50,12 +50,11 @@ public final class ConfigurationActivity extends AppCompatActivity { public static final String AUDIO_MIME_TYPE = "audio_mime_type"; public static final String VIDEO_MIME_TYPE = "video_mime_type"; public static final String RESOLUTION_HEIGHT = "resolution_height"; - public static final String TRANSLATE_X = "translate_x"; - public static final String TRANSLATE_Y = "translate_y"; public static final String SCALE_X = "scale_x"; public static final String SCALE_Y = "scale_y"; public static final String ROTATE_DEGREES = "rotate_degrees"; public static final String ENABLE_FALLBACK = "enable_fallback"; + public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping"; public static final String ENABLE_HDR_EDITING = "enable_hdr_editing"; private static final String[] INPUT_URIS = { "https://html5demos.com/assets/dizzy.mp4", @@ -63,6 +62,11 @@ public final class ConfigurationActivity extends AppCompatActivity { "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4", "https://html5demos.com/assets/dizzy.webm", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-hdr-hdr10.mp4", }; private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS "MP4 with H264 video and AAC audio", @@ -70,6 +74,11 @@ public final class ConfigurationActivity extends AppCompatActivity { "Long MP4 with H264 video and AAC audio", "WebM with VP8 video and Vorbis audio", "4K 60fps MP4 with H264 video and AAC audio (portrait, timestamps always increase)", + "8k 24fps MP4 with H265 video and AAC audio", + "MP4 with H264 video and AAC audio (portrait, H > W, 0\u00B0)", + "MP4 with H264 video and AAC audio (portrait, H < W, 90\u00B0)", + "SEF slow motion with 240 fps", + "MP4 with HDR (HDR10) H265 video (encoding may fail)", }; private static final String SAME_AS_INPUT_OPTION = "same as input"; @@ -81,10 +90,10 @@ public final class ConfigurationActivity extends AppCompatActivity { private @MonotonicNonNull Spinner audioMimeSpinner; private @MonotonicNonNull Spinner videoMimeSpinner; private @MonotonicNonNull Spinner resolutionHeightSpinner; - private @MonotonicNonNull Spinner translateSpinner; private @MonotonicNonNull Spinner scaleSpinner; private @MonotonicNonNull Spinner rotateSpinner; private @MonotonicNonNull CheckBox enableFallbackCheckBox; + private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox; private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; private int inputUriPosition; @@ -136,14 +145,6 @@ public final class ConfigurationActivity extends AppCompatActivity { resolutionHeightAdapter.addAll( SAME_AS_INPUT_OPTION, "144", "240", "360", "480", "720", "1080", "1440", "2160"); - ArrayAdapter translateAdapter = - new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); - translateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - translateSpinner = findViewById(R.id.translate_spinner); - translateSpinner.setAdapter(translateAdapter); - translateAdapter.addAll( - SAME_AS_INPUT_OPTION, "-.1, -.1", "0, 0", ".5, 0", "0, .5", "1, 1", "1.9, 0", "0, 1.9"); - ArrayAdapter scaleAdapter = new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); @@ -159,6 +160,9 @@ public final class ConfigurationActivity extends AppCompatActivity { rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "60", "90", "180"); enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox); + enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox); + enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported()); + findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported()); enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox); } @@ -185,10 +189,10 @@ public final class ConfigurationActivity extends AppCompatActivity { "audioMimeSpinner", "videoMimeSpinner", "resolutionHeightSpinner", - "translateSpinner", "scaleSpinner", "rotateSpinner", "enableFallbackCheckBox", + "enableRequestSdrToneMappingCheckBox", "enableHdrEditingCheckBox" }) private void startTransformation(View view) { @@ -209,13 +213,6 @@ public final class ConfigurationActivity extends AppCompatActivity { if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) { bundle.putInt(RESOLUTION_HEIGHT, Integer.parseInt(selectedResolutionHeight)); } - String selectedTranslate = String.valueOf(translateSpinner.getSelectedItem()); - if (!SAME_AS_INPUT_OPTION.equals(selectedTranslate)) { - List translateXY = Arrays.asList(selectedTranslate.split(", ")); - checkState(translateXY.size() == 2); - bundle.putFloat(TRANSLATE_X, Float.parseFloat(translateXY.get(0))); - bundle.putFloat(TRANSLATE_Y, Float.parseFloat(translateXY.get(1))); - } String selectedScale = String.valueOf(scaleSpinner.getSelectedItem()); if (!SAME_AS_INPUT_OPTION.equals(selectedScale)) { List scaleXY = Arrays.asList(selectedScale.split(", ")); @@ -228,6 +225,8 @@ public final class ConfigurationActivity extends AppCompatActivity { bundle.putFloat(ROTATE_DEGREES, Float.parseFloat(selectedRotate)); } bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked()); + bundle.putBoolean( + ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked()); bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked()); transformerIntent.putExtras(bundle); @@ -258,9 +257,9 @@ public final class ConfigurationActivity extends AppCompatActivity { "audioMimeSpinner", "videoMimeSpinner", "resolutionHeightSpinner", - "translateSpinner", "scaleSpinner", "rotateSpinner", + "enableRequestSdrToneMappingCheckBox", "enableHdrEditingCheckBox" }) private void onRemoveAudio(View view) { @@ -277,9 +276,9 @@ public final class ConfigurationActivity extends AppCompatActivity { "audioMimeSpinner", "videoMimeSpinner", "resolutionHeightSpinner", - "translateSpinner", "scaleSpinner", "rotateSpinner", + "enableRequestSdrToneMappingCheckBox", "enableHdrEditingCheckBox" }) private void onRemoveVideo(View view) { @@ -295,26 +294,32 @@ public final class ConfigurationActivity extends AppCompatActivity { "audioMimeSpinner", "videoMimeSpinner", "resolutionHeightSpinner", - "translateSpinner", "scaleSpinner", "rotateSpinner", + "enableRequestSdrToneMappingCheckBox", "enableHdrEditingCheckBox" }) private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) { audioMimeSpinner.setEnabled(isAudioEnabled); videoMimeSpinner.setEnabled(isVideoEnabled); resolutionHeightSpinner.setEnabled(isVideoEnabled); - translateSpinner.setEnabled(isVideoEnabled); scaleSpinner.setEnabled(isVideoEnabled); rotateSpinner.setEnabled(isVideoEnabled); + enableRequestSdrToneMappingCheckBox.setEnabled( + isRequestSdrToneMappingSupported() && isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled); findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled); findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled); findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled); - findViewById(R.id.translate).setEnabled(isVideoEnabled); findViewById(R.id.scale).setEnabled(isVideoEnabled); findViewById(R.id.rotate).setEnabled(isVideoEnabled); + findViewById(R.id.request_sdr_tone_mapping) + .setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled); findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled); } + + private static boolean isRequestSdrToneMappingSupported() { + return Util.SDK_INT >= 31; + } } 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 4dc5c0585a..3a957cba3e 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 @@ -21,7 +21,6 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager; -import android.graphics.Matrix; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -217,10 +216,17 @@ public final class TransformerActivity extends AppCompatActivity { if (resolutionHeight != C.LENGTH_UNSET) { requestBuilder.setResolution(resolutionHeight); } - Matrix transformationMatrix = getTransformationMatrix(bundle); - if (!transformationMatrix.isIdentity()) { - requestBuilder.setTransformationMatrix(transformationMatrix); - } + + float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1); + float scaleY = bundle.getFloat(ConfigurationActivity.SCALE_Y, /* defaultValue= */ 1); + requestBuilder.setScale(scaleX, scaleY); + + float rotateDegrees = + bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0); + requestBuilder.setRotationDegrees(rotateDegrees); + + requestBuilder.setEnableRequestSdrToneMapping( + bundle.getBoolean(ConfigurationActivity.ENABLE_REQUEST_SDR_TONE_MAPPING)); requestBuilder.experimental_setEnableHdrEditing( bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING)); transformerBuilder @@ -251,27 +257,6 @@ public final class TransformerActivity extends AppCompatActivity { .build(); } - private static Matrix getTransformationMatrix(Bundle bundle) { - Matrix transformationMatrix = new Matrix(); - - float translateX = bundle.getFloat(ConfigurationActivity.TRANSLATE_X, /* defaultValue= */ 0); - float translateY = bundle.getFloat(ConfigurationActivity.TRANSLATE_Y, /* defaultValue= */ 0); - // TODO(b/201293185): Implement an AdvancedFrameEditor to handle translation, as the current - // transformationMatrix is automatically adjusted to focus on the original pixels and - // effectively undo translations. - transformationMatrix.postTranslate(translateX, translateY); - - float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1); - float scaleY = bundle.getFloat(ConfigurationActivity.SCALE_Y, /* defaultValue= */ 1); - transformationMatrix.postScale(scaleX, scaleY); - - float rotateDegrees = - bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0); - transformationMatrix.postRotate(rotateDegrees); - - return transformationMatrix; - } - @RequiresNonNull({ "informationTextView", "progressViewGroup", diff --git a/demos/transformer/src/main/res/layout/configuration_activity.xml b/demos/transformer/src/main/res/layout/configuration_activity.xml index f58f2f7167..c973bf4137 100644 --- a/demos/transformer/src/main/res/layout/configuration_activity.xml +++ b/demos/transformer/src/main/res/layout/configuration_activity.xml @@ -137,17 +137,6 @@ android:layout_gravity="right|center_vertical" android:gravity="right" /> - - - - @@ -180,6 +169,16 @@ android:layout_gravity="right" android:checked="true"/> + + + + diff --git a/demos/transformer/src/main/res/values/strings.xml b/demos/transformer/src/main/res/values/strings.xml index 3b27a515ef..c01a06bd3a 100644 --- a/demos/transformer/src/main/res/values/strings.xml +++ b/demos/transformer/src/main/res/values/strings.xml @@ -24,12 +24,12 @@ Output audio MIME type Output video MIME type Output video resolution - Translate video Scale video Rotate video (degrees) Enable fallback - Transform + Request SDR tone-mapping (API 31+) [Experimental] HDR editing + Transform Debug preview: No debug preview available. Transformation started diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 747bb13173..41d9927a4d 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1e62b66061..b1159fc54f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Mar 04 12:41:50 GMT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/gradlew b/gradlew index 91a7e269e1..1b6c787337 100755 --- a/gradlew +++ b/gradlew @@ -1,79 +1,129 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -82,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -90,75 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 8a0b282aa6..ac1b06f938 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,89 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java index d60a7452f7..192d8dbabe 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java @@ -34,17 +34,14 @@ import androidx.media3.common.DeviceInfo; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.MediaMetadata; -import androidx.media3.common.MimeTypes; import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelection; -import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; +import androidx.media3.common.TracksInfo.TrackGroupInfo; import androidx.media3.common.VideoSize; import androidx.media3.common.text.Cue; import androidx.media3.common.util.Assertions; @@ -68,7 +65,6 @@ import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import com.google.common.collect.ImmutableList; import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** @@ -115,13 +111,7 @@ public final class CastPlayer extends BasePlayer { private static final String TAG = "CastPlayer"; - private static final int RENDERER_COUNT = 3; - private static final int RENDERER_INDEX_VIDEO = 0; - private static final int RENDERER_INDEX_AUDIO = 1; - private static final int RENDERER_INDEX_TEXT = 2; private static final long PROGRESS_REPORT_PERIOD_MS = 1000; - private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY = - new TrackSelectionArray(null, null, null); private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0]; private final CastContext castContext; @@ -146,8 +136,6 @@ public final class CastPlayer extends BasePlayer { private final StateHolder playbackParameters; @Nullable private RemoteMediaClient remoteMediaClient; private CastTimeline currentTimeline; - private TrackGroupArray currentTrackGroups; - private TrackSelectionArray currentTrackSelection; private TracksInfo currentTracksInfo; private Commands availableCommands; private @Player.State int playbackState; @@ -224,8 +212,6 @@ public final class CastPlayer extends BasePlayer { playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT); playbackState = STATE_IDLE; currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; - currentTrackGroups = TrackGroupArray.EMPTY; - currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; currentTracksInfo = TracksInfo.EMPTY; availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build(); pendingSeekWindowIndex = C.INDEX_UNSET; @@ -557,16 +543,6 @@ public final class CastPlayer extends BasePlayer { return false; } - @Override - public TrackGroupArray getCurrentTrackGroups() { - return currentTrackGroups; - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - return currentTrackSelection; - } - @Override public TracksInfo getCurrentTracksInfo() { return currentTracksInfo; @@ -841,9 +817,6 @@ public final class CastPlayer extends BasePlayer { getCurrentMediaItem(), MEDIA_ITEM_TRANSITION_REASON_AUTO)); } if (updateTracksAndSelectionsAndNotifyIfChanged()) { - listeners.queueEvent( - Player.EVENT_TRACKS_CHANGED, - listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)); listeners.queueEvent( Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksInfoChanged(currentTracksInfo)); } @@ -1000,55 +973,33 @@ public final class CastPlayer extends BasePlayer { return false; } - MediaStatus mediaStatus = getMediaStatus(); - MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null; + @Nullable MediaStatus mediaStatus = getMediaStatus(); + @Nullable MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null; + @Nullable List castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null; if (castMediaTracks == null || castMediaTracks.isEmpty()) { - boolean hasChanged = !currentTrackGroups.isEmpty(); - currentTrackGroups = TrackGroupArray.EMPTY; - currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; + boolean hasChanged = !TracksInfo.EMPTY.equals(currentTracksInfo); currentTracksInfo = TracksInfo.EMPTY; return hasChanged; } - long[] activeTrackIds = mediaStatus.getActiveTrackIds(); + @Nullable long[] activeTrackIds = mediaStatus.getActiveTrackIds(); if (activeTrackIds == null) { activeTrackIds = EMPTY_TRACK_ID_ARRAY; } - TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()]; - @NullableType TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT]; - TracksInfo.TrackGroupInfo[] trackGroupInfos = - new TracksInfo.TrackGroupInfo[castMediaTracks.size()]; + TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[castMediaTracks.size()]; for (int i = 0; i < castMediaTracks.size(); i++) { MediaTrack mediaTrack = castMediaTracks.get(i); - trackGroups[i] = + TrackGroup trackGroup = new TrackGroup(/* id= */ Integer.toString(i), CastUtils.mediaTrackToFormat(mediaTrack)); - - long id = mediaTrack.getId(); - @C.TrackType int trackType = MimeTypes.getTrackType(mediaTrack.getContentType()); - int rendererIndex = getRendererIndexForTrackType(trackType); - boolean supported = rendererIndex != C.INDEX_UNSET; - boolean selected = - isTrackActive(id, activeTrackIds) && supported && trackSelections[rendererIndex] == null; - if (selected) { - trackSelections[rendererIndex] = new CastTrackSelection(trackGroups[i]); - } - @C.FormatSupport - int[] trackSupport = new int[] {supported ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_TYPE}; - final boolean[] trackSelected = new boolean[] {selected}; + @C.FormatSupport int[] trackSupport = new int[] {C.FORMAT_HANDLED}; + boolean[] trackSelected = new boolean[] {isTrackActive(mediaTrack.getId(), activeTrackIds)}; trackGroupInfos[i] = - new TracksInfo.TrackGroupInfo( - trackGroups[i], /* adaptiveSupported= */ false, trackSupport, trackSelected); + new TrackGroupInfo( + trackGroup, /* adaptiveSupported= */ false, trackSupport, trackSelected); } - TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups); - TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections); TracksInfo newTracksInfo = new TracksInfo(ImmutableList.copyOf(trackGroupInfos)); - - if (!newTrackGroups.equals(currentTrackGroups) - || !newTrackSelections.equals(currentTrackSelection) - || !newTracksInfo.equals(currentTracksInfo)) { - currentTrackSelection = newTrackSelections; - currentTrackGroups = newTrackGroups; + if (!newTracksInfo.equals(currentTracksInfo)) { currentTracksInfo = newTracksInfo; return true; } @@ -1307,14 +1258,6 @@ public final class CastPlayer extends BasePlayer { return false; } - private static int getRendererIndexForTrackType(@C.TrackType int trackType) { - return trackType == C.TRACK_TYPE_VIDEO - ? RENDERER_INDEX_VIDEO - : trackType == C.TRACK_TYPE_AUDIO - ? RENDERER_INDEX_AUDIO - : trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT : C.INDEX_UNSET; - } - private static int getCastRepeatMode(@RepeatMode int repeatMode) { switch (repeatMode) { case REPEAT_MODE_ONE: diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastTrackSelection.java b/libraries/cast/src/main/java/androidx/media3/cast/CastTrackSelection.java deleted file mode 100644 index fa4cdc16aa..0000000000 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastTrackSelection.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.cast; - -import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.Format; -import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackSelection; -import androidx.media3.common.util.Assertions; - -/** - * {@link TrackSelection} that only selects the first track of the provided {@link TrackGroup}. - * - *

This relies on {@link CastPlayer} track groups only having one track. - */ -/* package */ class CastTrackSelection implements TrackSelection { - - private final TrackGroup trackGroup; - - /** - * @param trackGroup The {@link TrackGroup} from which the first track will only be selected. - */ - public CastTrackSelection(TrackGroup trackGroup) { - this.trackGroup = trackGroup; - } - - @Override - public int getType() { - return TYPE_UNSET; - } - - @Override - public TrackGroup getTrackGroup() { - return trackGroup; - } - - @Override - public int length() { - return 1; - } - - @Override - public Format getFormat(int index) { - Assertions.checkArgument(index == 0); - return trackGroup.getFormat(0); - } - - @Override - public int getIndexInTrackGroup(int index) { - return index == 0 ? 0 : C.INDEX_UNSET; - } - - @Override - @SuppressWarnings("ReferenceEquality") - public int indexOf(Format format) { - return format == trackGroup.getFormat(0) ? 0 : C.INDEX_UNSET; - } - - @Override - public int indexOf(int indexInTrackGroup) { - return indexInTrackGroup == 0 ? 0 : C.INDEX_UNSET; - } - - // Object overrides. - - @Override - public int hashCode() { - return System.identityHashCode(trackGroup); - } - - // Track groups are compared by identity not value, as distinct groups may have the same value. - @Override - @SuppressWarnings({"ReferenceEquality", "EqualsGetClass"}) - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - CastTrackSelection other = (CastTrackSelection) obj; - return trackGroup == other.trackGroup; - } -} diff --git a/libraries/cast/src/test/java/androidx/media3/cast/CastTrackSelectionTest.java b/libraries/cast/src/test/java/androidx/media3/cast/CastTrackSelectionTest.java deleted file mode 100644 index 4acc05b88c..0000000000 --- a/libraries/cast/src/test/java/androidx/media3/cast/CastTrackSelectionTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2018 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.cast; - -import static com.google.common.truth.Truth.assertThat; - -import androidx.media3.common.C; -import androidx.media3.common.Format; -import androidx.media3.common.TrackGroup; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Test for {@link CastTrackSelection}. */ -@RunWith(AndroidJUnit4.class) -public class CastTrackSelectionTest { - - private static final TrackGroup TRACK_GROUP = - new TrackGroup(new Format.Builder().build(), new Format.Builder().build()); - - private static final CastTrackSelection SELECTION = new CastTrackSelection(TRACK_GROUP); - - @Test - public void length_isOne() { - assertThat(SELECTION.length()).isEqualTo(1); - } - - @Test - public void getTrackGroup_returnsSameGroup() { - assertThat(SELECTION.getTrackGroup()).isSameInstanceAs(TRACK_GROUP); - } - - @Test - public void getFormatSelectedTrack_isFirstTrack() { - assertThat(SELECTION.getFormat(0)).isSameInstanceAs(TRACK_GROUP.getFormat(0)); - } - - @Test - public void getIndexInTrackGroup_ofSelectedTrack_returnsFirstTrack() { - assertThat(SELECTION.getIndexInTrackGroup(0)).isEqualTo(0); - } - - @Test - public void getIndexInTrackGroup_onePastTheEnd_returnsIndexUnset() { - assertThat(SELECTION.getIndexInTrackGroup(1)).isEqualTo(C.INDEX_UNSET); - } - - @Test - public void indexOf_selectedTrack_returnsFirstTrack() { - assertThat(SELECTION.indexOf(0)).isEqualTo(0); - } - - @Test - public void indexOf_onePastTheEnd_returnsIndexUnset() { - assertThat(SELECTION.indexOf(1)).isEqualTo(C.INDEX_UNSET); - } - - @Test(expected = Exception.class) - public void getFormat_outOfBound_throws() { - CastTrackSelection selection = new CastTrackSelection(TRACK_GROUP); - - selection.getFormat(1); - } -} diff --git a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java index b713f32ef3..ba3caf1d56 100644 --- a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java +++ b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java @@ -827,6 +827,36 @@ public final class AdPlaybackState implements Bundleable { adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); } + /** + * Returns a copy of the ad playback state with the given ads ID. + * + * @param adsId The new ads ID. + * @param adPlaybackState The ad playback state to copy. + * @return The new ad playback state. + */ + public static AdPlaybackState fromAdPlaybackState(Object adsId, AdPlaybackState adPlaybackState) { + AdGroup[] adGroups = + new AdGroup[adPlaybackState.adGroupCount - adPlaybackState.removedAdGroupCount]; + for (int i = 0; i < adGroups.length; i++) { + AdGroup adGroup = adPlaybackState.adGroups[i]; + adGroups[i] = + new AdGroup( + adGroup.timeUs, + adGroup.count, + Arrays.copyOf(adGroup.states, adGroup.states.length), + Arrays.copyOf(adGroup.uris, adGroup.uris.length), + Arrays.copyOf(adGroup.durationsUs, adGroup.durationsUs.length), + adGroup.contentResumeOffsetUs, + adGroup.isServerSideInserted); + } + return new AdPlaybackState( + adsId, + adGroups, + adPlaybackState.adResumePositionUs, + adPlaybackState.contentDurationUs, + adPlaybackState.removedAdGroupCount); + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { diff --git a/libraries/common/src/main/java/androidx/media3/common/C.java b/libraries/common/src/main/java/androidx/media3/common/C.java index 649c648207..24d4801d5c 100644 --- a/libraries/common/src/main/java/androidx/media3/common/C.java +++ b/libraries/common/src/main/java/androidx/media3/common/C.java @@ -732,17 +732,17 @@ public final class C { @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) @IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_RTSP, TYPE_OTHER}) public @interface ContentType {} - /** Value returned by {@link Util#inferContentType(String)} for DASH manifests. */ + /** Value returned by {@link Util#inferContentType} for DASH manifests. */ @UnstableApi public static final int TYPE_DASH = 0; - /** Value returned by {@link Util#inferContentType(String)} for Smooth Streaming manifests. */ + /** Value returned by {@link Util#inferContentType} for Smooth Streaming manifests. */ @UnstableApi public static final int TYPE_SS = 1; - /** Value returned by {@link Util#inferContentType(String)} for HLS manifests. */ + /** Value returned by {@link Util#inferContentType} for HLS manifests. */ @UnstableApi public static final int TYPE_HLS = 2; - /** Value returned by {@link Util#inferContentType(String)} for RTSP. */ + /** Value returned by {@link Util#inferContentType} for RTSP. */ @UnstableApi public static final int TYPE_RTSP = 3; /** - * Value returned by {@link Util#inferContentType(String)} for files other than DASH, HLS or - * Smooth Streaming manifests, or RTSP URIs. + * Value returned by {@link Util#inferContentType} for files other than DASH, HLS or Smooth + * Streaming manifests, or RTSP URIs. */ @UnstableApi public static final int TYPE_OTHER = 4; diff --git a/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java b/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java index 5e00fa0663..44ef3e373f 100644 --- a/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java @@ -442,22 +442,6 @@ public class ForwardingPlayer implements Player { player.release(); } - /** Calls {@link Player#getCurrentTrackGroups()} on the delegate and returns the result. */ - @SuppressWarnings("deprecation") // Forwarding to deprecated method - @Deprecated - @Override - public TrackGroupArray getCurrentTrackGroups() { - return player.getCurrentTrackGroups(); - } - - /** Calls {@link Player#getCurrentTrackSelections()} on the delegate and returns the result. */ - @SuppressWarnings("deprecation") // Forwarding to deprecated method - @Deprecated - @Override - public TrackSelectionArray getCurrentTrackSelections() { - return player.getCurrentTrackSelections(); - } - /** Calls {@link Player#getCurrentTracksInfo()} on the delegate and returns the result. */ @Override public TracksInfo getCurrentTracksInfo() { @@ -846,12 +830,6 @@ public class ForwardingPlayer implements Player { listener.onMediaItemTransition(mediaItem, reason); } - @Override - @SuppressWarnings("deprecation") - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - listener.onTracksChanged(trackGroups, trackSelections); - } - @Override public void onTracksInfoChanged(TracksInfo tracksInfo) { listener.onTracksInfoChanged(tracksInfo); 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 0fe428c1d2..0cf954df4f 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java @@ -27,23 +27,27 @@ public final class MediaLibraryInfo { /** A tag to use when logging library information. */ public static final String TAG = "AndroidXMedia3"; - /** The version of the library expressed as a string, for example "1.2.3". */ + /** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "1.0.0-alpha01"; + public static final String VERSION = "1.0.0-alpha03"; /** The version of the library expressed as {@code TAG + "/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-alpha01"; + public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-alpha03"; /** - * The version of the library expressed as an integer, for example 1002003. + * The version of the library expressed as an integer, for example 1002003300. * - *

Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the - * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding - * integer version 123045006 (123-045-006). + *

Three digits are used for each of the first three components of {@link #VERSION}, then a + * single digit represents the cycle of this version: alpha (0), beta (1), rc (2) or stable (3). + * Finally two digits are used for the cycle number (always 00 for stable releases). + * + *

For example "1.2.3-alpha05" has the corresponding integer version 1002003005 + * (001-002-003-0-05), and "123.45.6" has the corresponding integer version 123045006300 + * (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 = 1000000; + public static final int VERSION_INT = 1_000_000_0_03; /** Whether the library was compiled with {@link Assertions} checks enabled. */ public static final boolean ASSERTIONS_ENABLED = true; diff --git a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java index 2c6973d1e1..49de1ef802 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java +++ b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java @@ -108,11 +108,10 @@ public final class MimeTypes { public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4"; public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; - @UnstableApi public static final String APPLICATION_MATROSKA = BASE_TYPE_APPLICATION + "/x-matroska"; public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + "/dash+xml"; - @UnstableApi public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; + public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + "/vnd.ms-sstr+xml"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608"; @@ -135,7 +134,7 @@ public final class MimeTypes { @UnstableApi public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; @UnstableApi public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy"; public static final String APPLICATION_AIT = BASE_TYPE_APPLICATION + "/vnd.dvb.ait"; - @UnstableApi public static final String APPLICATION_RTSP = BASE_TYPE_APPLICATION + "/x-rtsp"; + public static final String APPLICATION_RTSP = BASE_TYPE_APPLICATION + "/x-rtsp"; // image/ MIME types diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index 3f0a47f167..2828b53e43 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -670,24 +670,6 @@ public interface Player { default void onMediaItemTransition( @Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {} - /** - * Called when the available or selected tracks change. - * - *

{@link #onEvents(Player, Events)} will also be called to report this event along with - * other events that happen in the same {@link Looper} message queue iteration. - * - * @param trackGroups The available tracks. Never null, but may be of length zero. - * @param trackSelections The selected tracks. Never null, but may contain null elements. A - * concrete implementation may include null elements if it has a fixed number of renderer - * components, wishes to report a TrackSelection for each of them, and has one or more - * renderer components that is not assigned any selected tracks. - * @deprecated Use {@link #onTracksInfoChanged(TracksInfo)} instead. - */ - @UnstableApi - @Deprecated - default void onTracksChanged( - TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} - /** * Called when the available or selected tracks change. * @@ -701,11 +683,12 @@ public interface Player { /** * Called when the combined {@link MediaMetadata} changes. * - *

The provided {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata} - * and the static and dynamic metadata from the {@link TrackSelection#getFormat(int) track - * selections' formats} and {@link Listener#onMetadata(Metadata)}. If a field is populated in - * the {@link MediaItem#mediaMetadata}, it will be prioritised above the same field coming from - * static or dynamic metadata. + *

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

This method may be called multiple times in quick succession. * @@ -2105,33 +2088,9 @@ public interface Player { void release(); /** - * Returns the available track groups. + * Returns information about the current tracks. * - * @see Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray) - * @deprecated Use {@link #getCurrentTracksInfo()}. - */ - @UnstableApi - @Deprecated - TrackGroupArray getCurrentTrackGroups(); - - /** - * Returns the current track selections. - * - *

A concrete implementation may include null elements if it has a fixed number of renderer - * components, wishes to report a TrackSelection for each of them, and has one or more renderer - * components that is not assigned any selected tracks. - * - * @see Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray) - * @deprecated Use {@link #getCurrentTracksInfo()}. - */ - @UnstableApi - @Deprecated - TrackSelectionArray getCurrentTrackSelections(); - - /** - * Returns the available tracks, as well as the tracks' support, type, and selection status. - * - * @see Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray) + * @see Listener#onTracksInfoChanged(TracksInfo) */ TracksInfo getCurrentTracksInfo(); @@ -2165,11 +2124,11 @@ public interface Player { * Returns the current combined {@link MediaMetadata}, or {@link MediaMetadata#EMPTY} if not * supported. * - *

This {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata} and the - * static and dynamic metadata from the {@link TrackSelection#getFormat(int) track selections' - * formats} and {@link Listener#onMetadata(Metadata)}. If a field is populated in the {@link - * MediaItem#mediaMetadata}, it will be prioritised above the same field coming from static or - * dynamic metadata. + *

This {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata MediaItem + * metadata}, the static metadata in the media's {@link Format#metadata Format}, and any timed + * metadata that has been parsed from the media and output via {@link + * Listener#onMetadata(Metadata)}. If a field is populated in the {@link MediaItem#mediaMetadata}, + * it will be prioritised above the same field coming from static or timed metadata. */ MediaMetadata getMediaMetadata(); diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java b/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java index 8631aac4ab..ecca4c3151 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java @@ -34,17 +34,31 @@ import java.lang.annotation.Target; import java.util.Arrays; import java.util.List; -/** Defines an immutable group of tracks identified by their format identity. */ +/** + * An immutable group of tracks. All tracks in a group present the same content, but their formats + * may differ. + * + *

As an example of how tracks can be grouped, consider an adaptive playback where a main video + * feed is provided in five resolutions, and an alternative video feed (e.g., a different camera + * angle in a sports match) is provided in two resolutions. In this case there will be two video + * track groups, one corresponding to the main video feed containing five tracks, and a second for + * the alternative video feed containing two tracks. + * + *

Note that audio tracks whose languages differ are not grouped, because content in different + * languages is not considered to be the same. Conversely, audio tracks in the same language that + * only differ in properties such as bitrate, sampling rate, channel count and so on can be grouped. + * This also applies to text tracks. + */ public final class TrackGroup implements Bundleable { private static final String TAG = "TrackGroup"; /** The number of tracks in the group. */ - public final int length; + @UnstableApi public final int length; /** An identifier for the track group. */ - public final String id; + @UnstableApi public final String id; /** The type of tracks in the group. */ - public final @C.TrackType int type; + @UnstableApi public final @C.TrackType int type; private final Format[] formats; @@ -99,6 +113,7 @@ public final class TrackGroup implements Bundleable { * @param index The index of the track. * @return The track's format. */ + @UnstableApi public Format getFormat(int index) { return formats[index]; } @@ -112,6 +127,7 @@ public final class TrackGroup implements Bundleable { * @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists. */ @SuppressWarnings("ReferenceEquality") + @UnstableApi public int indexOf(Format format) { for (int i = 0; i < formats.length; i++) { if (format == formats[i]) { diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionOverride.java b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionOverride.java index 791f4f7082..e194c5eebf 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionOverride.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionOverride.java @@ -31,16 +31,21 @@ import java.lang.annotation.RetentionPolicy; import java.util.List; /** - * Forces the selection of {@link #trackIndices} for a {@link TrackGroup}. + * A track selection override, consisting of a {@link TrackGroup} and the indices of the tracks + * within the group that should be selected. * - *

If multiple tracks in {@link #trackGroup} are overridden, as many as possible will be selected - * depending on the player capabilities. + *

A track selection override is applied during playback if the media being played contains a + * {@link TrackGroup} equal to the one in the override. If a {@link TrackSelectionParameters} + * contains only one override of a given track type that applies to the media, this override will be + * used to control the track selection for that type. If multiple overrides of a given track type + * apply then the player will apply only one of them. * - *

If {@link #trackIndices} is empty, no tracks from {@link #trackGroup} will be played. This is - * similar to {@link TrackSelectionParameters#disabledTrackTypes}, except it will only affect the - * playback of the associated {@link TrackGroup}. For example, if the only {@link - * C#TRACK_TYPE_VIDEO} {@link TrackGroup} is associated with no tracks, no video will play until the - * next video starts. + *

If {@link #trackIndices} is empty then the override specifies that no tracks should be + * selected. Adding an empty override to a {@link TrackSelectionParameters} is similar to {@link + * TrackSelectionParameters.Builder#setTrackTypeDisabled disabling a track type}, except that an + * empty override will only be applied if the media being played contains a {@link TrackGroup} equal + * to the one in the override. Conversely, disabling a track type will prevent selection of tracks + * of that type for all media. */ public final class TrackSelectionOverride implements Bundleable { @@ -60,14 +65,14 @@ public final class TrackSelectionOverride implements Bundleable { private static final int FIELD_TRACK_GROUP = 0; private static final int FIELD_TRACKS = 1; - /** Constructs an instance to force all tracks in {@code trackGroup} to be selected. */ - public TrackSelectionOverride(TrackGroup trackGroup) { - this.trackGroup = trackGroup; - ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for (int i = 0; i < trackGroup.length; i++) { - builder.add(i); - } - this.trackIndices = builder.build(); + /** + * Constructs an instance to force {@code trackIndex} in {@code trackGroup} to be selected. + * + * @param trackGroup The {@link TrackGroup} for which to override the track selection. + * @param trackIndex The index of the track in the {@link TrackGroup} to select. + */ + public TrackSelectionOverride(TrackGroup trackGroup, int trackIndex) { + this(trackGroup, ImmutableList.of(trackIndex)); } /** @@ -123,13 +128,9 @@ public final class TrackSelectionOverride implements Bundleable { @UnstableApi public static final Creator CREATOR = bundle -> { - @Nullable Bundle trackGroupBundle = bundle.getBundle(keyForField(FIELD_TRACK_GROUP)); - checkNotNull(trackGroupBundle); // Mandatory as there are no reasonable defaults. + Bundle trackGroupBundle = checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP))); TrackGroup trackGroup = TrackGroup.CREATOR.fromBundle(trackGroupBundle); - @Nullable int[] tracks = bundle.getIntArray(keyForField(FIELD_TRACKS)); - if (tracks == null) { - return new TrackSelectionOverride(trackGroup); - } + int[] tracks = checkNotNull(bundle.getIntArray(keyForField(FIELD_TRACKS))); return new TrackSelectionOverride(trackGroup, Ints.asList(tracks)); }; diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java index c70376d4fd..f0c2b019c4 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java @@ -38,6 +38,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -46,10 +47,11 @@ import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** - * Constraint parameters for track selection. + * Parameters for controlling track selection. * - *

For example the following code modifies the parameters to restrict video track selections to - * SD, and to select a German audio track if there is one: + *

Parameters can be queried and set on a {@link Player}. For example the following code modifies + * the parameters to restrict video track selections to SD, and to select a German audio track if + * there is one: * *

{@code
  * // Build on the current parameters.
@@ -94,12 +96,13 @@ public class TrackSelectionParameters implements Bundleable {
     // Text
     private ImmutableList preferredTextLanguages;
     private @C.RoleFlags int preferredTextRoleFlags;
+    private @C.SelectionFlags int ignoredTextSelectionFlags;
     private boolean selectUndeterminedTextLanguage;
     // General
     private boolean forceLowestBitrate;
     private boolean forceHighestSupportedBitrate;
     private HashMap overrides;
-    private ImmutableSet<@C.TrackType Integer> disabledTrackTypes;
+    private HashSet<@C.TrackType Integer> disabledTrackTypes;
 
     /**
      * @deprecated {@link Context} constraints will not be set using this constructor. Use {@link
@@ -127,12 +130,13 @@ public class TrackSelectionParameters implements Bundleable {
       // Text
       preferredTextLanguages = ImmutableList.of();
       preferredTextRoleFlags = 0;
+      ignoredTextSelectionFlags = 0;
       selectUndeterminedTextLanguage = false;
       // General
       forceLowestBitrate = false;
       forceHighestSupportedBitrate = false;
       overrides = new HashMap<>();
-      disabledTrackTypes = ImmutableSet.of();
+      disabledTrackTypes = new HashSet<>();
     }
 
     /**
@@ -227,6 +231,10 @@ public class TrackSelectionParameters implements Bundleable {
           bundle.getInt(
               keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS),
               DEFAULT_WITHOUT_CONTEXT.preferredTextRoleFlags);
+      ignoredTextSelectionFlags =
+          bundle.getInt(
+              keyForField(FIELD_IGNORED_TEXT_SELECTION_FLAGS),
+              DEFAULT_WITHOUT_CONTEXT.ignoredTextSelectionFlags);
       selectUndeterminedTextLanguage =
           bundle.getBoolean(
               keyForField(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE),
@@ -239,21 +247,22 @@ public class TrackSelectionParameters implements Bundleable {
           bundle.getBoolean(
               keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE),
               DEFAULT_WITHOUT_CONTEXT.forceHighestSupportedBitrate);
-      overrides = new HashMap<>();
       List overrideList =
           fromBundleNullableList(
               TrackSelectionOverride.CREATOR,
               bundle.getParcelableArrayList(keyForField(FIELD_SELECTION_OVERRIDES)),
               ImmutableList.of());
+      overrides = new HashMap<>();
       for (int i = 0; i < overrideList.size(); i++) {
         TrackSelectionOverride override = overrideList.get(i);
         overrides.put(override.trackGroup, override);
       }
-      disabledTrackTypes =
-          ImmutableSet.copyOf(
-              Ints.asList(
-                  firstNonNull(
-                      bundle.getIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE)), new int[0])));
+      int[] disabledTrackTypeArray =
+          firstNonNull(bundle.getIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE)), new int[0]);
+      disabledTrackTypes = new HashSet<>();
+      for (@C.TrackType int disabledTrackType : disabledTrackTypeArray) {
+        disabledTrackTypes.add(disabledTrackType);
+      }
     }
 
     /** Overrides the value of the builder with the value of {@link TrackSelectionParameters}. */
@@ -289,11 +298,12 @@ public class TrackSelectionParameters implements Bundleable {
       // Text
       preferredTextLanguages = parameters.preferredTextLanguages;
       preferredTextRoleFlags = parameters.preferredTextRoleFlags;
+      ignoredTextSelectionFlags = parameters.ignoredTextSelectionFlags;
       selectUndeterminedTextLanguage = parameters.selectUndeterminedTextLanguage;
       // General
       forceLowestBitrate = parameters.forceLowestBitrate;
       forceHighestSupportedBitrate = parameters.forceHighestSupportedBitrate;
-      disabledTrackTypes = parameters.disabledTrackTypes;
+      disabledTrackTypes = new HashSet<>(parameters.disabledTrackTypes);
       overrides = new HashMap<>(parameters.overrides);
     }
 
@@ -612,6 +622,18 @@ public class TrackSelectionParameters implements Bundleable {
       return this;
     }
 
+    /**
+     * Sets a bitmask of selection flags that are ignored for text track selections.
+     *
+     * @param ignoredTextSelectionFlags A bitmask of {@link C.SelectionFlags} that are ignored for
+     *     text track selections.
+     * @return This builder.
+     */
+    public Builder setIgnoredTextSelectionFlags(@C.SelectionFlags int ignoredTextSelectionFlags) {
+      this.ignoredTextSelectionFlags = ignoredTextSelectionFlags;
+      return this;
+    }
+
     /**
      * Sets whether a text track with undetermined language should be selected if no track with
      * {@link #setPreferredTextLanguages(String...) a preferred language} is available, or if the
@@ -654,28 +676,26 @@ public class TrackSelectionParameters implements Bundleable {
       return this;
     }
 
-    /** Adds an override for the provided {@link TrackGroup}. */
+    /** Adds an override, replacing any override for the same {@link TrackGroup}. */
     public Builder addOverride(TrackSelectionOverride override) {
       overrides.put(override.trackGroup, override);
       return this;
     }
 
-    /** Removes the override associated with the provided {@link TrackGroup} if present. */
-    public Builder clearOverride(TrackGroup trackGroup) {
-      overrides.remove(trackGroup);
-      return this;
-    }
-
-    /** Set the override for the type of the provided {@link TrackGroup}. */
+    /** Sets an override, replacing all existing overrides with the same track type. */
     public Builder setOverrideForType(TrackSelectionOverride override) {
       clearOverridesOfType(override.getTrackType());
       overrides.put(override.trackGroup, override);
       return this;
     }
 
-    /**
-     * Remove any override associated with {@link TrackGroup TrackGroups} of type {@code trackType}.
-     */
+    /** Removes the override for the provided {@link TrackGroup}, if there is one. */
+    public Builder clearOverride(TrackGroup trackGroup) {
+      overrides.remove(trackGroup);
+      return this;
+    }
+
+    /** Removes all overrides of the provided track type. */
     public Builder clearOverridesOfType(@C.TrackType int trackType) {
       Iterator it = overrides.values().iterator();
       while (it.hasNext()) {
@@ -687,7 +707,7 @@ public class TrackSelectionParameters implements Bundleable {
       return this;
     }
 
-    /** Removes all track overrides. */
+    /** Removes all overrides. */
     public Builder clearOverrides() {
       overrides.clear();
       return this;
@@ -695,13 +715,34 @@ public class TrackSelectionParameters implements Bundleable {
 
     /**
      * Sets the disabled track types, preventing all tracks of those types from being selected for
-     * playback.
+     * playback. Any previously disabled track types are cleared.
      *
      * @param disabledTrackTypes The track types to disable.
      * @return This builder.
+     * @deprecated Use {@link #setTrackTypeDisabled(int, boolean)}.
      */
+    @Deprecated
+    @UnstableApi
     public Builder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) {
-      this.disabledTrackTypes = ImmutableSet.copyOf(disabledTrackTypes);
+      this.disabledTrackTypes.clear();
+      this.disabledTrackTypes.addAll(disabledTrackTypes);
+      return this;
+    }
+
+    /**
+     * Sets whether a track type is disabled. If disabled, no tracks of the specified type will be
+     * selected for playback.
+     *
+     * @param trackType The track type.
+     * @param disabled Whether the track type should be disabled.
+     * @return This builder.
+     */
+    public Builder setTrackTypeDisabled(@C.TrackType int trackType, boolean disabled) {
+      if (disabled) {
+        disabledTrackTypes.add(trackType);
+      } else {
+        disabledTrackTypes.remove(trackType);
+      }
       return this;
     }
 
@@ -878,6 +919,11 @@ public class TrackSelectionParameters implements Bundleable {
    * is enabled.
    */
   public final @C.RoleFlags int preferredTextRoleFlags;
+  /**
+   * Bitmask of selection flags that are ignored for text track selections. See {@link
+   * C.SelectionFlags}. The default value is {@code 0} (i.e., no flags are ignored).
+   */
+  public final @C.SelectionFlags int ignoredTextSelectionFlags;
   /**
    * Whether a text track with undetermined language should be selected if no track with {@link
    * #preferredTextLanguages} is available, or if {@link #preferredTextLanguages} is unset. The
@@ -931,12 +977,13 @@ public class TrackSelectionParameters implements Bundleable {
     // Text
     this.preferredTextLanguages = builder.preferredTextLanguages;
     this.preferredTextRoleFlags = builder.preferredTextRoleFlags;
+    this.ignoredTextSelectionFlags = builder.ignoredTextSelectionFlags;
     this.selectUndeterminedTextLanguage = builder.selectUndeterminedTextLanguage;
     // General
     this.forceLowestBitrate = builder.forceLowestBitrate;
     this.forceHighestSupportedBitrate = builder.forceHighestSupportedBitrate;
     this.overrides = ImmutableMap.copyOf(builder.overrides);
-    this.disabledTrackTypes = builder.disabledTrackTypes;
+    this.disabledTrackTypes = ImmutableSet.copyOf(builder.disabledTrackTypes);
   }
 
   /** Creates a new {@link Builder}, copying the initial values from this instance. */
@@ -974,8 +1021,10 @@ public class TrackSelectionParameters implements Bundleable {
         && maxAudioChannelCount == other.maxAudioChannelCount
         && maxAudioBitrate == other.maxAudioBitrate
         && preferredAudioMimeTypes.equals(other.preferredAudioMimeTypes)
+        // Text
         && preferredTextLanguages.equals(other.preferredTextLanguages)
         && preferredTextRoleFlags == other.preferredTextRoleFlags
+        && ignoredTextSelectionFlags == other.ignoredTextSelectionFlags
         && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage
         // General
         && forceLowestBitrate == other.forceLowestBitrate
@@ -1010,6 +1059,7 @@ public class TrackSelectionParameters implements Bundleable {
     // Text
     result = 31 * result + preferredTextLanguages.hashCode();
     result = 31 * result + preferredTextRoleFlags;
+    result = 31 * result + ignoredTextSelectionFlags;
     result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0);
     // General
     result = 31 * result + (forceLowestBitrate ? 1 : 0);
@@ -1024,11 +1074,7 @@ public class TrackSelectionParameters implements Bundleable {
   @Documented
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({
-    FIELD_PREFERRED_AUDIO_LANGUAGES,
-    FIELD_PREFERRED_AUDIO_ROLE_FLAGS,
-    FIELD_PREFERRED_TEXT_LANGUAGES,
-    FIELD_PREFERRED_TEXT_ROLE_FLAGS,
-    FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE,
+    // Video
     FIELD_MAX_VIDEO_WIDTH,
     FIELD_MAX_VIDEO_HEIGHT,
     FIELD_MAX_VIDEO_FRAMERATE,
@@ -1041,14 +1087,23 @@ public class TrackSelectionParameters implements Bundleable {
     FIELD_VIEWPORT_HEIGHT,
     FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE,
     FIELD_PREFERRED_VIDEO_MIMETYPES,
+    FIELD_PREFERRED_VIDEO_ROLE_FLAGS,
+    // Audio
+    FIELD_PREFERRED_AUDIO_LANGUAGES,
+    FIELD_PREFERRED_AUDIO_ROLE_FLAGS,
     FIELD_MAX_AUDIO_CHANNEL_COUNT,
     FIELD_MAX_AUDIO_BITRATE,
     FIELD_PREFERRED_AUDIO_MIME_TYPES,
+    // Text
+    FIELD_PREFERRED_TEXT_LANGUAGES,
+    FIELD_PREFERRED_TEXT_ROLE_FLAGS,
+    FIELD_IGNORED_TEXT_SELECTION_FLAGS,
+    FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE,
+    // General
     FIELD_FORCE_LOWEST_BITRATE,
     FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE,
     FIELD_SELECTION_OVERRIDES,
     FIELD_DISABLED_TRACK_TYPE,
-    FIELD_PREFERRED_VIDEO_ROLE_FLAGS
   })
   private @interface FieldNumber {}
 
@@ -1077,6 +1132,7 @@ public class TrackSelectionParameters implements Bundleable {
   private static final int FIELD_SELECTION_OVERRIDES = 23;
   private static final int FIELD_DISABLED_TRACK_TYPE = 24;
   private static final int FIELD_PREFERRED_VIDEO_ROLE_FLAGS = 25;
+  private static final int FIELD_IGNORED_TEXT_SELECTION_FLAGS = 26;
 
   @UnstableApi
   @Override
@@ -1114,6 +1170,7 @@ public class TrackSelectionParameters implements Bundleable {
     bundle.putStringArray(
         keyForField(FIELD_PREFERRED_TEXT_LANGUAGES), preferredTextLanguages.toArray(new String[0]));
     bundle.putInt(keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS), preferredTextRoleFlags);
+    bundle.putInt(keyForField(FIELD_IGNORED_TEXT_SELECTION_FLAGS), ignoredTextSelectionFlags);
     bundle.putBoolean(
         keyForField(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE), selectUndeterminedTextLanguage);
     // General
diff --git a/libraries/common/src/main/java/androidx/media3/common/TracksInfo.java b/libraries/common/src/main/java/androidx/media3/common/TracksInfo.java
index 793da2a831..4fd69d6407 100644
--- a/libraries/common/src/main/java/androidx/media3/common/TracksInfo.java
+++ b/libraries/common/src/main/java/androidx/media3/common/TracksInfo.java
@@ -41,8 +41,8 @@ public final class TracksInfo implements Bundleable {
 
   /**
    * Information about a single group of tracks, including the underlying {@link TrackGroup}, the
-   * {@link C.TrackType type} of tracks it contains, and the level to which each track is supported
-   * by the player.
+   * level to which each track is supported by the player, and whether any of the tracks are
+   * selected.
    */
   public static final class TrackGroupInfo implements Bundleable {
 
@@ -55,26 +55,26 @@ public final class TracksInfo implements Bundleable {
     private final boolean[] trackSelected;
 
     /**
-     * Constructs a TrackGroupInfo.
+     * Constructs an instance.
      *
-     * @param trackGroup The {@link TrackGroup} described.
-     * @param adaptiveSupported Whether adaptive selections containing more than one track in the
-     *     {@code trackGroup} are supported.
-     * @param trackSupport The {@link C.FormatSupport} of each track in the {@code trackGroup}.
-     * @param tracksSelected Whether each track in the {@code trackGroup} is selected.
+     * @param trackGroup The underlying {@link TrackGroup}.
+     * @param adaptiveSupported Whether the player supports adaptive selections containing more than
+     *     one track in the group.
+     * @param trackSupport The {@link C.FormatSupport} of each track in the group.
+     * @param trackSelected Whether each track in the {@code trackGroup} is selected.
      */
     @UnstableApi
     public TrackGroupInfo(
         TrackGroup trackGroup,
         boolean adaptiveSupported,
         @C.FormatSupport int[] trackSupport,
-        boolean[] tracksSelected) {
+        boolean[] trackSelected) {
       length = trackGroup.length;
-      checkArgument(length == trackSupport.length && length == tracksSelected.length);
+      checkArgument(length == trackSupport.length && length == trackSelected.length);
       this.trackGroup = trackGroup;
       this.adaptiveSupported = adaptiveSupported && length > 1;
       this.trackSupport = trackSupport.clone();
-      this.trackSelected = tracksSelected.clone();
+      this.trackSelected = trackSelected.clone();
     }
 
     /** Returns the underlying {@link TrackGroup}. */
@@ -266,11 +266,11 @@ public final class TracksInfo implements Bundleable {
     }
   }
 
-  private final ImmutableList trackGroupInfos;
-
   /** An {@code TrackInfo} that contains no tracks. */
   @UnstableApi public static final TracksInfo EMPTY = new TracksInfo(ImmutableList.of());
 
+  private final ImmutableList trackGroupInfos;
+
   /**
    * Constructs an instance.
    *
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 b83620df38..d05a123346 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
@@ -15,6 +15,8 @@
  */
 package androidx.media3.common.util;
 
+import static androidx.media3.common.util.Assertions.checkArgument;
+
 import android.util.Pair;
 import androidx.annotation.Nullable;
 import androidx.media3.common.C;
@@ -31,6 +33,12 @@ public final class CodecSpecificDataUtil {
   private static final String[] HEVC_GENERAL_PROFILE_SPACE_STRINGS =
       new String[] {"", "A", "B", "C"};
 
+  // MP4V-ES
+  private static final int VISUAL_OBJECT_LAYER = 1;
+  private static final int VISUAL_OBJECT_LAYER_START = 0x20;
+  private static final int EXTENDED_PAR = 0x0F;
+  private static final int RECTANGULAR = 0x00;
+
   /**
    * Parses an ALAC AudioSpecificConfig (i.e. an ALACSpecificConfig).
@@ -72,6 +80,87 @@ public final class CodecSpecificDataUtil {
         && initializationData.get(0)[0] == 1;
   }
 
+  /**
+   * Parses an MPEG-4 Visual configuration information, as defined in ISO/IEC14496-2.
+   *
+   * @param videoSpecificConfig A byte array containing the MPEG-4 Visual configuration information
+   *     to parse.
+   * @return A pair of the video's width and height.
+   */
+  public static Pair getVideoResolutionFromMpeg4VideoConfig(
+      byte[] videoSpecificConfig) {
+    int offset = 0;
+    boolean foundVOL = false;
+    ParsableByteArray scratchBytes = new ParsableByteArray(videoSpecificConfig);
+    while (offset + 3 < videoSpecificConfig.length) {
+      if (scratchBytes.readUnsignedInt24() != VISUAL_OBJECT_LAYER
+          || (videoSpecificConfig[offset + 3] & 0xF0) != VISUAL_OBJECT_LAYER_START) {
+        scratchBytes.setPosition(scratchBytes.getPosition() - 2);
+        offset++;
+        continue;
+      }
+      foundVOL = true;
+      break;
+    }
+
+    checkArgument(foundVOL, "Invalid input: VOL not found.");
+
+    ParsableBitArray scratchBits = new ParsableBitArray(videoSpecificConfig);
+    // Skip the start codecs from the bitstream
+    scratchBits.skipBits((offset + 4) * 8);
+    scratchBits.skipBits(1); // random_accessible_vol
+    scratchBits.skipBits(8); // video_object_type_indication
+
+    if (scratchBits.readBit()) { // object_layer_identifier
+      scratchBits.skipBits(4); // video_object_layer_verid
+      scratchBits.skipBits(3); // video_object_layer_priority
+    }
+
+    int aspectRatioInfo = scratchBits.readBits(4);
+    if (aspectRatioInfo == EXTENDED_PAR) {
+      scratchBits.skipBits(8); // par_width
+      scratchBits.skipBits(8); // par_height
+    }
+
+    if (scratchBits.readBit()) { // vol_control_parameters
+      scratchBits.skipBits(2); // chroma_format
+      scratchBits.skipBits(1); // low_delay
+      if (scratchBits.readBit()) { // vbv_parameters
+        scratchBits.skipBits(79);
+      }
+    }
+
+    int videoObjectLayerShape = scratchBits.readBits(2);
+    checkArgument(
+        videoObjectLayerShape == RECTANGULAR,
+        "Only supports rectangular video object layer shape.");
+
+    checkArgument(scratchBits.readBit()); // marker_bit
+    int vopTimeIncrementResolution = scratchBits.readBits(16);
+    checkArgument(scratchBits.readBit()); // marker_bit
+
+    if (scratchBits.readBit()) { // fixed_vop_rate
+      checkArgument(vopTimeIncrementResolution > 0);
+      vopTimeIncrementResolution--;
+      int numBitsToSkip = 0;
+      while (vopTimeIncrementResolution > 0) {
+        numBitsToSkip++;
+        vopTimeIncrementResolution >>= 1;
+      }
+      scratchBits.skipBits(numBitsToSkip); // fixed_vop_time_increment
+    }
+
+    checkArgument(scratchBits.readBit()); // marker_bit
+    int videoObjectLayerWidth = scratchBits.readBits(13);
+    checkArgument(scratchBits.readBit()); // marker_bit
+    int videoObjectLayerHeight = scratchBits.readBits(13);
+    checkArgument(scratchBits.readBit()); // marker_bit
+
+    scratchBits.skipBits(1); // interlaced
+
+    return Pair.create(videoObjectLayerWidth, videoObjectLayerHeight);
+  }
+
   /**
    * Builds an RFC 6381 AVC codec string using the provided parameters.
    *
diff --git a/libraries/common/src/main/java/androidx/media3/common/util/GlProgram.java b/libraries/common/src/main/java/androidx/media3/common/util/GlProgram.java
index 681ec57433..82b0775a89 100644
--- a/libraries/common/src/main/java/androidx/media3/common/util/GlProgram.java
+++ b/libraries/common/src/main/java/androidx/media3/common/util/GlProgram.java
@@ -147,8 +147,6 @@ public final class GlProgram {
    * 

Call this in the rendering loop to switch between different programs. */ public void use() { - // TODO(b/214975934): When multiple GL programs are supported by Transformer, make sure - // to call use() to switch between programs. GLES20.glUseProgram(programId); GlUtil.checkGlError(); } @@ -175,9 +173,16 @@ public final class GlProgram { checkNotNull(attributeByName.get(name)).setBuffer(values, size); } - /** Sets a texture sampler type uniform. */ - public void setSamplerTexIdUniform(String name, int texId, int unit) { - checkNotNull(uniformByName.get(name)).setSamplerTexId(texId, unit); + /** + * Sets a texture sampler type uniform. + * + * @param name The uniform's name. + * @param texId The texture identifier. + * @param texUnitIndex The texture unit index. Use a different index (0, 1, 2, ...) for each + * texture sampler in the program. + */ + public void setSamplerTexIdUniform(String name, int texId, int texUnitIndex) { + checkNotNull(uniformByName.get(name)).setSamplerTexId(texId, texUnitIndex); } /** Sets a float type uniform. */ @@ -322,7 +327,7 @@ public final class GlProgram { private final float[] value; private int texId; - private int unit; + private int texUnitIndex; private Uniform(String name, int location, int type) { this.name = name; @@ -335,11 +340,11 @@ public final class GlProgram { * Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform. * * @param texId The GL texture identifier from which to sample. - * @param unit The GL texture unit index. + * @param texUnitIndex The GL texture unit index. */ - public void setSamplerTexId(int texId, int unit) { + public void setSamplerTexId(int texId, int texUnitIndex) { this.texId = texId; - this.unit = unit; + this.texUnitIndex = texUnitIndex; } /** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */ @@ -382,7 +387,7 @@ public final class GlProgram { if (texId == 0) { throw new IllegalStateException("No call to setSamplerTexId() before bind."); } - GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + texUnitIndex); if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES || type == GL_SAMPLER_EXTERNAL_2D_Y2Y_EXT) { GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId); } else if (type == GLES20.GL_SAMPLER_2D) { @@ -390,7 +395,7 @@ public final class GlProgram { } else { throw new IllegalStateException("Unexpected uniform type: " + type); } - GLES20.glUniform1i(location, unit); + GLES20.glUniform1i(location, texUnitIndex); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri( diff --git a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java index 16b040dc47..fe4532976c 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java @@ -119,7 +119,8 @@ public final class GlUtil { /** * Returns whether creating a GL context with {@value #EXTENSION_PROTECTED_CONTENT} is possible. - * If {@code true}, the device supports a protected output path for DRM content when using GL. + * + *

If {@code true}, the device supports a protected output path for DRM content when using GL. */ public static boolean isProtectedContentExtensionSupported(Context context) { if (Util.SDK_INT < 24) { @@ -146,7 +147,11 @@ public final class GlUtil { } /** - * Returns whether creating a GL context with {@value #EXTENSION_SURFACELESS_CONTEXT} is possible. + * Returns whether the {@value #EXTENSION_SURFACELESS_CONTEXT} extension is supported. + * + *

This extension allows passing {@link EGL14#EGL_NO_SURFACE} for both the write and read + * surfaces in a call to {@link EGL14#eglMakeCurrent(EGLDisplay, EGLSurface, EGLSurface, + * EGLContext)}. */ public static boolean isSurfacelessContextExtensionSupported() { if (Util.SDK_INT < 17) { @@ -206,6 +211,52 @@ public final class GlUtil { EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ); } + /** + * Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer. + * + * @param eglContext The {@link EGLContext} to make current. + * @param eglDisplay The {@link EGLDisplay} to attach the surface to. + */ + @RequiresApi(17) + public static void focusPlaceholderEglSurface(EGLContext eglContext, EGLDisplay eglDisplay) { + int[] pbufferAttributes = + new int[] { + EGL14.EGL_WIDTH, /* width= */ 1, + EGL14.EGL_HEIGHT, /* height= */ 1, + EGL14.EGL_NONE + }; + EGLSurface eglSurface = + Api17.createEglPbufferSurface( + eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_8888, pbufferAttributes); + focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1); + } + + /** + * Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer, for HDR rendering + * with Rec. 2020 color primaries and using the PQ transfer function. + * + * @param eglContext The {@link EGLContext} to make current. + * @param eglDisplay The {@link EGLDisplay} to attach the surface to. + */ + @RequiresApi(17) + public static void focusPlaceholderEglSurfaceBt2020Pq( + EGLContext eglContext, EGLDisplay eglDisplay) { + int[] pbufferAttributes = + new int[] { + EGL14.EGL_WIDTH, + /* width= */ 1, + EGL14.EGL_HEIGHT, + /* height= */ 1, + EGL_GL_COLORSPACE_KHR, + EGL_GL_COLORSPACE_BT2020_PQ_EXT, + EGL14.EGL_NONE + }; + EGLSurface eglSurface = + Api17.createEglPbufferSurface( + eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_1010102, pbufferAttributes); + focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1); + } + /** * If there is an OpenGl error, logs the error and if {@link #glAssertionsEnabled} is true throws * a {@link GlException}. @@ -222,6 +273,30 @@ public final class GlUtil { } } + /** + * Asserts the texture size is valid. + * + * @param width The width for a texture. + * @param height The height for a texture. + * @throws GlException If the texture width or height is invalid. + */ + public static void assertValidTextureSize(int width, int height) { + // TODO(b/201293185): Consider handling adjustments for sizes > GL_MAX_TEXTURE_SIZE + // (ex. downscaling appropriately) in a FrameProcessor instead of asserting incorrect values. + + // For valid GL sizes, see: + // https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexImage2D.xml + int[] maxTextureSizeBuffer = new int[1]; + GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeBuffer, 0); + int maxTextureSize = maxTextureSizeBuffer[0]; + if (width < 0 || height < 0) { + throwGlException("width or height is less than 0"); + } + if (width > maxTextureSize || height > maxTextureSize) { + throwGlException("width or height is greater than GL_MAX_TEXTURE_SIZE " + maxTextureSize); + } + } + /** * Makes the specified {@code eglSurface} the render target, using a viewport of {@code width} by * {@code height} pixels. @@ -320,6 +395,7 @@ public final class GlUtil { * @param height of the new texture in pixels */ public static int createTexture(int width, int height) { + assertValidTextureSize(width, height); int texId = generateAndBindTexture(GLES20.GL_TEXTURE_2D); ByteBuffer byteBuffer = ByteBuffer.allocateDirect(width * height * 4); GLES20.glTexImage2D( @@ -345,6 +421,9 @@ public final class GlUtil { * GLES11Ext#GL_TEXTURE_EXTERNAL_OES} for an external texture. */ private static int generateAndBindTexture(int textureTarget) { + checkEglException( + !Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context"); + int[] texId = new int[1]; GLES20.glGenTextures(/* n= */ 1, texId, /* offset= */ 0); checkGlError(); @@ -365,6 +444,9 @@ public final class GlUtil { * @param texId The identifier of the texture to attach to the framebuffer. */ public static int createFboForTexture(int texId) { + checkEglException( + !Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context"); + int[] fboId = new int[1]; GLES20.glGenFramebuffers(/* n= */ 1, fboId, /* offset= */ 0); checkGlError(); @@ -377,9 +459,10 @@ public final class GlUtil { } /* package */ static void throwGlException(String errorMsg) { - Log.e(TAG, errorMsg); if (glAssertionsEnabled) { throw new GlException(errorMsg); + } else { + Log.e(TAG, errorMsg); } } @@ -389,6 +472,11 @@ public final class GlUtil { } } + private static void checkEglException(String errorMessage) { + int error = EGL14.eglGetError(); + checkEglException(error == EGL14.EGL_SUCCESS, errorMessage + ", error code: " + error); + } + @RequiresApi(17) private static final class Api17 { private Api17() {} @@ -437,12 +525,28 @@ public final class GlUtil { Object surface, int[] configAttributes, int[] windowSurfaceAttributes) { - return EGL14.eglCreateWindowSurface( - eglDisplay, - getEglConfig(eglDisplay, configAttributes), - surface, - windowSurfaceAttributes, - /* offset= */ 0); + EGLSurface eglSurface = + EGL14.eglCreateWindowSurface( + eglDisplay, + getEglConfig(eglDisplay, configAttributes), + surface, + windowSurfaceAttributes, + /* offset= */ 0); + checkEglException("Error creating surface"); + return eglSurface; + } + + @DoNotInline + public static EGLSurface createEglPbufferSurface( + EGLDisplay eglDisplay, int[] configAttributes, int[] pbufferAttributes) { + EGLSurface eglSurface = + EGL14.eglCreatePbufferSurface( + eglDisplay, + getEglConfig(eglDisplay, configAttributes), + pbufferAttributes, + /* offset= */ 0); + checkEglException("Error creating surface"); + return eglSurface; } @DoNotInline @@ -458,8 +562,11 @@ public final class GlUtil { if (boundFramebuffer[0] != framebuffer) { GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer); } + checkGlError(); EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext); + checkEglException("Error making context current"); GLES20.glViewport(/* x= */ 0, /* y= */ 0, width, height); + checkGlError(); } @DoNotInline @@ -470,19 +577,15 @@ public final class GlUtil { } EGL14.eglMakeCurrent( eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); - int error = EGL14.eglGetError(); - checkEglException(error == EGL14.EGL_SUCCESS, "Error releasing context: " + error); + checkEglException("Error releasing context"); if (eglContext != null) { EGL14.eglDestroyContext(eglDisplay, eglContext); - error = EGL14.eglGetError(); - checkEglException(error == EGL14.EGL_SUCCESS, "Error destroying context: " + error); + checkEglException("Error destroying context"); } EGL14.eglReleaseThread(); - error = EGL14.eglGetError(); - checkEglException(error == EGL14.EGL_SUCCESS, "Error releasing thread: " + error); + checkEglException("Error releasing thread"); EGL14.eglTerminate(eglDisplay); - error = EGL14.eglGetError(); - checkEglException(error == EGL14.EGL_SUCCESS, "Error terminating display: " + error); + checkEglException("Error terminating display"); } @DoNotInline diff --git a/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java index 74b244c1b4..6b62f15247 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java @@ -47,6 +47,19 @@ public final class MediaFormatUtil { // The constant value must not be changed, because it's also set by the framework MediaParser API. public static final String KEY_PCM_ENCODING_EXTENDED = "exo-pcm-encoding-int"; + /** + * The {@link MediaFormat} key for the maximum bitrate in bits per second. + * + *

The associated value is an integer. + * + *

The key string constant is the same as {@code MediaFormat#KEY_MAX_BITRATE}. Values for it + * are already returned by the framework MediaExtractor; the key is a hidden field in {@code + * MediaFormat} though, which is why it's being replicated here. + */ + // The constant value must not be changed, because it's also set by the framework MediaParser and + // MediaExtractor APIs. + public static final String KEY_MAX_BIT_RATE = "max-bitrate"; + private static final int MAX_POWER_OF_TWO_INT = 1 << 30; /** @@ -63,6 +76,7 @@ public final class MediaFormatUtil { public static MediaFormat createMediaFormatFromFormat(Format format) { MediaFormat result = new MediaFormat(); maybeSetInteger(result, MediaFormat.KEY_BIT_RATE, format.bitrate); + maybeSetInteger(result, KEY_MAX_BIT_RATE, format.peakBitrate); maybeSetInteger(result, MediaFormat.KEY_CHANNEL_COUNT, format.channelCount); maybeSetColorInfo(result, format.colorInfo); diff --git a/libraries/common/src/main/java/androidx/media3/common/util/UnstableApi.java b/libraries/common/src/main/java/androidx/media3/common/util/UnstableApi.java index a624093e08..e4a7f5c20e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/UnstableApi.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/UnstableApi.java @@ -47,8 +47,27 @@ import java.lang.annotation.Target; * Android Studio, in order to alert developers to the risk of breaking changes. * *

Individual usage sites can be opted-in to suppress the lint error by using the {@link - * androidx.annotation.OptIn} annotation: {@code @androidx.annotation.OptIn(markerClass = - * androidx.media3.common.util.UnstableApi.class)}. + * androidx.annotation.OptIn} annotation. + * + *

In Java: + * + *

{@code
+ * import androidx.annotation.OptIn;
+ * import androidx.media3.common.util.UnstableApi;
+ * ...
+ * @OptIn(markerClass = UnstableApi.class)
+ * private void methodUsingUnstableApis() { ... }
+ * }
+ * + *

In Kotlin: + * + *

{@code
+ * import androidx.annotation.OptIn
+ * import androidx.media3.common.util.UnstableApi
+ * ...
+ * @OptIn(UnstableApi::class)
+ * private fun methodUsingUnstableApis() { ... }
+ * }
* *

Whole projects can be opted-in by suppressing the specific lint error in their {@code lint.xml} file: diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index d45d1d35dc..d3831ed4d0 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -1121,6 +1121,25 @@ public final class Util { return min; } + /** + * Returns the maximum value in the given {@link SparseLongArray}. + * + * @param sparseLongArray The {@link SparseLongArray}. + * @return The maximum value. + * @throws NoSuchElementException If the array is empty. + */ + @RequiresApi(18) + public static long maxValue(SparseLongArray sparseLongArray) { + if (sparseLongArray.size() == 0) { + throw new NoSuchElementException(); + } + long max = Long.MIN_VALUE; + for (int i = 0; i < sparseLongArray.size(); i++) { + max = max(max, sparseLongArray.valueAt(i)); + } + return max; + } + /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving {@link * C#TIME_UNSET} and {@link C#TIME_END_OF_SOURCE} values. @@ -1143,18 +1162,6 @@ public final class Util { return (timeMs == C.TIME_UNSET || timeMs == C.TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000); } - /** - * Converts a time in seconds to the corresponding time in microseconds. - * - * @param timeSec The time in seconds. - * @return The corresponding time in microseconds. - */ - public static long secToUs(double timeSec) { - return BigDecimal.valueOf(timeSec) - .multiply(BigDecimal.valueOf(C.MICROS_PER_SECOND)) - .longValue(); - } - /** * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. * @@ -1897,10 +1904,10 @@ public final class Util { /** * Returns the MIME type corresponding to the given adaptive {@link ContentType}, or {@code null} - * if the content type is {@link C#TYPE_OTHER}. + * if the content type is not adaptive. */ @Nullable - public static String getAdaptiveMimeTypeForContentType(int contentType) { + public static String getAdaptiveMimeTypeForContentType(@ContentType int contentType) { switch (contentType) { case C.TYPE_DASH: return MimeTypes.APPLICATION_MPD; @@ -1908,6 +1915,7 @@ public final class Util { return MimeTypes.APPLICATION_M3U8; case C.TYPE_SS: return MimeTypes.APPLICATION_SS; + case C.TYPE_RTSP: case C.TYPE_OTHER: default: return null; diff --git a/libraries/common/src/test/java/androidx/media3/common/TrackSelectionOverrideTest.java b/libraries/common/src/test/java/androidx/media3/common/TrackSelectionOverrideTest.java index 1de9157adb..a65ff4572f 100644 --- a/libraries/common/src/test/java/androidx/media3/common/TrackSelectionOverrideTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/TrackSelectionOverrideTest.java @@ -29,12 +29,12 @@ import org.junit.runner.RunWith; public final class TrackSelectionOverrideTest { @Test - public void newTrackSelectionOverride_withJustTrackGroup_selectsAllTracks() { + public void newTrackSelectionOverride_withOneTrack_selectsOneTrack() { TrackSelectionOverride trackSelectionOverride = - new TrackSelectionOverride(newTrackGroupWithIds(1, 2)); + new TrackSelectionOverride(newTrackGroupWithIds(1, 2), /* trackIndex= */ 1); assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2)); - assertThat(trackSelectionOverride.trackIndices).containsExactly(0, 1).inOrder(); + assertThat(trackSelectionOverride.trackIndices).containsExactly(1).inOrder(); } @Test diff --git a/libraries/common/src/test/java/androidx/media3/common/TrackSelectionParametersTest.java b/libraries/common/src/test/java/androidx/media3/common/TrackSelectionParametersTest.java index d11efe782d..5b04a0f115 100644 --- a/libraries/common/src/test/java/androidx/media3/common/TrackSelectionParametersTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/TrackSelectionParametersTest.java @@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import org.junit.Test; import org.junit.runner.RunWith; @@ -57,6 +56,7 @@ public final class TrackSelectionParametersTest { assertThat(parameters.preferredAudioMimeTypes).isEmpty(); assertThat(parameters.preferredTextLanguages).isEmpty(); assertThat(parameters.preferredTextRoleFlags).isEqualTo(0); + assertThat(parameters.ignoredTextSelectionFlags).isEqualTo(0); assertThat(parameters.selectUndeterminedTextLanguage).isFalse(); // General assertThat(parameters.forceLowestBitrate).isFalse(); @@ -68,7 +68,8 @@ public final class TrackSelectionParametersTest { @Test public void parametersSet_fromDefault_isAsExpected() { TrackSelectionOverride override1 = - new TrackSelectionOverride(new TrackGroup(new Format.Builder().build())); + new TrackSelectionOverride( + new TrackGroup(new Format.Builder().build()), /* trackIndex= */ 0); TrackSelectionOverride override2 = new TrackSelectionOverride( new TrackGroup( @@ -98,18 +99,22 @@ public final class TrackSelectionParametersTest { // Text .setPreferredTextLanguages("de", "en") .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION) + .setIgnoredTextSelectionFlags(C.SELECTION_FLAG_AUTOSELECT) .setSelectUndeterminedTextLanguage(true) // General .setForceLowestBitrate(false) .setForceHighestSupportedBitrate(true) - .addOverride(new TrackSelectionOverride(new TrackGroup(new Format.Builder().build()))) + .addOverride( + new TrackSelectionOverride( + new TrackGroup(new Format.Builder().build()), /* trackIndex= */ 0)) .addOverride( new TrackSelectionOverride( new TrackGroup( new Format.Builder().setId(4).build(), new Format.Builder().setId(5).build()), /* trackIndices= */ ImmutableList.of(1))) - .setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT)) + .setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, /* disabled= */ true) + .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, /* disabled= */ true) .build(); // Video @@ -138,6 +143,7 @@ public final class TrackSelectionParametersTest { // Text assertThat(parameters.preferredTextLanguages).containsExactly("de", "en").inOrder(); assertThat(parameters.preferredTextRoleFlags).isEqualTo(C.ROLE_FLAG_CAPTION); + assertThat(parameters.ignoredTextSelectionFlags).isEqualTo(C.SELECTION_FLAG_AUTOSELECT); assertThat(parameters.selectUndeterminedTextLanguage).isTrue(); // General assertThat(parameters.forceLowestBitrate).isFalse(); @@ -202,8 +208,10 @@ public final class TrackSelectionParametersTest { @Test public void addOverride_onDifferentGroups_addsOverride() { - TrackSelectionOverride override1 = new TrackSelectionOverride(newTrackGroupWithIds(1)); - TrackSelectionOverride override2 = new TrackSelectionOverride(newTrackGroupWithIds(2)); + TrackSelectionOverride override1 = + new TrackSelectionOverride(newTrackGroupWithIds(1), /* trackIndex= */ 0); + TrackSelectionOverride override2 = + new TrackSelectionOverride(newTrackGroupWithIds(2), /* trackIndex= */ 0); TrackSelectionParameters trackSelectionParameters = new TrackSelectionParameters.Builder(getApplicationContext()) @@ -234,8 +242,10 @@ public final class TrackSelectionParametersTest { @Test public void setOverrideForType_onSameType_replacesOverride() { - TrackSelectionOverride override1 = new TrackSelectionOverride(newTrackGroupWithIds(1)); - TrackSelectionOverride override2 = new TrackSelectionOverride(newTrackGroupWithIds(2)); + TrackSelectionOverride override1 = + new TrackSelectionOverride(newTrackGroupWithIds(1), /* trackIndex= */ 0); + TrackSelectionOverride override2 = + new TrackSelectionOverride(newTrackGroupWithIds(2), /* trackIndex= */ 0); TrackSelectionParameters trackSelectionParameters = new TrackSelectionParameters.Builder(getApplicationContext()) @@ -248,8 +258,10 @@ public final class TrackSelectionParametersTest { @Test public void clearOverridesOfType_ofTypeAudio_removesAudioOverride() { - TrackSelectionOverride override1 = new TrackSelectionOverride(AAC_TRACK_GROUP); - TrackSelectionOverride override2 = new TrackSelectionOverride(newTrackGroupWithIds(1)); + TrackSelectionOverride override1 = + new TrackSelectionOverride(AAC_TRACK_GROUP, /* trackIndex= */ 0); + TrackSelectionOverride override2 = + new TrackSelectionOverride(newTrackGroupWithIds(1), /* trackIndex= */ 0); TrackSelectionParameters trackSelectionParameters = new TrackSelectionParameters.Builder(getApplicationContext()) .addOverride(override1) @@ -262,8 +274,10 @@ public final class TrackSelectionParametersTest { @Test public void clearOverride_ofTypeGroup_removesOverride() { - TrackSelectionOverride override1 = new TrackSelectionOverride(AAC_TRACK_GROUP); - TrackSelectionOverride override2 = new TrackSelectionOverride(newTrackGroupWithIds(1)); + TrackSelectionOverride override1 = + new TrackSelectionOverride(AAC_TRACK_GROUP, /* trackIndex= */ 0); + TrackSelectionOverride override2 = + new TrackSelectionOverride(newTrackGroupWithIds(1), /* trackIndex= */ 0); TrackSelectionParameters trackSelectionParameters = new TrackSelectionParameters.Builder(getApplicationContext()) .addOverride(override1) diff --git a/libraries/common/src/test/java/androidx/media3/common/util/ColorParserTest.java b/libraries/common/src/test/java/androidx/media3/common/util/ColorParserTest.java index ba4d2b7a75..4791df0905 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/ColorParserTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/ColorParserTest.java @@ -22,6 +22,7 @@ import static android.graphics.Color.argb; import static android.graphics.Color.parseColor; import static androidx.media3.common.util.ColorParser.parseTtmlColor; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import android.graphics.Color; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -34,24 +35,26 @@ public final class ColorParserTest { // Negative tests. - @Test(expected = IllegalArgumentException.class) + @Test public void parseUnknownColor() { - ColorParser.parseTtmlColor("colorOfAnElectron"); + assertThrows( + IllegalArgumentException.class, () -> ColorParser.parseTtmlColor("colorOfAnElectron")); } - @Test(expected = IllegalArgumentException.class) + @Test public void parseNull() { - ColorParser.parseTtmlColor(null); + assertThrows(IllegalArgumentException.class, () -> ColorParser.parseTtmlColor(null)); } - @Test(expected = IllegalArgumentException.class) + @Test public void parseEmpty() { - ColorParser.parseTtmlColor(""); + assertThrows(IllegalArgumentException.class, () -> ColorParser.parseTtmlColor("")); } - @Test(expected = IllegalArgumentException.class) + @Test public void rgbColorParsingRgbValuesNegative() { - ColorParser.parseTtmlColor("rgb(-4, 55, 209)"); + assertThrows( + IllegalArgumentException.class, () -> ColorParser.parseTtmlColor("rgb(-4, 55, 209)")); } // Positive tests. diff --git a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java index 3ffc4bfa4f..a2056e1542 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java @@ -21,6 +21,7 @@ import static androidx.media3.common.util.Util.escapeFileName; import static androidx.media3.common.util.Util.getCodecsOfType; import static androidx.media3.common.util.Util.getStringForTime; import static androidx.media3.common.util.Util.gzip; +import static androidx.media3.common.util.Util.maxValue; import static androidx.media3.common.util.Util.minValue; import static androidx.media3.common.util.Util.parseXsDateTime; import static androidx.media3.common.util.Util.parseXsDuration; @@ -747,6 +748,21 @@ public class UtilTest { assertThrows(NoSuchElementException.class, () -> minValue(new SparseLongArray())); } + @Test + public void sparseLongArrayMaxValue_returnsMaxValue() { + SparseLongArray sparseLongArray = new SparseLongArray(); + sparseLongArray.put(0, 2); + sparseLongArray.put(25, 10); + sparseLongArray.put(42, 1); + + assertThat(maxValue(sparseLongArray)).isEqualTo(10); + } + + @Test + public void sparseLongArrayMaxValue_emptyArray_throws() { + assertThrows(NoSuchElementException.class, () -> maxValue(new SparseLongArray())); + } + @Test public void parseXsDuration_returnsParsedDurationInMillis() { assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/BaseDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/BaseDataSource.java index 4a826df2c2..55d721bce7 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/BaseDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/BaseDataSource.java @@ -48,6 +48,7 @@ public abstract class BaseDataSource implements DataSource { this.listeners = new ArrayList<>(/* initialCapacity= */ 1); } + @UnstableApi @Override public final void addTransferListener(TransferListener transferListener) { checkNotNull(transferListener); diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/DataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/DataSource.java index e1a34d7883..fc93269055 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/DataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/DataSource.java @@ -26,13 +26,13 @@ import java.util.List; import java.util.Map; /** Reads data from URI-identified resources. */ -@UnstableApi public interface DataSource extends DataReader { /** A factory for {@link DataSource} instances. */ interface Factory { /** Creates a {@link DataSource} instance. */ + @UnstableApi DataSource createDataSource(); } @@ -41,6 +41,7 @@ public interface DataSource extends DataReader { * * @param transferListener A {@link TransferListener}. */ + @UnstableApi void addTransferListener(TransferListener transferListener); /** @@ -72,6 +73,7 @@ public interface DataSource extends DataReader { * unresolved. For all other requests, the value returned will be equal to the request's * {@link DataSpec#length}. */ + @UnstableApi long open(DataSpec dataSpec) throws IOException; /** @@ -82,6 +84,7 @@ public interface DataSource extends DataReader { * * @return The {@link Uri} from which data is being read, or null if the source is not open. */ + @UnstableApi @Nullable Uri getUri(); @@ -91,6 +94,7 @@ public interface DataSource extends DataReader { * *

Key look-up in the returned map is case-insensitive. */ + @UnstableApi default Map> getResponseHeaders() { return Collections.emptyMap(); } @@ -101,5 +105,6 @@ public interface DataSource extends DataReader { * * @throws IOException If an error occurs closing the source. */ + @UnstableApi void close() throws IOException; } diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceException.java b/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceException.java index fa14682255..a2ad3ba6d5 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceException.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceException.java @@ -21,7 +21,6 @@ import androidx.media3.common.util.UnstableApi; import java.io.IOException; /** Used to specify reason of a DataSource error. */ -@UnstableApi public class DataSourceException extends IOException { /** @@ -29,6 +28,7 @@ public class DataSourceException extends IOException { * {@link #reason} is {@link PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE} in its * cause stack. */ + @UnstableApi public static boolean isCausedByPositionOutOfRange(IOException e) { @Nullable Throwable cause = e; while (cause != null) { @@ -49,7 +49,7 @@ public class DataSourceException extends IOException { * * @deprecated Use {@link PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE}. */ - @Deprecated + @UnstableApi @Deprecated public static final int POSITION_OUT_OF_RANGE = PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE; @@ -65,6 +65,7 @@ public class DataSourceException extends IOException { * @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link * PlaybackException.ErrorCode}. */ + @UnstableApi public DataSourceException(@PlaybackException.ErrorCode int reason) { this.reason = reason; } @@ -76,6 +77,7 @@ public class DataSourceException extends IOException { * @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link * PlaybackException.ErrorCode}. */ + @UnstableApi public DataSourceException(@Nullable Throwable cause, @PlaybackException.ErrorCode int reason) { super(cause); this.reason = reason; @@ -88,6 +90,7 @@ public class DataSourceException extends IOException { * @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link * PlaybackException.ErrorCode}. */ + @UnstableApi public DataSourceException(@Nullable String message, @PlaybackException.ErrorCode int reason) { super(message); this.reason = reason; @@ -101,6 +104,7 @@ public class DataSourceException extends IOException { * @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link * PlaybackException.ErrorCode}. */ + @UnstableApi public DataSourceException( @Nullable String message, @Nullable Throwable cause, diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultDataSource.java index 0a726541fa..cc179e2d55 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultDataSource.java @@ -54,7 +54,6 @@ import java.util.Map; * #DefaultDataSource(Context, DataSource)}. * */ -@UnstableApi public final class DefaultDataSource implements DataSource { /** {@link DataSource.Factory} for {@link DefaultDataSource} instances. */ @@ -98,11 +97,13 @@ public final class DefaultDataSource implements DataSource { * @param transferListener The listener that will be used. * @return This factory. */ + @UnstableApi public Factory setTransferListener(@Nullable TransferListener transferListener) { this.transferListener = transferListener; return this; } + @UnstableApi @Override public DefaultDataSource createDataSource() { DefaultDataSource dataSource = @@ -144,6 +145,7 @@ public final class DefaultDataSource implements DataSource { * * @param context A context. */ + @UnstableApi public DefaultDataSource(Context context, boolean allowCrossProtocolRedirects) { this( context, @@ -162,6 +164,7 @@ public final class DefaultDataSource implements DataSource { * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled when fetching remote data. */ + @UnstableApi public DefaultDataSource( Context context, @Nullable String userAgent, boolean allowCrossProtocolRedirects) { this( @@ -185,6 +188,7 @@ public final class DefaultDataSource implements DataSource { * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled when fetching remote data. */ + @UnstableApi public DefaultDataSource( Context context, @Nullable String userAgent, @@ -209,12 +213,14 @@ public final class DefaultDataSource implements DataSource { * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and * content. This {@link DataSource} should normally support at least http(s). */ + @UnstableApi public DefaultDataSource(Context context, DataSource baseDataSource) { this.context = context.getApplicationContext(); this.baseDataSource = Assertions.checkNotNull(baseDataSource); transferListeners = new ArrayList<>(); } + @UnstableApi @Override public void addTransferListener(TransferListener transferListener) { Assertions.checkNotNull(transferListener); @@ -229,6 +235,7 @@ public final class DefaultDataSource implements DataSource { maybeAddListenerToDataSource(rawResourceDataSource, transferListener); } + @UnstableApi @Override public long open(DataSpec dataSpec) throws IOException { Assertions.checkState(dataSource == null); @@ -260,22 +267,26 @@ public final class DefaultDataSource implements DataSource { return dataSource.open(dataSpec); } + @UnstableApi @Override public int read(byte[] buffer, int offset, int length) throws IOException { return Assertions.checkNotNull(dataSource).read(buffer, offset, length); } + @UnstableApi @Override @Nullable public Uri getUri() { return dataSource == null ? null : dataSource.getUri(); } + @UnstableApi @Override public Map> getResponseHeaders() { return dataSource == null ? Collections.emptyMap() : dataSource.getResponseHeaders(); } + @UnstableApi @Override public void close() throws IOException { if (dataSource != null) { diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultHttpDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultHttpDataSource.java index 4b6a9f0772..59d8d4a40f 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultHttpDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/DefaultHttpDataSource.java @@ -60,7 +60,6 @@ import java.util.zip.GZIPInputStream; * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default properties that can * be passed to {@link HttpDataSource.Factory#setDefaultRequestProperties(Map)}. */ -@UnstableApi public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource { /** {@link DataSource.Factory} for {@link DefaultHttpDataSource} instances. */ @@ -83,6 +82,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS; } + @UnstableApi @Override public final Factory setDefaultRequestProperties(Map defaultRequestProperties) { this.defaultRequestProperties.clearAndSet(defaultRequestProperties); @@ -99,6 +99,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * agent of the underlying platform. * @return This factory. */ + @UnstableApi public Factory setUserAgent(@Nullable String userAgent) { this.userAgent = userAgent; return this; @@ -112,6 +113,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @param connectTimeoutMs The connect timeout, in milliseconds, that will be used. * @return This factory. */ + @UnstableApi public Factory setConnectTimeoutMs(int connectTimeoutMs) { this.connectTimeoutMs = connectTimeoutMs; return this; @@ -125,6 +127,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @param readTimeoutMs The connect timeout, in milliseconds, that will be used. * @return This factory. */ + @UnstableApi public Factory setReadTimeoutMs(int readTimeoutMs) { this.readTimeoutMs = readTimeoutMs; return this; @@ -138,6 +141,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @param allowCrossProtocolRedirects Whether to allow cross protocol redirects. * @return This factory. */ + @UnstableApi public Factory setAllowCrossProtocolRedirects(boolean allowCrossProtocolRedirects) { this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; return this; @@ -154,6 +158,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * predicate that was previously set. * @return This factory. */ + @UnstableApi public Factory setContentTypePredicate(@Nullable Predicate contentTypePredicate) { this.contentTypePredicate = contentTypePredicate; return this; @@ -169,6 +174,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @param transferListener The listener that will be used. * @return This factory. */ + @UnstableApi public Factory setTransferListener(@Nullable TransferListener transferListener) { this.transferListener = transferListener; return this; @@ -178,11 +184,13 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * Sets whether we should keep the POST method and body when we have HTTP 302 redirects for a * POST request. */ + @UnstableApi public Factory setKeepPostFor302Redirects(boolean keepPostFor302Redirects) { this.keepPostFor302Redirects = keepPostFor302Redirects; return this; } + @UnstableApi @Override public DefaultHttpDataSource createDataSource() { DefaultHttpDataSource dataSource = @@ -202,9 +210,9 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } /** The default connection timeout, in milliseconds. */ - public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; + @UnstableApi public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; /** The default read timeout, in milliseconds. */ - public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; + @UnstableApi public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; private static final String TAG = "DefaultHttpDataSource"; private static final int MAX_REDIRECTS = 20; // Same limit as okhttp. @@ -232,6 +240,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou /** * @deprecated Use {@link DefaultHttpDataSource.Factory} instead. */ + @UnstableApi @SuppressWarnings("deprecation") @Deprecated public DefaultHttpDataSource() { @@ -241,6 +250,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou /** * @deprecated Use {@link DefaultHttpDataSource.Factory} instead. */ + @UnstableApi @SuppressWarnings("deprecation") @Deprecated public DefaultHttpDataSource(@Nullable String userAgent) { @@ -250,6 +260,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou /** * @deprecated Use {@link DefaultHttpDataSource.Factory} instead. */ + @UnstableApi @SuppressWarnings("deprecation") @Deprecated public DefaultHttpDataSource( @@ -265,6 +276,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou /** * @deprecated Use {@link DefaultHttpDataSource.Factory} instead. */ + @UnstableApi @Deprecated public DefaultHttpDataSource( @Nullable String userAgent, @@ -305,22 +317,26 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @deprecated Use {@link DefaultHttpDataSource.Factory#setContentTypePredicate(Predicate)} * instead. */ + @UnstableApi @Deprecated public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) { this.contentTypePredicate = contentTypePredicate; } + @UnstableApi @Override @Nullable public Uri getUri() { return connection == null ? null : Uri.parse(connection.getURL().toString()); } + @UnstableApi @Override public int getResponseCode() { return connection == null || responseCode <= 0 ? -1 : responseCode; } + @UnstableApi @Override public Map> getResponseHeaders() { if (connection == null) { @@ -337,6 +353,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou return new NullFilteringHeadersMap(connection.getHeaderFields()); } + @UnstableApi @Override public void setRequestProperty(String name, String value) { checkNotNull(name); @@ -344,18 +361,21 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou requestProperties.set(name, value); } + @UnstableApi @Override public void clearRequestProperty(String name) { checkNotNull(name); requestProperties.remove(name); } + @UnstableApi @Override public void clearAllRequestProperties() { requestProperties.clear(); } /** Opens the source to read the specified data. */ + @UnstableApi @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { this.dataSpec = dataSpec; @@ -474,6 +494,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou return bytesToRead; } + @UnstableApi @Override public int read(byte[] buffer, int offset, int length) throws HttpDataSourceException { try { @@ -484,6 +505,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } } + @UnstableApi @Override public void close() throws HttpDataSourceException { try { diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java index 967a00b49c..55afcf273e 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java @@ -38,12 +38,12 @@ import java.util.List; import java.util.Map; /** An HTTP {@link DataSource}. */ -@UnstableApi public interface HttpDataSource extends DataSource { /** A factory for {@link HttpDataSource} instances. */ interface Factory extends DataSource.Factory { + @UnstableApi @Override HttpDataSource createDataSource(); @@ -59,6 +59,7 @@ public interface HttpDataSource extends DataSource { * @param defaultRequestProperties The default request properties. * @return This factory. */ + @UnstableApi Factory setDefaultRequestProperties(Map defaultRequestProperties); } @@ -67,6 +68,7 @@ public interface HttpDataSource extends DataSource { * a thread safe way to avoid the potential of creating snapshots of an inconsistent or unintended * state. */ + @UnstableApi final class RequestProperties { private final Map requestProperties; @@ -141,6 +143,7 @@ public interface HttpDataSource extends DataSource { } /** Base implementation of {@link Factory} that sets default request properties. */ + @UnstableApi abstract class BaseFactory implements Factory { private final RequestProperties defaultRequestProperties; @@ -172,6 +175,7 @@ public interface HttpDataSource extends DataSource { } /** A {@link Predicate} that rejects content types often used for pay-walls. */ + @UnstableApi Predicate REJECT_PAYWALL_TYPES = contentType -> { if (contentType == null) { @@ -208,6 +212,7 @@ public interface HttpDataSource extends DataSource { * Returns a {@code HttpDataSourceException} whose error code is assigned according to the cause * and type. */ + @UnstableApi public static HttpDataSourceException createForIOException( IOException cause, DataSpec dataSpec, @Type int type) { @PlaybackException.ErrorCode int errorCode; @@ -231,7 +236,7 @@ public interface HttpDataSource extends DataSource { } /** The {@link DataSpec} associated with the current connection. */ - public final DataSpec dataSpec; + @UnstableApi public final DataSpec dataSpec; public final @Type int type; @@ -239,6 +244,7 @@ public interface HttpDataSource extends DataSource { * @deprecated Use {@link #HttpDataSourceException(DataSpec, int, int) * HttpDataSourceException(DataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, int)}. */ + @UnstableApi @Deprecated public HttpDataSourceException(DataSpec dataSpec, @Type int type) { this(dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, type); @@ -252,6 +258,7 @@ public interface HttpDataSource extends DataSource { * PlaybackException.ErrorCode}. * @param type See {@link Type}. */ + @UnstableApi public HttpDataSourceException( DataSpec dataSpec, @PlaybackException.ErrorCode int errorCode, @Type int type) { super(assignErrorCode(errorCode, type)); @@ -264,6 +271,7 @@ public interface HttpDataSource extends DataSource { * HttpDataSourceException(String, DataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, * int)}. */ + @UnstableApi @Deprecated public HttpDataSourceException(String message, DataSpec dataSpec, @Type int type) { this(message, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, type); @@ -278,6 +286,7 @@ public interface HttpDataSource extends DataSource { * PlaybackException.ErrorCode}. * @param type See {@link Type}. */ + @UnstableApi public HttpDataSourceException( String message, DataSpec dataSpec, @@ -293,6 +302,7 @@ public interface HttpDataSource extends DataSource { * HttpDataSourceException(IOException, DataSpec, * PlaybackException.ERROR_CODE_IO_UNSPECIFIED, int)}. */ + @UnstableApi @Deprecated public HttpDataSourceException(IOException cause, DataSpec dataSpec, @Type int type) { this(cause, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, type); @@ -307,6 +317,7 @@ public interface HttpDataSource extends DataSource { * PlaybackException.ErrorCode}. * @param type See {@link Type}. */ + @UnstableApi public HttpDataSourceException( IOException cause, DataSpec dataSpec, @@ -322,6 +333,7 @@ public interface HttpDataSource extends DataSource { * HttpDataSourceException(String, IOException, DataSpec, * PlaybackException.ERROR_CODE_IO_UNSPECIFIED, int)}. */ + @UnstableApi @Deprecated public HttpDataSourceException( String message, IOException cause, DataSpec dataSpec, @Type int type) { @@ -338,6 +350,7 @@ public interface HttpDataSource extends DataSource { * PlaybackException.ErrorCode}. * @param type See {@link Type}. */ + @UnstableApi public HttpDataSourceException( String message, @Nullable IOException cause, @@ -365,6 +378,7 @@ public interface HttpDataSource extends DataSource { */ final class CleartextNotPermittedException extends HttpDataSourceException { + @UnstableApi public CleartextNotPermittedException(IOException cause, DataSpec dataSpec) { super( "Cleartext HTTP traffic not permitted. See" @@ -381,6 +395,7 @@ public interface HttpDataSource extends DataSource { public final String contentType; + @UnstableApi public InvalidContentTypeException(String contentType, DataSpec dataSpec) { super( "Invalid content type: " + contentType, @@ -412,6 +427,7 @@ public interface HttpDataSource extends DataSource { * @deprecated Use {@link #InvalidResponseCodeException(int, String, IOException, Map, DataSpec, * byte[])}. */ + @UnstableApi @Deprecated public InvalidResponseCodeException( int responseCode, Map> headerFields, DataSpec dataSpec) { @@ -428,6 +444,7 @@ public interface HttpDataSource extends DataSource { * @deprecated Use {@link #InvalidResponseCodeException(int, String, IOException, Map, DataSpec, * byte[])}. */ + @UnstableApi @Deprecated public InvalidResponseCodeException( int responseCode, @@ -443,6 +460,7 @@ public interface HttpDataSource extends DataSource { /* responseBody= */ Util.EMPTY_BYTE_ARRAY); } + @UnstableApi public InvalidResponseCodeException( int responseCode, @Nullable String responseMessage, @@ -470,12 +488,15 @@ public interface HttpDataSource extends DataSource { * (in order of decreasing priority) the {@code dataSpec}, {@link #setRequestProperty} and the * default parameters set in the {@link Factory}. */ + @UnstableApi @Override long open(DataSpec dataSpec) throws HttpDataSourceException; + @UnstableApi @Override void close() throws HttpDataSourceException; + @UnstableApi @Override int read(byte[] buffer, int offset, int length) throws HttpDataSourceException; @@ -490,6 +511,7 @@ public interface HttpDataSource extends DataSource { * @param name The name of the header field. * @param value The value of the field. */ + @UnstableApi void setRequestProperty(String name, String value); /** @@ -498,17 +520,21 @@ public interface HttpDataSource extends DataSource { * * @param name The name of the header field. */ + @UnstableApi void clearRequestProperty(String name); /** Clears all request headers that were set by {@link #setRequestProperty(String, String)}. */ + @UnstableApi void clearAllRequestProperties(); /** * When the source is open, returns the HTTP response status code associated with the last {@link * #open} call. Otherwise, returns a negative value. */ + @UnstableApi int getResponseCode(); + @UnstableApi @Override Map> getResponseHeaders(); } diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/DummyDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/PlaceholderDataSource.java similarity index 77% rename from libraries/datasource/src/main/java/androidx/media3/datasource/DummyDataSource.java rename to libraries/datasource/src/main/java/androidx/media3/datasource/PlaceholderDataSource.java index 0c70595d73..dd29920192 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/DummyDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/PlaceholderDataSource.java @@ -22,14 +22,14 @@ import java.io.IOException; /** A DataSource which provides no data. {@link #open(DataSpec)} throws {@link IOException}. */ @UnstableApi -public final class DummyDataSource implements DataSource { +public final class PlaceholderDataSource implements DataSource { - public static final DummyDataSource INSTANCE = new DummyDataSource(); + public static final PlaceholderDataSource INSTANCE = new PlaceholderDataSource(); - /** A factory that produces {@link DummyDataSource}. */ - public static final Factory FACTORY = DummyDataSource::new; + /** A factory that produces {@link PlaceholderDataSource}. */ + public static final Factory FACTORY = PlaceholderDataSource::new; - private DummyDataSource() {} + private PlaceholderDataSource() {} @Override public void addTransferListener(TransferListener transferListener) { @@ -38,7 +38,7 @@ public final class DummyDataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { - throw new IOException("DummyDataSource cannot be opened"); + throw new IOException("PlaceholderDataSource cannot be opened"); } @Override diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/cache/CacheDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/cache/CacheDataSource.java index 4eae94a1ee..66bb73e4ad 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/cache/CacheDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/cache/CacheDataSource.java @@ -36,8 +36,8 @@ import androidx.media3.datasource.DataSink; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSourceException; import androidx.media3.datasource.DataSpec; -import androidx.media3.datasource.DummyDataSource; import androidx.media3.datasource.FileDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.PriorityDataSource; import androidx.media3.datasource.TeeDataSource; import androidx.media3.datasource.TransferListener; @@ -541,7 +541,7 @@ public final class CacheDataSource implements DataSource { ? new TeeDataSource(upstreamDataSource, cacheWriteDataSink) : null; } else { - this.upstreamDataSource = DummyDataSource.INSTANCE; + this.upstreamDataSource = PlaceholderDataSource.INSTANCE; this.cacheWriteDataSource = null; } this.eventListener = eventListener; diff --git a/libraries/datasource_cronet/README.md b/libraries/datasource_cronet/README.md index 62599879fb..c64f8b3bae 100644 --- a/libraries/datasource_cronet/README.md +++ b/libraries/datasource_cronet/README.md @@ -38,8 +38,9 @@ If your application only needs to play http(s) content, using the Cronet extension is as simple as updating `DataSource.Factory` instantiations in your application code to use `CronetDataSource.Factory`. If your application also needs to play non-http(s) content such as local files, use: + ``` -new DefaultDataSourceFactory( +new DefaultDataSource.Factory( ... /* baseDataSourceFactory= */ new CronetDataSource.Factory(...) ); ``` diff --git a/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetDataSource.java b/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetDataSource.java index 4d5988398e..505a65033c 100644 --- a/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetDataSource.java +++ b/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetDataSource.java @@ -68,7 +68,6 @@ import org.chromium.net.UrlResponseInfo; * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to * construct the instance. */ -@UnstableApi public class CronetDataSource extends BaseDataSource implements HttpDataSource { static { @@ -132,6 +131,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * CronetEngine}, or {@link DefaultHttpDataSource} for cases where {@link * CronetEngineWrapper#getCronetEngine()} would have returned {@code null}. */ + @UnstableApi @Deprecated public Factory(CronetEngineWrapper cronetEngineWrapper, Executor executor) { this.cronetEngine = cronetEngineWrapper.getCronetEngine(); @@ -142,6 +142,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS; } + @UnstableApi @Override public final Factory setDefaultRequestProperties(Map defaultRequestProperties) { this.defaultRequestProperties.clearAndSet(defaultRequestProperties); @@ -161,6 +162,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * agent of the underlying {@link CronetEngine}. * @return This factory. */ + @UnstableApi public Factory setUserAgent(@Nullable String userAgent) { this.userAgent = userAgent; if (internalFallbackFactory != null) { @@ -179,6 +181,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * UrlRequest.Builder#REQUEST_PRIORITY_*} constants. * @return This factory. */ + @UnstableApi public Factory setRequestPriority(int requestPriority) { this.requestPriority = requestPriority; return this; @@ -192,6 +195,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @param connectTimeoutMs The connect timeout, in milliseconds, that will be used. * @return This factory. */ + @UnstableApi public Factory setConnectionTimeoutMs(int connectTimeoutMs) { this.connectTimeoutMs = connectTimeoutMs; if (internalFallbackFactory != null) { @@ -208,6 +212,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. * @return This factory. */ + @UnstableApi public Factory setResetTimeoutOnRedirects(boolean resetTimeoutOnRedirects) { this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; return this; @@ -223,6 +228,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * to the redirect url in the "Cookie" header. * @return This factory. */ + @UnstableApi public Factory setHandleSetCookieRequests(boolean handleSetCookieRequests) { this.handleSetCookieRequests = handleSetCookieRequests; return this; @@ -236,6 +242,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @param readTimeoutMs The connect timeout, in milliseconds, that will be used. * @return This factory. */ + @UnstableApi public Factory setReadTimeoutMs(int readTimeoutMs) { this.readTimeoutMs = readTimeoutMs; if (internalFallbackFactory != null) { @@ -254,6 +261,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * predicate that was previously set. * @return This factory. */ + @UnstableApi public Factory setContentTypePredicate(@Nullable Predicate contentTypePredicate) { this.contentTypePredicate = contentTypePredicate; if (internalFallbackFactory != null) { @@ -266,6 +274,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * Sets whether we should keep the POST method and body when we have HTTP 302 redirects for a * POST request. */ + @UnstableApi public Factory setKeepPostFor302Redirects(boolean keepPostFor302Redirects) { this.keepPostFor302Redirects = keepPostFor302Redirects; if (internalFallbackFactory != null) { @@ -284,6 +293,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @param transferListener The listener that will be used. * @return This factory. */ + @UnstableApi public Factory setTransferListener(@Nullable TransferListener transferListener) { this.transferListener = transferListener; if (internalFallbackFactory != null) { @@ -303,12 +313,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @deprecated Do not use {@link CronetDataSource} or its factory in cases where a suitable * {@link CronetEngine} is not available. Use the fallback factory directly in such cases. */ + @UnstableApi @Deprecated public Factory setFallbackFactory(@Nullable HttpDataSource.Factory fallbackFactory) { this.fallbackFactory = fallbackFactory; return this; } + @UnstableApi @Override public HttpDataSource createDataSource() { if (cronetEngine == null) { @@ -337,6 +349,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } /** Thrown when an error is encountered when trying to open a {@link CronetDataSource}. */ + @UnstableApi public static final class OpenException extends HttpDataSourceException { /** @@ -389,9 +402,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } /** The default connection timeout, in milliseconds. */ - public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; + @UnstableApi public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; /** The default read timeout, in milliseconds. */ - public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; + @UnstableApi public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; /* package */ final UrlRequest.Callback urlRequestCallback; @@ -436,6 +449,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private volatile long currentConnectTimeoutMs; + @UnstableApi protected CronetDataSource( CronetEngine cronetEngine, Executor executor, @@ -473,6 +487,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a * predicate that was previously set. */ + @UnstableApi @Deprecated public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) { this.contentTypePredicate = contentTypePredicate; @@ -480,21 +495,25 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { // HttpDataSource implementation. + @UnstableApi @Override public void setRequestProperty(String name, String value) { requestProperties.set(name, value); } + @UnstableApi @Override public void clearRequestProperty(String name) { requestProperties.remove(name); } + @UnstableApi @Override public void clearAllRequestProperties() { requestProperties.clear(); } + @UnstableApi @Override public int getResponseCode() { return responseInfo == null || responseInfo.getHttpStatusCode() <= 0 @@ -502,17 +521,20 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { : responseInfo.getHttpStatusCode(); } + @UnstableApi @Override public Map> getResponseHeaders() { return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders(); } + @UnstableApi @Override @Nullable public Uri getUri() { return responseInfo == null ? null : Uri.parse(responseInfo.getUrl()); } + @UnstableApi @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { Assertions.checkNotNull(dataSpec); @@ -644,6 +666,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { return bytesRemaining; } + @UnstableApi @Override public int read(byte[] buffer, int offset, int length) throws HttpDataSourceException { Assertions.checkState(opened); @@ -715,6 +738,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @throws HttpDataSourceException If an error occurs reading from the source. * @throws IllegalArgumentException If {@code buffer} is not a direct ByteBuffer. */ + @UnstableApi public int read(ByteBuffer buffer) throws HttpDataSourceException { Assertions.checkState(opened); @@ -759,6 +783,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { return bytesRead; } + @UnstableApi @Override public synchronized void close() { if (currentUrlRequest != null) { @@ -779,17 +804,20 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } /** Returns current {@link UrlRequest}. May be null if the data source is not opened. */ + @UnstableApi @Nullable protected UrlRequest getCurrentUrlRequest() { return currentUrlRequest; } /** Returns current {@link UrlResponseInfo}. May be null if the data source is not opened. */ + @UnstableApi @Nullable protected UrlResponseInfo getCurrentUrlResponseInfo() { return responseInfo; } + @UnstableApi protected UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException { UrlRequest.Builder requestBuilder = cronetEngine diff --git a/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetUtil.java b/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetUtil.java index 6afce324f9..03951c0255 100644 --- a/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetUtil.java +++ b/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetUtil.java @@ -31,7 +31,6 @@ import org.chromium.net.CronetEngine; import org.chromium.net.CronetProvider; /** Cronet utility methods. */ -@UnstableApi public final class CronetUtil { private static final String TAG = "CronetUtil"; @@ -77,6 +76,7 @@ public final class CronetUtil { * over Cronet Embedded, if both are available. * @return The {@link CronetEngine}, or {@code null} if no suitable engine could be built. */ + @UnstableApi @Nullable public static CronetEngine buildCronetEngine( Context context, @Nullable String userAgent, boolean preferGooglePlayServices) { diff --git a/libraries/datasource_okhttp/src/main/java/androidx/media3/datasource/okhttp/OkHttpDataSource.java b/libraries/datasource_okhttp/src/main/java/androidx/media3/datasource/okhttp/OkHttpDataSource.java index 889c5f6d6a..42ed422550 100644 --- a/libraries/datasource_okhttp/src/main/java/androidx/media3/datasource/okhttp/OkHttpDataSource.java +++ b/libraries/datasource_okhttp/src/main/java/androidx/media3/datasource/okhttp/OkHttpDataSource.java @@ -60,7 +60,6 @@ import okhttp3.ResponseBody; * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to * construct the instance. */ -@UnstableApi public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { static { @@ -89,6 +88,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { defaultRequestProperties = new RequestProperties(); } + @UnstableApi @Override public final Factory setDefaultRequestProperties(Map defaultRequestProperties) { this.defaultRequestProperties.clearAndSet(defaultRequestProperties); @@ -105,6 +105,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { * agent of the underlying {@link OkHttpClient}. * @return This factory. */ + @UnstableApi public Factory setUserAgent(@Nullable String userAgent) { this.userAgent = userAgent; return this; @@ -118,6 +119,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { * @param cacheControl The cache control that will be used. * @return This factory. */ + @UnstableApi public Factory setCacheControl(@Nullable CacheControl cacheControl) { this.cacheControl = cacheControl; return this; @@ -134,6 +136,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { * predicate that was previously set. * @return This factory. */ + @UnstableApi public Factory setContentTypePredicate(@Nullable Predicate contentTypePredicate) { this.contentTypePredicate = contentTypePredicate; return this; @@ -149,11 +152,13 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { * @param transferListener The listener that will be used. * @return This factory. */ + @UnstableApi public Factory setTransferListener(@Nullable TransferListener transferListener) { this.transferListener = transferListener; return this; } + @UnstableApi @Override public OkHttpDataSource createDataSource() { OkHttpDataSource dataSource = @@ -185,6 +190,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { * @deprecated Use {@link OkHttpDataSource.Factory} instead. */ @SuppressWarnings("deprecation") + @UnstableApi @Deprecated public OkHttpDataSource(Call.Factory callFactory) { this(callFactory, /* userAgent= */ null); @@ -194,6 +200,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { * @deprecated Use {@link OkHttpDataSource.Factory} instead. */ @SuppressWarnings("deprecation") + @UnstableApi @Deprecated public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) { this(callFactory, userAgent, /* cacheControl= */ null, /* defaultRequestProperties= */ null); @@ -202,6 +209,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { /** * @deprecated Use {@link OkHttpDataSource.Factory} instead. */ + @UnstableApi @Deprecated public OkHttpDataSource( Call.Factory callFactory, @@ -234,27 +242,32 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { /** * @deprecated Use {@link OkHttpDataSource.Factory#setContentTypePredicate(Predicate)} instead. */ + @UnstableApi @Deprecated public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) { this.contentTypePredicate = contentTypePredicate; } + @UnstableApi @Override @Nullable public Uri getUri() { return response == null ? null : Uri.parse(response.request().url().toString()); } + @UnstableApi @Override public int getResponseCode() { return response == null ? -1 : response.code(); } + @UnstableApi @Override public Map> getResponseHeaders() { return response == null ? Collections.emptyMap() : response.headers().toMultimap(); } + @UnstableApi @Override public void setRequestProperty(String name, String value) { Assertions.checkNotNull(name); @@ -262,17 +275,20 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { requestProperties.set(name, value); } + @UnstableApi @Override public void clearRequestProperty(String name) { Assertions.checkNotNull(name); requestProperties.remove(name); } + @UnstableApi @Override public void clearAllRequestProperties() { requestProperties.clear(); } + @UnstableApi @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { this.dataSpec = dataSpec; @@ -358,6 +374,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { return bytesToRead; } + @UnstableApi @Override public int read(byte[] buffer, int offset, int length) throws HttpDataSourceException { try { @@ -368,6 +385,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { } } + @UnstableApi @Override public void close() { if (opened) { diff --git a/libraries/datasource_rtmp/README.md b/libraries/datasource_rtmp/README.md index 8e69321c95..7f52a665b3 100644 --- a/libraries/datasource_rtmp/README.md +++ b/libraries/datasource_rtmp/README.md @@ -39,7 +39,7 @@ injected from application code. `DefaultDataSource` will automatically use the RTMP extension whenever it's available. Hence if your application is using `DefaultDataSource` or -`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as +`DefaultDataSource.Factory`, adding support for RTMP streams is as simple as adding a dependency to the RTMP extension as described above. No changes to your application code are required. Alternatively, if you know that your application doesn't need to handle any other protocols, you can update any diff --git a/libraries/decoder_ffmpeg/build.gradle b/libraries/decoder_ffmpeg/build.gradle index d80695c936..b404aba0d4 100644 --- a/libraries/decoder_ffmpeg/build.gradle +++ b/libraries/decoder_ffmpeg/build.gradle @@ -17,7 +17,8 @@ apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" // failures if ffmpeg hasn't been built according to the README instructions. if (project.file('src/main/jni/ffmpeg').exists()) { android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt' - android.externalNativeBuild.cmake.version = '3.7.1+' + // Should match cmake_minimum_required. + android.externalNativeBuild.cmake.version = '3.21.0+' } dependencies { diff --git a/libraries/decoder_ffmpeg/src/main/jni/CMakeLists.txt b/libraries/decoder_ffmpeg/src/main/jni/CMakeLists.txt index c6a89975cf..9b41852481 100644 --- a/libraries/decoder_ffmpeg/src/main/jni/CMakeLists.txt +++ b/libraries/decoder_ffmpeg/src/main/jni/CMakeLists.txt @@ -14,7 +14,7 @@ # limitations under the License. # -cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR) +cmake_minimum_required(VERSION 3.21.0 FATAL_ERROR) # Enable C++11 features. set(CMAKE_CXX_STANDARD 11) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultLoadControl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultLoadControl.java index 978a9bc829..196d26098d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultLoadControl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultLoadControl.java @@ -21,11 +21,11 @@ import static java.lang.Math.min; import androidx.annotation.Nullable; import androidx.media3.common.C; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.DefaultAllocator; 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 230fc2e03b..b817109c2b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java @@ -171,25 +171,6 @@ public class DefaultRenderersFactory implements RenderersFactory { return this; } - /** - * Enable calling {@link MediaCodec#start} immediately after {@link MediaCodec#flush} on the - * playback thread, when operating the codec in asynchronous mode. If disabled, {@link - * MediaCodec#start} will be called by the callback thread after pending callbacks are handled. - * - *

By default, this feature is disabled. - * - *

This method is experimental, and will be renamed or removed in a future release. - * - * @param enabled Whether {@link MediaCodec#start} will be called on the playback thread - * immediately after {@link MediaCodec#flush}. - * @return This factory, for convenience. - */ - public DefaultRenderersFactory experimentalSetImmediateCodecStartAfterFlushEnabled( - boolean enabled) { - codecAdapterFactory.experimentalSetImmediateCodecStartAfterFlushEnabled(enabled); - return this; - } - /** * Sets whether to enable fallback to lower-priority decoders if decoder initialization fails. * This may result in using a decoder that is less efficient or slower than the primary decoder. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index d367de7648..eda8f6c146 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -38,6 +38,7 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.common.PriorityTaskManager; import androidx.media3.common.Timeline; +import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; import androidx.media3.common.text.Cue; import androidx.media3.common.util.Clock; @@ -54,8 +55,10 @@ import androidx.media3.exoplayer.metadata.MetadataRenderer; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.ShuffleOrder; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.text.TextRenderer; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; +import androidx.media3.exoplayer.trackselection.TrackSelectionArray; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.exoplayer.upstream.BandwidthMeter; import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter; @@ -1216,6 +1219,27 @@ public interface ExoPlayer extends Player { @Nullable TrackSelector getTrackSelector(); + /** + * Returns the available track groups. + * + * @see Listener#onTracksInfoChanged(TracksInfo) + * @deprecated Use {@link #getCurrentTracksInfo()}. + */ + @UnstableApi + @Deprecated + TrackGroupArray getCurrentTrackGroups(); + + /** + * Returns the current track selections for each renderer, which may include {@code null} elements + * if some renderers do not have any selected tracks. + * + * @see Listener#onTracksInfoChanged(TracksInfo) + * @deprecated Use {@link #getCurrentTracksInfo()}. + */ + @UnstableApi + @Deprecated + TrackSelectionArray getCurrentTrackSelections(); + /** Returns the {@link Looper} associated with the playback thread. */ @UnstableApi Looper getPlaybackLooper(); 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 930349e302..0e6428c0ee 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -69,8 +69,6 @@ import androidx.media3.common.Player; import androidx.media3.common.PriorityTaskManager; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; @@ -93,8 +91,10 @@ import androidx.media3.exoplayer.metadata.MetadataOutput; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.ShuffleOrder; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.text.TextOutput; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; +import androidx.media3.exoplayer.trackselection.TrackSelectionArray; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.exoplayer.trackselection.TrackSelectorResult; import androidx.media3.exoplayer.upstream.BandwidthMeter; @@ -1896,11 +1896,6 @@ import java.util.concurrent.TimeoutException; } if (previousPlaybackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult) { trackSelector.onSelectionActivated(newPlaybackInfo.trackSelectorResult.info); - TrackSelectionArray newSelection = - new TrackSelectionArray(newPlaybackInfo.trackSelectorResult.selections); - listeners.queueEvent( - Player.EVENT_TRACKS_CHANGED, - listener -> listener.onTracksChanged(newPlaybackInfo.trackGroups, newSelection)); listeners.queueEvent( Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksInfoChanged(newPlaybackInfo.trackSelectorResult.tracksInfo)); 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 99896816ae..ba32561c63 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -44,7 +44,6 @@ import androidx.media3.common.Player.PlayWhenReadyChangeReason; import androidx.media3.common.Player.PlaybackSuppressionReason; import androidx.media3.common.Player.RepeatMode; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Clock; import androidx.media3.common.util.HandlerWrapper; @@ -56,11 +55,13 @@ import androidx.media3.exoplayer.DefaultMediaClock.PlaybackParametersListener; import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.drm.DrmSession; +import androidx.media3.exoplayer.metadata.MetadataRenderer; import androidx.media3.exoplayer.source.BehindLiveWindowException; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.ShuffleOrder; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.text.TextRenderer; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.TrackSelector; @@ -2228,6 +2229,7 @@ import java.util.concurrent.atomic.AtomicBoolean; return reading.info.isFollowedByTransitionToSameStream && nextPeriod.prepared && (renderer instanceof TextRenderer // [internal: b/181312195] + || renderer instanceof MetadataRenderer || renderer.getReadingPositionUs() >= nextPeriod.getStartPositionRendererTime()); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadControl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadControl.java index 62e41a6ba3..8f8c37c8d4 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadControl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/LoadControl.java @@ -18,8 +18,8 @@ package androidx.media3.exoplayer; import androidx.media3.common.C; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.Allocator; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java index fac91eb349..d16412f219 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodHolder.java @@ -21,7 +21,6 @@ import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; import androidx.media3.exoplayer.source.ClippingMediaPeriod; @@ -29,6 +28,7 @@ import androidx.media3.exoplayer.source.EmptySampleStream; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.SampleStream; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.exoplayer.trackselection.TrackSelectorResult; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java index a0731462bb..f812c11ed5 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java @@ -925,7 +925,8 @@ import com.google.common.collect.ImmutableList; : endPositionUs; if (durationUs != C.TIME_UNSET && startPositionUs >= durationUs) { // Ensure start position doesn't exceed duration. - startPositionUs = max(0, durationUs - 1); + boolean endAtLastFrame = isLastInTimeline || !clipPeriodAtContentDuration; + startPositionUs = max(0, durationUs - (endAtLastFrame ? 1 : 0)); } return new MediaPeriodInfo( id, diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java index ad49de0654..9a58981f5d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MetadataRetriever.java @@ -26,7 +26,6 @@ import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Clock; import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.UnstableApi; @@ -34,6 +33,7 @@ import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.extractor.DefaultExtractorsFactory; @@ -157,7 +157,7 @@ public final class MetadataRetriever { mediaPeriod.maybeThrowPrepareError(); } mediaSourceHandler.sendEmptyMessageDelayed( - MESSAGE_CHECK_FOR_FAILURE, /* delayMillis= */ ERROR_POLL_INTERVAL_MS); + MESSAGE_CHECK_FOR_FAILURE, /* delayMs= */ ERROR_POLL_INTERVAL_MS); } catch (Exception e) { trackGroupsFuture.setException(e); mediaSourceHandler.obtainMessage(MESSAGE_RELEASE).sendToTarget(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java index 6dab072666..f0e104a75a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java @@ -23,8 +23,8 @@ import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Player.PlaybackSuppressionReason; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroupArray; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.TrackSelectorResult; import com.google.common.collect.ImmutableList; import java.util.List; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java index d791222b9d..861adf766c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java @@ -35,8 +35,6 @@ import androidx.media3.common.MediaMetadata; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.PriorityTaskManager; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; @@ -49,6 +47,8 @@ import androidx.media3.exoplayer.analytics.AnalyticsListener; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.ShuffleOrder; +import androidx.media3.exoplayer.source.TrackGroupArray; +import androidx.media3.exoplayer.trackselection.TrackSelectionArray; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.exoplayer.upstream.BandwidthMeter; import androidx.media3.exoplayer.video.VideoFrameMetadataListener; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsListener.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsListener.java index b4900c904a..fad26fe0d8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsListener.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsListener.java @@ -45,7 +45,6 @@ import androidx.media3.common.Player.DiscontinuityReason; import androidx.media3.common.Player.PlaybackSuppressionReason; import androidx.media3.common.Player.TimelineChangeReason; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackSelection; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; @@ -60,6 +59,7 @@ import androidx.media3.exoplayer.metadata.MetadataOutput; import androidx.media3.exoplayer.source.LoadEventInfo; import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; +import androidx.media3.exoplayer.trackselection.TrackSelection; import androidx.media3.exoplayer.video.VideoDecoderOutputBufferRenderer; import com.google.common.base.Objects; import java.io.IOException; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollector.java index 625753d85d..65e307439b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollector.java @@ -38,8 +38,6 @@ import androidx.media3.common.Player.PlaybackSuppressionReason; import androidx.media3.common.Timeline; import androidx.media3.common.Timeline.Period; import androidx.media3.common.Timeline.Window; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; @@ -484,13 +482,6 @@ public class DefaultAnalyticsCollector implements AnalyticsCollector { listener -> listener.onMediaItemTransition(eventTime, mediaItem, reason)); } - @SuppressWarnings("deprecation") // Implementing deprecated method. - @Override - public final void onTracksChanged( - TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - // Do nothing. Handled by non-deprecated onTracksInfoChanged. - } - @Override public void onTracksInfoChanged(TracksInfo tracksInfo) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 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 620278baad..70b54350f8 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 @@ -1745,8 +1745,11 @@ public final class DefaultAudioSink implements AudioSink { // the channel count for this encoding, but before then there is no way to query it so we // assume 6 channel audio is supported. if (Util.SDK_INT >= 29) { + // Default to 48 kHz if the format doesn't have a sample rate (for example, for chunkless + // HLS preparation). See [Internal: b/222127949]. + int sampleRate = format.sampleRate != Format.NO_VALUE ? format.sampleRate : 48000; channelCount = - getMaxSupportedChannelCountForPassthroughV29(C.ENCODING_E_AC3_JOC, format.sampleRate); + getMaxSupportedChannelCountForPassthroughV29(C.ENCODING_E_AC3_JOC, sampleRate); if (channelCount == 0) { Log.w(TAG, "E-AC3 JOC encoding supported but no channel count supported"); return null; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManagerProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManagerProvider.java index a67cb15336..01a0f78385 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManagerProvider.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManagerProvider.java @@ -24,8 +24,8 @@ import androidx.annotation.RequiresApi; import androidx.media3.common.MediaItem; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DefaultHttpDataSource; -import androidx.media3.datasource.HttpDataSource; import com.google.common.primitives.Ints; import java.util.Map; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -42,7 +42,7 @@ public final class DefaultDrmSessionManagerProvider implements DrmSessionManager @GuardedBy("lock") private @MonotonicNonNull DrmSessionManager manager; - @Nullable private HttpDataSource.Factory drmHttpDataSourceFactory; + @Nullable private DataSource.Factory drmHttpDataSourceFactory; @Nullable private String userAgent; public DefaultDrmSessionManagerProvider() { @@ -50,26 +50,22 @@ public final class DefaultDrmSessionManagerProvider implements DrmSessionManager } /** - * Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback - * HttpMediaDrmCallbacks} which executes key and provisioning requests over HTTP. If {@code null} - * is passed the {@link DefaultHttpDataSource.Factory} is used. + * Sets the {@link DataSource.Factory} which is used to create {@link HttpMediaDrmCallback} + * instances. If {@code null} is passed a {@link DefaultHttpDataSource.Factory} is used. * - * @param drmHttpDataSourceFactory The HTTP data source factory or {@code null} to use {@link + * @param drmDataSourceFactory The data source factory or {@code null} to use {@link * DefaultHttpDataSource.Factory}. */ - public void setDrmHttpDataSourceFactory( - @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { - this.drmHttpDataSourceFactory = drmHttpDataSourceFactory; + public void setDrmHttpDataSourceFactory(@Nullable DataSource.Factory drmDataSourceFactory) { + this.drmHttpDataSourceFactory = drmDataSourceFactory; } /** - * Sets the optional user agent to be used for DRM requests. - * - *

In case a factory has been set by {@link - * #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}, this user agent is ignored. - * - * @param userAgent The user agent to be used for DRM requests. + * @deprecated Pass a custom {@link DataSource.Factory} to {@link + * #setDrmHttpDataSourceFactory(DataSource.Factory)} which sets the desired user agent on + * outgoing requests. */ + @Deprecated public void setDrmUserAgent(@Nullable String userAgent) { this.userAgent = userAgent; } @@ -94,7 +90,7 @@ public final class DefaultDrmSessionManagerProvider implements DrmSessionManager @RequiresApi(18) private DrmSessionManager createManager(MediaItem.DrmConfiguration drmConfiguration) { - HttpDataSource.Factory dataSourceFactory = + DataSource.Factory dataSourceFactory = drmHttpDataSourceFactory != null ? drmHttpDataSourceFactory : new DefaultHttpDataSource.Factory().setUserAgent(userAgent); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java index 10fda5099a..9489014fea 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/FrameworkMediaDrm.java @@ -192,7 +192,11 @@ public final class FrameworkMediaDrm implements ExoMediaDrm { @Override public void setPlayerIdForSession(byte[] sessionId, PlayerId playerId) { if (Util.SDK_INT >= 31) { - Api31.setLogSessionIdOnMediaDrmSession(mediaDrm, sessionId, playerId); + try { + Api31.setLogSessionIdOnMediaDrmSession(mediaDrm, sessionId, playerId); + } catch (UnsupportedOperationException e) { + Log.w(TAG, "setLogSessionId failed."); + } } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/HttpMediaDrmCallback.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/HttpMediaDrmCallback.java index 18a1e63696..95eb55e4d6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/HttpMediaDrmCallback.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/HttpMediaDrmCallback.java @@ -22,9 +22,9 @@ import androidx.media3.common.C; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSourceInputStream; import androidx.media3.datasource.DataSpec; -import androidx.media3.datasource.HttpDataSource; import androidx.media3.datasource.HttpDataSource.InvalidResponseCodeException; import androidx.media3.datasource.StatsDataSource; import androidx.media3.exoplayer.drm.ExoMediaDrm.KeyRequest; @@ -36,41 +36,47 @@ import java.util.List; import java.util.Map; import java.util.UUID; -/** A {@link MediaDrmCallback} that makes requests using {@link HttpDataSource} instances. */ +/** A {@link MediaDrmCallback} that makes requests using {@link DataSource} instances. */ @UnstableApi public final class HttpMediaDrmCallback implements MediaDrmCallback { private static final int MAX_MANUAL_REDIRECTS = 5; - private final HttpDataSource.Factory dataSourceFactory; + private final DataSource.Factory dataSourceFactory; @Nullable private final String defaultLicenseUrl; private final boolean forceDefaultLicenseUrl; private final Map keyRequestProperties; /** + * Constructs an instance. + * * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify * their own license URL. May be {@code null} if it's known that all key requests will specify * their own URLs. - * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @param dataSourceFactory A factory from which to obtain {@link DataSource} instances. This will + * usually be an HTTP-based {@link DataSource}. */ public HttpMediaDrmCallback( - @Nullable String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) { + @Nullable String defaultLicenseUrl, DataSource.Factory dataSourceFactory) { this(defaultLicenseUrl, /* forceDefaultLicenseUrl= */ false, dataSourceFactory); } /** + * Constructs an instance. + * * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify * their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is set to * true. May be {@code null} if {@code forceDefaultLicenseUrl} is {@code false} and if it's * known that all key requests will specify their own URLs. * @param forceDefaultLicenseUrl Whether to force use of {@code defaultLicenseUrl} for key * requests that include their own license URL. - * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @param dataSourceFactory A factory from which to obtain {@link DataSource} instances. This will + * * usually be an HTTP-based {@link DataSource}. */ public HttpMediaDrmCallback( @Nullable String defaultLicenseUrl, boolean forceDefaultLicenseUrl, - HttpDataSource.Factory dataSourceFactory) { + DataSource.Factory dataSourceFactory) { Assertions.checkArgument(!(forceDefaultLicenseUrl && TextUtils.isEmpty(defaultLicenseUrl))); this.dataSourceFactory = dataSourceFactory; this.defaultLicenseUrl = defaultLicenseUrl; @@ -156,7 +162,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { } private static byte[] executePost( - HttpDataSource.Factory dataSourceFactory, + DataSource.Factory dataSourceFactory, String url, @Nullable byte[] httpBody, Map requestProperties) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java index cae26bbf4e..3da5a1dbcd 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java @@ -26,7 +26,7 @@ import androidx.media3.common.DrmInitData; import androidx.media3.common.Format; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; -import androidx.media3.datasource.HttpDataSource; +import androidx.media3.datasource.DataSource; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.drm.DefaultDrmSessionManager.Mode; import androidx.media3.exoplayer.drm.DrmSession.DrmSessionException; @@ -53,20 +53,17 @@ public final class OfflineLicenseHelper { * * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify * their own license URL. - * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @param dataSourceFactory A factory from which to obtain {@link DataSource} instances. * @param eventDispatcher A {@link DrmSessionEventListener.EventDispatcher} used to distribute * DRM-related events. * @return A new instance which uses Widevine CDM. */ public static OfflineLicenseHelper newWidevineInstance( String defaultLicenseUrl, - HttpDataSource.Factory httpDataSourceFactory, + DataSource.Factory dataSourceFactory, DrmSessionEventListener.EventDispatcher eventDispatcher) { return newWidevineInstance( - defaultLicenseUrl, - /* forceDefaultLicenseUrl= */ false, - httpDataSourceFactory, - eventDispatcher); + defaultLicenseUrl, /* forceDefaultLicenseUrl= */ false, dataSourceFactory, eventDispatcher); } /** @@ -77,7 +74,7 @@ public final class OfflineLicenseHelper { * their own license URL. * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that * include their own license URL. - * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @param dataSourceFactory A factory from which to obtain {@link DataSource} instances. * @param eventDispatcher A {@link DrmSessionEventListener.EventDispatcher} used to distribute * DRM-related events. * @return A new instance which uses Widevine CDM. @@ -85,12 +82,12 @@ public final class OfflineLicenseHelper { public static OfflineLicenseHelper newWidevineInstance( String defaultLicenseUrl, boolean forceDefaultLicenseUrl, - HttpDataSource.Factory httpDataSourceFactory, + DataSource.Factory dataSourceFactory, DrmSessionEventListener.EventDispatcher eventDispatcher) { return newWidevineInstance( defaultLicenseUrl, forceDefaultLicenseUrl, - httpDataSourceFactory, + dataSourceFactory, /* optionalKeyRequestParameters= */ null, eventDispatcher); } @@ -113,7 +110,7 @@ public final class OfflineLicenseHelper { public static OfflineLicenseHelper newWidevineInstance( String defaultLicenseUrl, boolean forceDefaultLicenseUrl, - HttpDataSource.Factory httpDataSourceFactory, + DataSource.Factory dataSourceFactory, @Nullable Map optionalKeyRequestParameters, DrmSessionEventListener.EventDispatcher eventDispatcher) { return new OfflineLicenseHelper( @@ -121,7 +118,7 @@ public final class OfflineLicenseHelper { .setKeyRequestParameters(optionalKeyRequestParameters) .build( new HttpMediaDrmCallback( - defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory)), + defaultLicenseUrl, forceDefaultLicenseUrl, dataSourceFactory)), eventDispatcher); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/UnsupportedDrmException.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/UnsupportedDrmException.java index 4530eab122..2b70a51439 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/UnsupportedDrmException.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/UnsupportedDrmException.java @@ -24,6 +24,8 @@ import static java.lang.annotation.ElementType.TYPE_USE; import androidx.annotation.IntDef; import androidx.media3.common.util.UnstableApi; import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** Thrown when the requested DRM scheme is not supported. */ @@ -35,8 +37,9 @@ public final class UnsupportedDrmException extends Exception { * #REASON_INSTANTIATION_ERROR}. */ // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility - // with Kotlin usages from before TYPE_USE was added. @Retention(RetentionPolicy.SOURCE) + // with Kotlin usages from before TYPE_USE was added. @Documented + @Retention(RetentionPolicy.SOURCE) @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) @IntDef({REASON_UNSUPPORTED_SCHEME, REASON_INSTANTIATION_ERROR}) public @interface Reason {} 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 e013c4b990..91aa86ca06 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 @@ -54,7 +54,6 @@ import java.nio.ByteBuffer; private final Supplier callbackThreadSupplier; private final Supplier queueingThreadSupplier; private final boolean synchronizeCodecInteractionsWithQueueing; - private final boolean enableImmediateCodecStartAfterFlush; /** * Creates an factory for {@link AsynchronousMediaCodecAdapter} instances. @@ -66,29 +65,23 @@ import java.nio.ByteBuffer; * interactions will wait until all input buffers pending queueing wil be submitted to the * {@link MediaCodec}. */ - public Factory( - @C.TrackType int trackType, - boolean synchronizeCodecInteractionsWithQueueing, - boolean enableImmediateCodecStartAfterFlush) { + public Factory(@C.TrackType int trackType, boolean synchronizeCodecInteractionsWithQueueing) { this( /* callbackThreadSupplier= */ () -> new HandlerThread(createCallbackThreadLabel(trackType)), /* queueingThreadSupplier= */ () -> new HandlerThread(createQueueingThreadLabel(trackType)), - synchronizeCodecInteractionsWithQueueing, - enableImmediateCodecStartAfterFlush); + synchronizeCodecInteractionsWithQueueing); } @VisibleForTesting /* package */ Factory( Supplier callbackThreadSupplier, Supplier queueingThreadSupplier, - boolean synchronizeCodecInteractionsWithQueueing, - boolean enableImmediateCodecStartAfterFlush) { + boolean synchronizeCodecInteractionsWithQueueing) { this.callbackThreadSupplier = callbackThreadSupplier; this.queueingThreadSupplier = queueingThreadSupplier; this.synchronizeCodecInteractionsWithQueueing = synchronizeCodecInteractionsWithQueueing; - this.enableImmediateCodecStartAfterFlush = enableImmediateCodecStartAfterFlush; } @Override @@ -105,8 +98,7 @@ import java.nio.ByteBuffer; codec, callbackThreadSupplier.get(), queueingThreadSupplier.get(), - synchronizeCodecInteractionsWithQueueing, - enableImmediateCodecStartAfterFlush); + synchronizeCodecInteractionsWithQueueing); TraceUtil.endSection(); codecAdapter.initialize( configuration.mediaFormat, @@ -139,7 +131,6 @@ import java.nio.ByteBuffer; private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback; private final AsynchronousMediaCodecBufferEnqueuer bufferEnqueuer; private final boolean synchronizeCodecInteractionsWithQueueing; - private final boolean enableImmediateCodecStartAfterFlush; private boolean codecReleased; private @State int state; @@ -147,13 +138,11 @@ import java.nio.ByteBuffer; MediaCodec codec, HandlerThread callbackThread, HandlerThread enqueueingThread, - boolean synchronizeCodecInteractionsWithQueueing, - boolean enableImmediateCodecStartAfterFlush) { + boolean synchronizeCodecInteractionsWithQueueing) { this.codec = codec; this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread); this.bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, enqueueingThread); this.synchronizeCodecInteractionsWithQueueing = synchronizeCodecInteractionsWithQueueing; - this.enableImmediateCodecStartAfterFlush = enableImmediateCodecStartAfterFlush; this.state = STATE_CREATED; } @@ -232,18 +221,13 @@ import java.nio.ByteBuffer; // The order of calls is important: // 1. Flush the bufferEnqueuer to stop queueing input buffers. // 2. Flush the codec to stop producing available input/output buffers. - // 3. Flush the callback after flushing the codec so that in-flight callbacks are discarded. + // 3. Flush the callback so that in-flight callbacks are discarded. + // 4. Start the codec. The asynchronous callback will drop pending callbacks and we can start + // the codec now. bufferEnqueuer.flush(); codec.flush(); - if (enableImmediateCodecStartAfterFlush) { - // The asynchronous callback will drop pending callbacks but we can start the codec now. - asynchronousMediaCodecCallback.flush(/* codec= */ null); - codec.start(); - } else { - // Let the asynchronous callback start the codec in the callback thread after pending - // callbacks are handled. - asynchronousMediaCodecCallback.flush(codec); - } + asynchronousMediaCodecCallback.flush(); + codec.start(); } @Override diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java index a3c5d44d6d..67c6649d27 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java @@ -191,14 +191,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Initiates a flush asynchronously, which will be completed on the callback thread. When the * flush is complete, it will trigger {@code onFlushCompleted} from the callback thread. - * - * @param codec A {@link MediaCodec} to {@link MediaCodec#start start} after all pending callbacks - * are handled, or {@code null} if starting the {@link MediaCodec} is performed elsewhere. */ - public void flush(@Nullable MediaCodec codec) { + public void flush() { synchronized (lock) { ++pendingFlushCount; - Util.castNonNull(handler).post(() -> this.onFlushCompleted(codec)); + Util.castNonNull(handler).post(this::onFlushCompleted); } } @@ -238,7 +235,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } - private void onFlushCompleted(@Nullable MediaCodec codec) { + private void onFlushCompleted() { synchronized (lock) { if (shutDown) { return; @@ -254,15 +251,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return; } flushInternal(); - if (codec != null) { - try { - codec.start(); - } catch (IllegalStateException e) { - setInternalException(e); - } catch (Exception e) { - setInternalException(new IllegalStateException(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 03e5aafd44..d5de1d0bdd 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 @@ -17,7 +17,6 @@ package androidx.media3.exoplayer.mediacodec; import static java.lang.annotation.ElementType.TYPE_USE; -import android.media.MediaCodec; import androidx.annotation.IntDef; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Log; @@ -55,11 +54,9 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. private @Mode int asynchronousMode; private boolean enableSynchronizeCodecInteractionsWithQueueing; - private boolean enableImmediateCodecStartAfterFlush; public DefaultMediaCodecAdapterFactory() { asynchronousMode = MODE_DEFAULT; - enableImmediateCodecStartAfterFlush = true; } /** @@ -96,27 +93,12 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. enableSynchronizeCodecInteractionsWithQueueing = enabled; } - /** - * Enable calling {@link MediaCodec#start} immediately after {@link MediaCodec#flush} on the - * playback thread, when operating the codec in asynchronous mode. If disabled, {@link - * MediaCodec#start} will be called by the callback thread after pending callbacks are handled. - * - *

By default, this feature is enabled. - * - *

This method is experimental, and will be renamed or removed in a future release. - * - * @param enabled Whether {@link MediaCodec#start()} will be called on the playback thread - * immediately after {@link MediaCodec#flush}. - */ - public void experimentalSetImmediateCodecStartAfterFlushEnabled(boolean enabled) { - enableImmediateCodecStartAfterFlush = enabled; - } - @Override public MediaCodecAdapter createAdapter(MediaCodecAdapter.Configuration configuration) throws IOException { - if ((asynchronousMode == MODE_ENABLED && Util.SDK_INT >= 23) - || (asynchronousMode == MODE_DEFAULT && Util.SDK_INT >= 31)) { + if (Util.SDK_INT >= 23 + && (asynchronousMode == MODE_ENABLED + || (asynchronousMode == MODE_DEFAULT && Util.SDK_INT >= 31))) { int trackType = MimeTypes.getTrackType(configuration.format.sampleMimeType); Log.i( TAG, @@ -124,9 +106,7 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. + Util.getTrackTypeString(trackType)); AsynchronousMediaCodecAdapter.Factory factory = new AsynchronousMediaCodecAdapter.Factory( - trackType, - enableSynchronizeCodecInteractionsWithQueueing, - enableImmediateCodecStartAfterFlush); + trackType, enableSynchronizeCodecInteractionsWithQueueing); return factory.createAdapter(configuration); } return new SynchronousMediaCodecAdapter.Factory().createAdapter(configuration); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java index ca24d21b20..c3200150d0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java @@ -443,6 +443,8 @@ public final class MediaCodecUtil { return "audio/x-lg-alac"; } else if (mimeType.equals(MimeTypes.AUDIO_FLAC) && "OMX.lge.flac.decoder".equals(name)) { return "audio/x-lg-flac"; + } else if (mimeType.equals(MimeTypes.AUDIO_AC3) && "OMX.lge.ac3.decoder".equals(name)) { + return "audio/lg-ac3"; } return null; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java index bf67d93ff5..7f9b5a9f15 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java @@ -31,7 +31,9 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; +import androidx.media3.common.TrackSelectionOverride; +import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.TracksInfo; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -49,14 +51,15 @@ import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller; +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.DefaultTrackSelector; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.Parameters; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo; +import androidx.media3.exoplayer.trackselection.TrackSelectionUtil; import androidx.media3.exoplayer.trackselection.TrackSelectorResult; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.BandwidthMeter; @@ -85,8 +88,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *

  • Prepare the helper using {@link #prepare(Callback)} and wait for the callback. *
  • Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link * #getTrackSelections(int, int)}, and make adjustments using {@link - * #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link - * #addTrackSelection(int, Parameters)}. + * #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, TrackSelectionParameters)} + * and {@link #addTrackSelection(int, TrackSelectionParameters)}. *
  • Create a download request for the selected track using {@link #getDownloadRequest(byte[])}. *
  • Release the helper using {@link #release()}. * @@ -100,30 +103,18 @@ public final class DownloadHelper { * *

    If possible, use {@link #getDefaultTrackSelectorParameters(Context)} instead. * - * @see Parameters#DEFAULT_WITHOUT_CONTEXT + * @see DefaultTrackSelector.Parameters#DEFAULT_WITHOUT_CONTEXT */ - public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT = - Parameters.DEFAULT_WITHOUT_CONTEXT.buildUpon().setForceHighestSupportedBitrate(true).build(); - - /** - * @deprecated This instance does not have {@link Context} constraints. Use {@link - * #getDefaultTrackSelectorParameters(Context)} instead. - */ - @Deprecated - public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT = - DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT; - - /** - * @deprecated This instance does not have {@link Context} constraints. Use {@link - * #getDefaultTrackSelectorParameters(Context)} instead. - */ - @Deprecated - public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS = - DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT; + public static final DefaultTrackSelector.Parameters + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT = + DefaultTrackSelector.Parameters.DEFAULT_WITHOUT_CONTEXT + .buildUpon() + .setForceHighestSupportedBitrate(true) + .build(); /** Returns the default parameters used for track selection for downloading. */ public static DefaultTrackSelector.Parameters getDefaultTrackSelectorParameters(Context context) { - return Parameters.getDefaults(context) + return DefaultTrackSelector.Parameters.getDefaults(context) .buildUpon() .setForceHighestSupportedBitrate(true) .build(); @@ -191,7 +182,7 @@ public final class DownloadHelper { } /** - * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory, + * @deprecated Use {@link #forMediaItem(MediaItem, TrackSelectionParameters, RenderersFactory, * DataSource.Factory)} instead. */ @SuppressWarnings("deprecation") @@ -210,7 +201,7 @@ public final class DownloadHelper { } /** - * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory, + * @deprecated Use {@link #forMediaItem(MediaItem, TrackSelectionParameters, RenderersFactory, * DataSource.Factory, DrmSessionManager)} instead. */ @Deprecated @@ -219,17 +210,17 @@ public final class DownloadHelper { DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, @Nullable DrmSessionManager drmSessionManager, - DefaultTrackSelector.Parameters trackSelectorParameters) { + TrackSelectionParameters trackSelectionParameters) { return forMediaItem( new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_MPD).build(), - trackSelectorParameters, + trackSelectionParameters, renderersFactory, dataSourceFactory, drmSessionManager); } /** - * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory, + * @deprecated Use {@link #forMediaItem(MediaItem, TrackSelectionParameters, RenderersFactory, * DataSource.Factory)} instead. */ @SuppressWarnings("deprecation") @@ -248,7 +239,7 @@ public final class DownloadHelper { } /** - * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory, + * @deprecated Use {@link #forMediaItem(MediaItem, TrackSelectionParameters, RenderersFactory, * DataSource.Factory, DrmSessionManager)} instead. */ @Deprecated @@ -257,17 +248,17 @@ public final class DownloadHelper { DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, @Nullable DrmSessionManager drmSessionManager, - DefaultTrackSelector.Parameters trackSelectorParameters) { + TrackSelectionParameters trackSelectionParameters) { return forMediaItem( new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_M3U8).build(), - trackSelectorParameters, + trackSelectionParameters, renderersFactory, dataSourceFactory, drmSessionManager); } /** - * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory, + * @deprecated Use {@link #forMediaItem(MediaItem, TrackSelectionParameters, RenderersFactory, * DataSource.Factory)} instead. */ @SuppressWarnings("deprecation") @@ -283,7 +274,7 @@ public final class DownloadHelper { } /** - * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory, + * @deprecated Use {@link #forMediaItem(MediaItem, TrackSelectionParameters, RenderersFactory, * DataSource.Factory)} instead. */ @SuppressWarnings("deprecation") @@ -302,7 +293,7 @@ public final class DownloadHelper { } /** - * @deprecated Use {@link #forMediaItem(MediaItem, Parameters, RenderersFactory, + * @deprecated Use {@link #forMediaItem(MediaItem, TrackSelectionParameters, RenderersFactory, * DataSource.Factory, DrmSessionManager)} instead. */ @Deprecated @@ -311,10 +302,10 @@ public final class DownloadHelper { DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory, @Nullable DrmSessionManager drmSessionManager, - DefaultTrackSelector.Parameters trackSelectorParameters) { + TrackSelectionParameters trackSelectionParameters) { return forMediaItem( new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_SS).build(), - trackSelectorParameters, + trackSelectionParameters, renderersFactory, dataSourceFactory, drmSessionManager); @@ -372,7 +363,7 @@ public final class DownloadHelper { * @param mediaItem A {@link MediaItem}. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for + * @param trackSelectionParameters {@link TrackSelectionParameters} for selecting tracks for * downloading. * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive * streams. This argument is required for adaptive streams and ignored for progressive @@ -384,12 +375,12 @@ public final class DownloadHelper { */ public static DownloadHelper forMediaItem( MediaItem mediaItem, - DefaultTrackSelector.Parameters trackSelectorParameters, + TrackSelectionParameters trackSelectionParameters, @Nullable RenderersFactory renderersFactory, @Nullable DataSource.Factory dataSourceFactory) { return forMediaItem( mediaItem, - trackSelectorParameters, + trackSelectionParameters, renderersFactory, dataSourceFactory, /* drmSessionManager= */ null); @@ -401,7 +392,7 @@ public final class DownloadHelper { * @param mediaItem A {@link MediaItem}. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for + * @param trackSelectionParameters {@link TrackSelectionParameters} for selecting tracks for * downloading. * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest for adaptive * streams. This argument is required for adaptive streams and ignored for progressive @@ -415,7 +406,7 @@ public final class DownloadHelper { */ public static DownloadHelper forMediaItem( MediaItem mediaItem, - DefaultTrackSelector.Parameters trackSelectorParameters, + TrackSelectionParameters trackSelectionParameters, @Nullable RenderersFactory renderersFactory, @Nullable DataSource.Factory dataSourceFactory, @Nullable DrmSessionManager drmSessionManager) { @@ -427,7 +418,7 @@ public final class DownloadHelper { ? null : createMediaSourceInternal( mediaItem, castNonNull(dataSourceFactory), drmSessionManager), - trackSelectorParameters, + trackSelectionParameters, renderersFactory != null ? getRendererCapabilities(renderersFactory) : new RendererCapabilities[0]); @@ -483,7 +474,7 @@ public final class DownloadHelper { * @param mediaItem The media item. * @param mediaSource A {@link MediaSource} for which tracks are selected, or null if no track * selection needs to be made. - * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for + * @param trackSelectionParameters {@link TrackSelectionParameters} for selecting tracks for * downloading. * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks * are selected. @@ -491,12 +482,12 @@ public final class DownloadHelper { public DownloadHelper( MediaItem mediaItem, @Nullable MediaSource mediaSource, - DefaultTrackSelector.Parameters trackSelectorParameters, + TrackSelectionParameters trackSelectionParameters, RendererCapabilities[] rendererCapabilities) { this.localConfiguration = checkNotNull(mediaItem.localConfiguration); this.mediaSource = mediaSource; this.trackSelector = - new DefaultTrackSelector(trackSelectorParameters, new DownloadTrackSelection.Factory()); + new DefaultTrackSelector(trackSelectionParameters, new DownloadTrackSelection.Factory()); this.rendererCapabilities = rendererCapabilities; this.scratchSet = new SparseIntArray(); trackSelector.init(/* listener= */ () -> {}, new FakeBandwidthMeter()); @@ -554,6 +545,20 @@ public final class DownloadHelper { return trackGroupArrays.length; } + /** + * Returns {@link TracksInfo} for the given period. Must not be called until after preparation + * completes. + * + * @param periodIndex The period index. + * @return The {@link TracksInfo} for the period. May be {@link TracksInfo#EMPTY} for single + * stream content. + */ + public TracksInfo getTracksInfo(int periodIndex) { + assertPreparedWithMedia(); + return TrackSelectionUtil.buildTracksInfo( + mappedTrackInfos[periodIndex], immutableTrackSelectionsByPeriodAndRenderer[periodIndex]); + } + /** * Returns the track groups for the given period. Must not be called until after preparation * completes. @@ -612,13 +617,14 @@ public final class DownloadHelper { * completes. * * @param periodIndex The period index for which the track selection is replaced. - * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new + * @param trackSelectionParameters The {@link TrackSelectionParameters} to obtain the new * selection of tracks. */ public void replaceTrackSelections( - int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { + int periodIndex, TrackSelectionParameters trackSelectionParameters) { + assertPreparedWithMedia(); clearTrackSelections(periodIndex); - addTrackSelection(periodIndex, trackSelectorParameters); + addTrackSelectionInternal(periodIndex, trackSelectionParameters); } /** @@ -626,14 +632,13 @@ public final class DownloadHelper { * completes. * * @param periodIndex The period index this track selection is added for. - * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new + * @param trackSelectionParameters The {@link TrackSelectionParameters} to obtain the new * selection of tracks. */ public void addTrackSelection( - int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { + int periodIndex, TrackSelectionParameters trackSelectionParameters) { assertPreparedWithMedia(); - trackSelector.setParameters(trackSelectorParameters); - runTrackSelection(periodIndex); + addTrackSelectionInternal(periodIndex, trackSelectionParameters); } /** @@ -646,19 +651,25 @@ public final class DownloadHelper { */ public void addAudioLanguagesToSelection(String... languages) { assertPreparedWithMedia(); - for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) { - DefaultTrackSelector.ParametersBuilder parametersBuilder = - DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon(); - MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex]; - int rendererCount = mappedTrackInfo.getRendererCount(); - for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { - if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_AUDIO) { - parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true); - } - } - for (String language : languages) { - parametersBuilder.setPreferredAudioLanguage(language); - addTrackSelection(periodIndex, parametersBuilder.build()); + + TrackSelectionParameters.Builder parametersBuilder = + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon(); + // Prefer highest supported bitrate for downloads. + parametersBuilder.setForceHighestSupportedBitrate(true); + // Disable all non-audio track types supported by the renderers. + for (RendererCapabilities capabilities : rendererCapabilities) { + @C.TrackType int trackType = capabilities.getTrackType(); + parametersBuilder.setTrackTypeDisabled( + trackType, /* disabled= */ trackType != C.TRACK_TYPE_AUDIO); + } + + // Add a track selection to each period for each of the languages. + int periodCount = getPeriodCount(); + for (String language : languages) { + TrackSelectionParameters parameters = + parametersBuilder.setPreferredAudioLanguage(language).build(); + for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { + addTrackSelectionInternal(periodIndex, parameters); } } } @@ -676,20 +687,26 @@ public final class DownloadHelper { public void addTextLanguagesToSelection( boolean selectUndeterminedTextLanguage, String... languages) { assertPreparedWithMedia(); - for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) { - DefaultTrackSelector.ParametersBuilder parametersBuilder = - DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon(); - MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex]; - int rendererCount = mappedTrackInfo.getRendererCount(); - for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { - if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_TEXT) { - parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true); - } - } - parametersBuilder.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage); - for (String language : languages) { - parametersBuilder.setPreferredTextLanguage(language); - addTrackSelection(periodIndex, parametersBuilder.build()); + + TrackSelectionParameters.Builder parametersBuilder = + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon(); + parametersBuilder.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage); + // Prefer highest supported bitrate for downloads. + parametersBuilder.setForceHighestSupportedBitrate(true); + // Disable all non-text track types supported by the renderers. + for (RendererCapabilities capabilities : rendererCapabilities) { + @C.TrackType int trackType = capabilities.getTrackType(); + parametersBuilder.setTrackTypeDisabled( + trackType, /* disabled= */ trackType != C.TRACK_TYPE_TEXT); + } + + // Add a track selection to each period for each of the languages. + int periodCount = getPeriodCount(); + for (String language : languages) { + TrackSelectionParameters parameters = + parametersBuilder.setPreferredTextLanguage(language).build(); + for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { + addTrackSelectionInternal(periodIndex, parameters); } } } @@ -716,12 +733,12 @@ public final class DownloadHelper { builder.setRendererDisabled(/* rendererIndex= */ i, /* disabled= */ i != rendererIndex); } if (overrides.isEmpty()) { - addTrackSelection(periodIndex, builder.build()); + addTrackSelectionInternal(periodIndex, builder.build()); } else { TrackGroupArray trackGroupArray = mappedTrackInfos[periodIndex].getTrackGroups(rendererIndex); for (int i = 0; i < overrides.size(); i++) { builder.setSelectionOverride(rendererIndex, trackGroupArray, overrides.get(i)); - addTrackSelection(periodIndex, builder.build()); + addTrackSelectionInternal(periodIndex, builder.build()); } } } @@ -773,8 +790,28 @@ public final class DownloadHelper { return requestBuilder.setStreamKeys(streamKeys).build(); } - // Initialization of array of Lists. - @SuppressWarnings("unchecked") + @RequiresNonNull({ + "trackGroupArrays", + "trackSelectionsByPeriodAndRenderer", + "mediaPreparer", + "mediaPreparer.timeline" + }) + private void addTrackSelectionInternal( + int periodIndex, TrackSelectionParameters trackSelectionParameters) { + trackSelector.setParameters(trackSelectionParameters); + runTrackSelection(periodIndex); + // TrackSelectionParameters can contain multiple overrides for each track type. The track + // selector will only use one of them (because it's designed for playback), but for downloads we + // want to use all of them. Run selection again with each override being the only one of its + // type, to ensure that all of the desired tracks are included. + for (TrackSelectionOverride override : trackSelectionParameters.overrides.values()) { + trackSelector.setParameters( + trackSelectionParameters.buildUpon().setOverrideForType(override).build()); + runTrackSelection(periodIndex); + } + } + + @SuppressWarnings("unchecked") // Initialization of array of Lists. private void onMediaPrepared() { checkNotNull(mediaPreparer); checkNotNull(mediaPreparer.mediaPeriods); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaPeriod.java index 1744873ab4..4d9e0a1274 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ClippingMediaPeriod.java @@ -19,7 +19,6 @@ import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java index c64201c8b8..c667c4e7a9 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java @@ -147,7 +147,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { * @param dataSourceFactory A {@link DataSource.Factory} to create {@link DataSource} instances * for requesting media data. */ - @UnstableApi public DefaultMediaSourceFactory(DataSource.Factory dataSourceFactory) { this(dataSourceFactory, new DefaultExtractorsFactory()); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaPeriod.java index 2553a6ab03..0050985cbb 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaPeriod.java @@ -21,7 +21,6 @@ import static androidx.media3.common.util.Util.castNonNull; import androidx.annotation.Nullable; import androidx.media3.common.C; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.SeekParameters; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaPeriod.java index d625c0033b..c176ebd752 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaPeriod.java @@ -19,7 +19,6 @@ import androidx.media3.common.C; import androidx.media3.common.StreamKey; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.SeekParameters; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java index cc6b34ebb4..925998ea1c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MergingMediaPeriod.java @@ -23,7 +23,6 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.StreamKey; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.exoplayer.FormatHolder; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java index f20a57caeb..026917b9a0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java @@ -28,7 +28,6 @@ import androidx.media3.common.Metadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.ParserException; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.ParsableByteArray; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SilenceMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SilenceMediaSource.java index d2857100fc..18d52dfa09 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SilenceMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SilenceMediaSource.java @@ -26,7 +26,6 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaPeriod.java index 55c2a4c614..16e36635f8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaPeriod.java @@ -20,7 +20,6 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaSource.java index ace3d6971c..5c0cb861c4 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SingleSampleMediaSource.java @@ -74,11 +74,12 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } /** - * Sets an optional track id to be used. - * - * @param trackId An optional track id. - * @return This factory, for convenience. + * @deprecated Use {@link MediaItem.SubtitleConfiguration.Builder#setId(String)} instead (on the + * {@link MediaItem.SubtitleConfiguration} passed to {@link + * #createMediaSource(MediaItem.SubtitleConfiguration, long)}). {@code trackId} will only be + * used if {@link MediaItem.SubtitleConfiguration#id} is {@code null}. */ + @Deprecated public Factory setTrackId(@Nullable String trackId) { this.trackId = trackId; return this; @@ -157,29 +158,28 @@ public final class SingleSampleMediaSource extends BaseMediaSource { this.durationUs = durationUs; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; - mediaItem = + this.mediaItem = new MediaItem.Builder() .setUri(Uri.EMPTY) .setMediaId(subtitleConfiguration.uri.toString()) .setSubtitleConfigurations(ImmutableList.of(subtitleConfiguration)) .setTag(tag) .build(); - format = + this.format = new Format.Builder() - .setId(trackId) .setSampleMimeType(firstNonNull(subtitleConfiguration.mimeType, MimeTypes.TEXT_UNKNOWN)) .setLanguage(subtitleConfiguration.language) .setSelectionFlags(subtitleConfiguration.selectionFlags) .setRoleFlags(subtitleConfiguration.roleFlags) .setLabel(subtitleConfiguration.label) - .setId(subtitleConfiguration.id) + .setId(subtitleConfiguration.id != null ? subtitleConfiguration.id : trackId) .build(); - dataSpec = + this.dataSpec = new DataSpec.Builder() .setUri(subtitleConfiguration.uri) .setFlags(DataSpec.FLAG_ALLOW_GZIP) .build(); - timeline = + this.timeline = new SinglePeriodTimeline( durationUs, /* isSeekable= */ true, diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackGroupArray.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/TrackGroupArray.java similarity index 85% rename from libraries/common/src/main/java/androidx/media3/common/TrackGroupArray.java rename to libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/TrackGroupArray.java index 671d9848fa..d37b8802d3 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackGroupArray.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/TrackGroupArray.java @@ -13,13 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.media3.common; +package androidx.media3.exoplayer.source; import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.media3.common.Bundleable; +import androidx.media3.common.C; +import androidx.media3.common.TrackGroup; import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; @@ -30,7 +33,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.List; -/** An immutable array of {@link TrackGroup}s. */ +/** + * An immutable array of {@link TrackGroup}s. + * + *

    This class is typically used to represent all of the tracks available in a piece of media. + * Tracks that are known to present the same content are grouped together (e.g., the same video feed + * provided at different resolutions in an adaptive stream). Tracks that are known to present + * different content are in separate track groups (e.g., an audio track will not be in the same + * group as a video track, and an audio track in one language will be in a different group to an + * audio track in another language). + */ @UnstableApi public final class TrackGroupArray implements Bundleable { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java index e5e29d4c97..c0a1828be6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ads/ServerSideAdInsertionMediaSource.java @@ -36,7 +36,6 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.StreamKey; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.TransferListener; @@ -54,6 +53,7 @@ import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.source.SampleStream; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.Allocator; import com.google.common.collect.ArrayListMultimap; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelection.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelection.java index 0d1c02e80f..6c056a328c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelection.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelection.java @@ -24,7 +24,6 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackSelection; import androidx.media3.common.util.Clock; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; @@ -781,7 +780,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } double[] logBitrates = new double[trackBitrates[i].length]; for (int j = 0; j < trackBitrates[i].length; j++) { - logBitrates[j] = trackBitrates[i][j] == Format.NO_VALUE ? 0 : Math.log(trackBitrates[i][j]); + logBitrates[j] = + trackBitrates[i][j] == Format.NO_VALUE ? 0 : Math.log((double) trackBitrates[i][j]); } double totalBitrateDiff = logBitrates[logBitrates.length - 1] - logBitrates[0]; for (int j = 0; j < logBitrates.length - 1; j++) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/BaseTrackSelection.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/BaseTrackSelection.java index 2e24629e18..0f5f53dde6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/BaseTrackSelection.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/BaseTrackSelection.java @@ -22,7 +22,6 @@ import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackSelection; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; 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 6361c53e45..77dd197b3c 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 @@ -36,8 +36,6 @@ import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelection; import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.util.Assertions; @@ -51,6 +49,7 @@ import androidx.media3.exoplayer.RendererCapabilities.AdaptiveSupport; import androidx.media3.exoplayer.RendererCapabilities.Capabilities; import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; +import androidx.media3.exoplayer.source.TrackGroupArray; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; import com.google.common.collect.Ordering; @@ -86,11 +85,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * .setMaxVideoSizeSd() * .setPreferredAudioLanguage("de") * .build()); - * * }

  • * * Some specialized parameters are only available in the extended {@link Parameters} class, which - * can be retrieved and modified in a similar way in this track selector: + * can be retrieved and modified in a similar way by calling methods directly on this class: * *
    {@code
      * defaultTrackSelector.setParameters(
    @@ -98,7 +96,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
      *         .buildUpon()
      *         .setTunnelingEnabled(true)
      *         .build());
    - *
      * }
    */ @UnstableApi @@ -121,13 +118,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean allowAudioMixedSampleRateAdaptiveness; private boolean allowAudioMixedChannelCountAdaptiveness; private boolean allowAudioMixedDecoderSupportAdaptiveness; - // Text - private @C.SelectionFlags int disabledTextTrackSelectionFlags; // General private boolean exceedRendererCapabilitiesIfNecessary; private boolean tunnelingEnabled; private boolean allowMultipleAdaptiveSelections; - + // Overrides private final SparseArray> selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; @@ -163,8 +158,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ private ParametersBuilder(Parameters initialValues) { super(initialValues); - // Text - disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags; // Video exceedVideoConstraintsIfNecessary = initialValues.exceedVideoConstraintsIfNecessary; allowVideoMixedMimeTypeAdaptiveness = initialValues.allowVideoMixedMimeTypeAdaptiveness; @@ -232,11 +225,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { Parameters.keyForField( Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), defaultValue.allowAudioMixedDecoderSupportAdaptiveness)); - // Text - setDisabledTextTrackSelectionFlags( - bundle.getInt( - Parameters.keyForField(Parameters.FIELD_DISABLED_TEXT_TRACK_SELECTION_FLAGS), - defaultValue.disabledTextTrackSelectionFlags)); // General setExceedRendererCapabilitiesIfNecessary( bundle.getBoolean( @@ -250,10 +238,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { bundle.getBoolean( Parameters.keyForField(Parameters.FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS), defaultValue.allowMultipleAdaptiveSelections)); - + // Overrides selectionOverrides = new SparseArray<>(); setSelectionOverridesFromBundle(bundle); - rendererDisabledFlags = makeSparseBooleanArrayFromTrueKeys( bundle.getIntArray( @@ -562,6 +549,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + @Override + public ParametersBuilder setIgnoredTextSelectionFlags( + @C.SelectionFlags int ignoredTextSelectionFlags) { + super.setIgnoredTextSelectionFlags(ignoredTextSelectionFlags); + return this; + } + @Override public ParametersBuilder setSelectUndeterminedTextLanguage( boolean selectUndeterminedTextLanguage) { @@ -570,16 +564,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Sets a bitmask of selection flags that are disabled for text track selections. - * - * @param disabledTextTrackSelectionFlags A bitmask of {@link C.SelectionFlags} that are - * disabled for text track selections. - * @return This builder. + * @deprecated Use {@link #setIgnoredTextSelectionFlags}. */ + @Deprecated public ParametersBuilder setDisabledTextTrackSelectionFlags( @C.SelectionFlags int disabledTextTrackSelectionFlags) { - this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; - return this; + return setIgnoredTextSelectionFlags(disabledTextTrackSelectionFlags); } // General @@ -627,11 +617,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { } @Override + @Deprecated + @SuppressWarnings("deprecation") public ParametersBuilder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) { super.setDisabledTrackTypes(disabledTrackTypes); return this; } + @Override + public ParametersBuilder setTrackTypeDisabled(@C.TrackType int trackType, boolean disabled) { + super.setTrackTypeDisabled(trackType, disabled); + return this; + } + /** * Sets whether to exceed renderer capabilities when no selection can be made otherwise. * @@ -823,8 +821,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { allowAudioMixedSampleRateAdaptiveness = false; allowAudioMixedChannelCountAdaptiveness = false; allowAudioMixedDecoderSupportAdaptiveness = false; - // Text - disabledTextTrackSelectionFlags = 0; // General exceedRendererCapabilitiesIfNecessary = true; tunnelingEnabled = false; @@ -912,12 +908,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Deprecated public static final Parameters DEFAULT = DEFAULT_WITHOUT_CONTEXT; - /** - * Bitmask of selection flags that are disabled for text track selections. See {@link - * C.SelectionFlags}. The default value is {@code 0} (i.e. no flags). - */ - public final @C.SelectionFlags int disabledTextTrackSelectionFlags; - /** Returns an instance configured with default values. */ public static Parameters getDefaults(Context context) { return new ParametersBuilder(context).build(); @@ -1015,8 +1005,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { allowAudioMixedSampleRateAdaptiveness = builder.allowAudioMixedSampleRateAdaptiveness; allowAudioMixedChannelCountAdaptiveness = builder.allowAudioMixedChannelCountAdaptiveness; allowAudioMixedDecoderSupportAdaptiveness = builder.allowAudioMixedDecoderSupportAdaptiveness; - // Text - disabledTextTrackSelectionFlags = builder.disabledTextTrackSelectionFlags; // General exceedRendererCapabilitiesIfNecessary = builder.exceedRendererCapabilitiesIfNecessary; tunnelingEnabled = builder.tunnelingEnabled; @@ -1048,6 +1036,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Deprecated public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { + @Nullable Map overrides = selectionOverrides.get(rendererIndex); return overrides != null && overrides.containsKey(groups); @@ -1066,6 +1055,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Deprecated @Nullable public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { + @Nullable Map overrides = selectionOverrides.get(rendererIndex); return overrides != null ? overrides.get(groups) : null; @@ -1102,8 +1092,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { == other.allowAudioMixedChannelCountAdaptiveness && allowAudioMixedDecoderSupportAdaptiveness == other.allowAudioMixedDecoderSupportAdaptiveness - // Text - && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags // General && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary && tunnelingEnabled == other.tunnelingEnabled @@ -1128,8 +1116,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0); result = 31 * result + (allowAudioMixedChannelCountAdaptiveness ? 1 : 0); result = 31 * result + (allowAudioMixedDecoderSupportAdaptiveness ? 1 : 0); - // Text - result = 31 * result + disabledTextTrackSelectionFlags; // General result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); result = 31 * result + (tunnelingEnabled ? 1 : 0); @@ -1144,23 +1130,26 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Retention(RetentionPolicy.SOURCE) @Target(TYPE_USE) @IntDef({ + // Video FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY, FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS, FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS, + FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS, + // Audio FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY, FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS, FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS, FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS, - FIELD_DISABLED_TEXT_TRACK_SELECTION_FLAGS, + FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS, + // General FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY, FIELD_TUNNELING_ENABLED, FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS, + // Overrides FIELD_SELECTION_OVERRIDES_RENDERER_INDICES, FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS, FIELD_SELECTION_OVERRIDES, FIELD_RENDERER_DISABLED_INDICES, - FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS, - FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS }) private @interface FieldNumber {} @@ -1172,16 +1161,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static final int FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS = 1004; private static final int FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS = 1005; private static final int FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS = 1006; - private static final int FIELD_DISABLED_TEXT_TRACK_SELECTION_FLAGS = 1007; - private static final int FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY = 1008; - private static final int FIELD_TUNNELING_ENABLED = 1009; - private static final int FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = 1010; - private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = 1011; - private static final int FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS = 1012; - private static final int FIELD_SELECTION_OVERRIDES = 1013; - private static final int FIELD_RENDERER_DISABLED_INDICES = 1014; - private static final int FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = 1015; - private static final int FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = 1016; + private static final int FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY = 1007; + private static final int FIELD_TUNNELING_ENABLED = 1008; + private static final int FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = 1009; + private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = 1010; + private static final int FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS = 1011; + private static final int FIELD_SELECTION_OVERRIDES = 1012; + private static final int FIELD_RENDERER_DISABLED_INDICES = 1013; + private static final int FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = 1014; + private static final int FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = 1015; @Override public Bundle toBundle() { @@ -1216,9 +1204,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { bundle.putBoolean( keyForField(FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), allowAudioMixedDecoderSupportAdaptiveness); - // Text - bundle.putInt( - keyForField(FIELD_DISABLED_TEXT_TRACK_SELECTION_FLAGS), disabledTextTrackSelectionFlags); // General bundle.putBoolean( keyForField(FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY), @@ -1516,13 +1501,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * @param parameters Initial {@link Parameters}. + * @param parameters Initial {@link TrackSelectionParameters}. * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s. */ public DefaultTrackSelector( - Parameters parameters, ExoTrackSelection.Factory trackSelectionFactory) { + TrackSelectionParameters parameters, ExoTrackSelection.Factory trackSelectionFactory) { this.trackSelectionFactory = trackSelectionFactory; - parametersReference = new AtomicReference<>(parameters); + parametersReference = + new AtomicReference<>( + parameters instanceof Parameters + ? (Parameters) parameters + : Parameters.DEFAULT_WITHOUT_CONTEXT.buildUpon().set(parameters).build()); } @Override @@ -1592,30 +1581,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererMixedMimeTypeAdaptationSupports, params); - // Apply per track type overrides. - SparseArray> applicableOverridesByTrackType = - getApplicableOverrides(mappedTrackInfo, params); - for (int i = 0; i < applicableOverridesByTrackType.size(); i++) { - Pair overrideAndRendererIndex = - applicableOverridesByTrackType.valueAt(i); - applyTrackTypeOverride( - mappedTrackInfo, - definitions, - /* trackType= */ applicableOverridesByTrackType.keyAt(i), - /* override= */ overrideAndRendererIndex.first, - /* overrideRendererIndex= */ overrideAndRendererIndex.second); - } - - // Apply legacy per renderer overrides. - for (int i = 0; i < rendererCount; i++) { - if (hasLegacyRendererOverride(mappedTrackInfo, params, /* rendererIndex= */ i)) { - definitions[i] = getLegacyRendererOverride(mappedTrackInfo, params, /* rendererIndex= */ i); - } - } + applyTrackSelectionOverrides(mappedTrackInfo, params, definitions); + applyLegacyRendererOverrides(mappedTrackInfo, params, definitions); // Disable renderers if needed. for (int i = 0; i < rendererCount; i++) { - if (isRendererDisabled(mappedTrackInfo, params, /* rendererIndex= */ i)) { + @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i); + if (params.getRendererDisabled(i) || params.disabledTrackTypes.contains(rendererType)) { definitions[i] = null; } } @@ -1649,94 +1621,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { return Pair.create(rendererConfigurations, rendererTrackSelections); } - private boolean isRendererDisabled( - MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { - @C.TrackType int rendererType = mappedTrackInfo.getRendererType(rendererIndex); - return params.getRendererDisabled(rendererIndex) - || params.disabledTrackTypes.contains(rendererType); - } - - @SuppressWarnings("deprecation") // Calling deprecated hasSelectionOverride. - private boolean hasLegacyRendererOverride( - MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { - TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - return params.hasSelectionOverride(rendererIndex, rendererTrackGroups); - } - - @SuppressWarnings("deprecation") // Calling deprecated getSelectionOverride. - private ExoTrackSelection.@NullableType Definition getLegacyRendererOverride( - MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { - TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - @Nullable - SelectionOverride override = params.getSelectionOverride(rendererIndex, rendererTrackGroups); - if (override == null) { - return null; - } - return new ExoTrackSelection.Definition( - rendererTrackGroups.get(override.groupIndex), override.tracks, override.type); - } - - /** - * Returns applicable overrides. Mapping from track type to a pair of override and renderer index - * for this override. - */ - private SparseArray> getApplicableOverrides( - MappedTrackInfo mappedTrackInfo, Parameters params) { - SparseArray> applicableOverrides = new SparseArray<>(); - // Iterate through all existing track groups to ensure only overrides for those groups are used. - int rendererCount = mappedTrackInfo.getRendererCount(); - for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { - TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - for (int j = 0; j < rendererTrackGroups.length; j++) { - maybeUpdateApplicableOverrides( - applicableOverrides, params.overrides.get(rendererTrackGroups.get(j)), rendererIndex); - } - } - // Also iterate unmapped groups to see if they have overrides. - TrackGroupArray unmappedGroups = mappedTrackInfo.getUnmappedTrackGroups(); - for (int i = 0; i < unmappedGroups.length; i++) { - maybeUpdateApplicableOverrides( - applicableOverrides, - params.overrides.get(unmappedGroups.get(i)), - /* rendererIndex= */ C.INDEX_UNSET); - } - return applicableOverrides; - } - - private void maybeUpdateApplicableOverrides( - SparseArray> applicableOverrides, - @Nullable TrackSelectionOverride override, - int rendererIndex) { - if (override == null) { - return; - } - @C.TrackType int trackType = override.getTrackType(); - @Nullable - Pair existingOverride = applicableOverrides.get(trackType); - if (existingOverride == null || existingOverride.first.trackIndices.isEmpty()) { - // We only need to choose one non-empty override per type. - applicableOverrides.put(trackType, Pair.create(override, rendererIndex)); - } - } - - private void applyTrackTypeOverride( - MappedTrackInfo mappedTrackInfo, - ExoTrackSelection.@NullableType Definition[] definitions, - @C.TrackType int trackType, - TrackSelectionOverride override, - int overrideRendererIndex) { - for (int i = 0; i < definitions.length; i++) { - if (overrideRendererIndex == i) { - definitions[i] = - new ExoTrackSelection.Definition( - override.trackGroup, Ints.toArray(override.trackIndices)); - } else if (mappedTrackInfo.getRendererType(i) == trackType) { - // Disable other renderers of the same type. - definitions[i] = null; - } - } - } - // Track selection prior to overrides and disabled flags being applied. /** @@ -1830,7 +1714,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { * renderer index, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - @SuppressLint("WrongConstant") // Lint doesn't understand arrays of IntDefs. @Nullable protected Pair selectVideoTrack( MappedTrackInfo mappedTrackInfo, @@ -1842,7 +1725,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { C.TRACK_TYPE_VIDEO, mappedTrackInfo, rendererFormatSupports, - (rendererIndex, group, support) -> + (int rendererIndex, TrackGroup group, @Capabilities int[] support) -> VideoTrackInfo.createForTrackGroup( rendererIndex, group, params, support, mixedMimeTypeSupports[rendererIndex]), VideoTrackInfo::compareSelections); @@ -1864,7 +1747,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { * renderer index, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - @SuppressLint("WrongConstant") // Lint doesn't understand arrays of IntDefs. @Nullable protected Pair selectAudioTrack( MappedTrackInfo mappedTrackInfo, @@ -1885,7 +1767,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { C.TRACK_TYPE_AUDIO, mappedTrackInfo, rendererFormatSupports, - (rendererIndex, group, support) -> + (int rendererIndex, TrackGroup group, @Capabilities int[] support) -> AudioTrackInfo.createForTrackGroup( rendererIndex, group, params, support, hasVideoRendererWithMappedTracksFinal), AudioTrackInfo::compareSelections); @@ -1907,7 +1789,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { * renderer index, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - @SuppressLint("WrongConstant") // Lint doesn't understand arrays of IntDefs. @Nullable protected Pair selectTextTrack( MappedTrackInfo mappedTrackInfo, @@ -1919,7 +1800,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { C.TRACK_TYPE_TEXT, mappedTrackInfo, rendererFormatSupports, - (rendererIndex, group, support) -> + (int rendererIndex, TrackGroup group, @Capabilities int[] support) -> TextTrackInfo.createForTrackGroup( rendererIndex, group, params, support, selectedAudioLanguage), TextTrackInfo::compareSelections); @@ -2027,6 +1908,95 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Utility methods. + private static void applyTrackSelectionOverrides( + MappedTrackInfo mappedTrackInfo, + TrackSelectionParameters params, + ExoTrackSelection.@NullableType Definition[] outDefinitions) { + int rendererCount = mappedTrackInfo.getRendererCount(); + + // Determine overrides to apply. + HashMap<@C.TrackType Integer, TrackSelectionOverride> overridesByType = new HashMap<>(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + collectTrackSelectionOverrides( + mappedTrackInfo.getTrackGroups(rendererIndex), params, overridesByType); + } + collectTrackSelectionOverrides( + mappedTrackInfo.getUnmappedTrackGroups(), params, overridesByType); + + // Apply the overrides. + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + @C.TrackType int trackType = mappedTrackInfo.getRendererType(rendererIndex); + @Nullable TrackSelectionOverride overrideForType = overridesByType.get(trackType); + if (overrideForType == null) { + continue; + } + // If the override is non-empty and applies to this renderer, then apply it. Else we don't + // want the renderer to be enabled at all, so clear any existing selection. + @Nullable ExoTrackSelection.Definition selection; + if (!overrideForType.trackIndices.isEmpty() + && mappedTrackInfo.getTrackGroups(rendererIndex).indexOf(overrideForType.trackGroup) + != -1) { + selection = + new ExoTrackSelection.Definition( + overrideForType.trackGroup, Ints.toArray(overrideForType.trackIndices)); + } else { + selection = null; + } + outDefinitions[rendererIndex] = selection; + } + } + + /** + * Adds {@link TrackSelectionOverride TrackSelectionOverrides} in {@code params} to {@code + * overridesByType} if they apply to tracks in {@code trackGroups}. If there's an existing + * override for a track type, it is replaced only if the existing override is empty and the one + * being considered is not. + */ + private static void collectTrackSelectionOverrides( + TrackGroupArray trackGroups, + TrackSelectionParameters params, + Map<@C.TrackType Integer, TrackSelectionOverride> overridesByType) { + for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.length; trackGroupIndex++) { + TrackGroup trackGroup = trackGroups.get(trackGroupIndex); + @Nullable TrackSelectionOverride override = params.overrides.get(trackGroup); + if (override == null) { + continue; + } + @Nullable + TrackSelectionOverride existingOverride = overridesByType.get(override.getTrackType()); + // Only replace an existing override if it's empty and the one being considered is not. + if (existingOverride == null + || (existingOverride.trackIndices.isEmpty() && !override.trackIndices.isEmpty())) { + overridesByType.put(override.getTrackType(), override); + } + } + } + + @SuppressWarnings("deprecation") // Calling legacy hasSelectionOverride and getSelectionOverride + private static void applyLegacyRendererOverrides( + MappedTrackInfo mappedTrackInfo, + Parameters params, + ExoTrackSelection.@NullableType Definition[] outDefinitions) { + int rendererCount = mappedTrackInfo.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + if (!params.hasSelectionOverride(rendererIndex, trackGroups)) { + continue; + } + @Nullable + SelectionOverride override = params.getSelectionOverride(rendererIndex, trackGroups); + @Nullable ExoTrackSelection.Definition selection; + if (override != null && override.tracks.length != 0) { + selection = + new ExoTrackSelection.Definition( + trackGroups.get(override.groupIndex), override.tracks, override.type); + } else { + selection = null; + } + outDefinitions[rendererIndex] = selection; + } + } + /** * Determines whether tunneling can be enabled, replacing {@link RendererConfiguration}s in {@code * rendererConfigurations} with configurations that enable tunneling on the appropriate renderers @@ -2753,8 +2723,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { super(rendererIndex, trackGroup, trackIndex); isWithinRendererCapabilities = isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false); - int maskedSelectionFlags = - format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; + int maskedSelectionFlags = format.selectionFlags & ~parameters.ignoredTextSelectionFlags; isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; int bestLanguageIndex = Integer.MAX_VALUE; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/ExoTrackSelection.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/ExoTrackSelection.java index b8df05df1a..ab7f8dc6de 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/ExoTrackSelection.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/ExoTrackSelection.java @@ -20,7 +20,7 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackSelection; +import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.chunk.Chunk; @@ -48,6 +48,8 @@ public interface ExoTrackSelection extends TrackSelection { /** The type that will be returned from {@link TrackSelection#getType()}. */ public final @Type int type; + private static final String TAG = "ETSDefinition"; + /** * @param group The {@link TrackGroup}. Must not be null. * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be @@ -64,6 +66,10 @@ public interface ExoTrackSelection extends TrackSelection { * @param type The type that will be returned from {@link TrackSelection#getType()}. */ public Definition(TrackGroup group, int[] tracks, @Type int type) { + if (tracks.length == 0) { + // TODO: Turn this into an assertion. + Log.e(TAG, "Empty tracks are not allowed", new IllegalArgumentException()); + } this.group = group; this.tracks = tracks; this.type = type; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/FixedTrackSelection.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/FixedTrackSelection.java index d6c57bb730..c41b8389f7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/FixedTrackSelection.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/FixedTrackSelection.java @@ -18,7 +18,6 @@ package androidx.media3.exoplayer.trackselection; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackSelection; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.source.chunk.MediaChunk; import androidx.media3.exoplayer.source.chunk.MediaChunkIterator; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/MappingTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/MappingTrackSelector.java index 5f031720de..66bbbb3aee 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/MappingTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/MappingTrackSelector.java @@ -31,8 +31,6 @@ import androidx.media3.common.C; import androidx.media3.common.C.FormatSupport; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelection; import androidx.media3.common.TracksInfo; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -43,7 +41,7 @@ import androidx.media3.exoplayer.RendererCapabilities.AdaptiveSupport; import androidx.media3.exoplayer.RendererCapabilities.Capabilities; import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; -import com.google.common.collect.ImmutableList; +import androidx.media3.exoplayer.source.TrackGroupArray; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -429,7 +427,7 @@ public abstract class MappingTrackSelector extends TrackSelector { periodId, timeline); - TracksInfo tracksInfo = buildTracksInfo(result.second, mappedTrackInfo); + TracksInfo tracksInfo = TrackSelectionUtil.buildTracksInfo(mappedTrackInfo, result.second); return new TrackSelectorResult(result.first, result.second, tracksInfo, mappedTrackInfo); } @@ -559,48 +557,4 @@ public abstract class MappingTrackSelector extends TrackSelector { } return mixedMimeTypeAdaptationSupport; } - - @VisibleForTesting - /* package */ static TracksInfo buildTracksInfo( - @NullableType TrackSelection[] selections, MappedTrackInfo mappedTrackInfo) { - ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for (int rendererIndex = 0; - rendererIndex < mappedTrackInfo.getRendererCount(); - rendererIndex++) { - TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); - @Nullable TrackSelection trackSelection = selections[rendererIndex]; - for (int groupIndex = 0; groupIndex < trackGroupArray.length; groupIndex++) { - TrackGroup trackGroup = trackGroupArray.get(groupIndex); - boolean adaptiveSupported = - mappedTrackInfo.getAdaptiveSupport( - rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false) - != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; - @C.FormatSupport int[] trackSupport = new int[trackGroup.length]; - boolean[] selected = new boolean[trackGroup.length]; - for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - trackSupport[trackIndex] = - mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex); - boolean isTrackSelected = - trackSelection != null - && trackSelection.getTrackGroup().equals(trackGroup) - && trackSelection.indexOf(trackIndex) != C.INDEX_UNSET; - selected[trackIndex] = isTrackSelected; - } - builder.add( - new TracksInfo.TrackGroupInfo(trackGroup, adaptiveSupported, trackSupport, selected)); - } - } - TrackGroupArray unmappedTrackGroups = mappedTrackInfo.getUnmappedTrackGroups(); - for (int groupIndex = 0; groupIndex < unmappedTrackGroups.length; groupIndex++) { - TrackGroup trackGroup = unmappedTrackGroups.get(groupIndex); - @C.FormatSupport int[] trackSupport = new int[trackGroup.length]; - Arrays.fill(trackSupport, C.FORMAT_UNSUPPORTED_TYPE); - // A track group only contains tracks of the same type, thus only consider the first track. - boolean[] selected = new boolean[trackGroup.length]; // Initialized to false. - builder.add( - new TracksInfo.TrackGroupInfo( - trackGroup, /* adaptiveSupported= */ false, trackSupport, selected)); - } - return new TracksInfo(builder.build()); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackSelection.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelection.java similarity index 95% rename from libraries/common/src/main/java/androidx/media3/common/TrackSelection.java rename to libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelection.java index a3d73a567e..ff496ca5d9 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackSelection.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelection.java @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.media3.common; +package androidx.media3.exoplayer.trackselection; import static java.lang.annotation.ElementType.TYPE_USE; import androidx.annotation.IntDef; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.TrackGroup; import androidx.media3.common.util.UnstableApi; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionArray.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectionArray.java similarity index 97% rename from libraries/common/src/main/java/androidx/media3/common/TrackSelectionArray.java rename to libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectionArray.java index 4867d8029a..5c9f18b30e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionArray.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectionArray.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.media3.common; +package androidx.media3.exoplayer.trackselection; import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectionUtil.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectionUtil.java index 9c78ae31ca..3cd0b19180 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectionUtil.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectionUtil.java @@ -17,11 +17,19 @@ package androidx.media3.exoplayer.trackselection; import android.os.SystemClock; import androidx.annotation.Nullable; -import androidx.media3.common.TrackGroupArray; +import androidx.media3.common.C; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.TracksInfo; +import androidx.media3.common.TracksInfo.TrackGroupInfo; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.RendererCapabilities; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride; import androidx.media3.exoplayer.trackselection.ExoTrackSelection.Definition; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.List; import org.checkerframework.checker.nullness.compatqual.NullableType; /** Track selection related utility methods. */ @@ -126,4 +134,81 @@ public final class TrackSelectionUtil { numberOfTracks, numberOfExcludedTracks); } + + /** + * Returns {@link TracksInfo} built from {@link MappingTrackSelector.MappedTrackInfo} and {@link + * TrackSelection TrackSelections} for each renderer. + * + * @param mappedTrackInfo The {@link MappingTrackSelector.MappedTrackInfo} + * @param selections The track selections, indexed by renderer. A null entry indicates that a + * renderer does not have any selected tracks. + * @return The corresponding {@link TracksInfo}. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) // Initialization of array of Lists. + public static TracksInfo buildTracksInfo( + MappingTrackSelector.MappedTrackInfo mappedTrackInfo, + @NullableType TrackSelection[] selections) { + List[] listSelections = new List[selections.length]; + for (int i = 0; i < selections.length; i++) { + @Nullable TrackSelection selection = selections[i]; + listSelections[i] = selection != null ? ImmutableList.of(selection) : ImmutableList.of(); + } + return buildTracksInfo(mappedTrackInfo, listSelections); + } + + /** + * Returns {@link TracksInfo} built from {@link MappingTrackSelector.MappedTrackInfo} and {@link + * TrackSelection TrackSelections} for each renderer. + * + * @param mappedTrackInfo The {@link MappingTrackSelector.MappedTrackInfo} + * @param selections The track selections, indexed by renderer. Null entries are not permitted. An + * empty list indicates that a renderer does not have any selected tracks. + * @return The corresponding {@link TracksInfo}. + */ + public static TracksInfo buildTracksInfo( + MappingTrackSelector.MappedTrackInfo mappedTrackInfo, + List[] selections) { + ImmutableList.Builder trackGroupInfos = new ImmutableList.Builder<>(); + for (int rendererIndex = 0; + rendererIndex < mappedTrackInfo.getRendererCount(); + rendererIndex++) { + TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); + List rendererTrackSelections = selections[rendererIndex]; + for (int groupIndex = 0; groupIndex < trackGroupArray.length; groupIndex++) { + TrackGroup trackGroup = trackGroupArray.get(groupIndex); + boolean adaptiveSupported = + mappedTrackInfo.getAdaptiveSupport( + rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false) + != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; + @C.FormatSupport int[] trackSupport = new int[trackGroup.length]; + boolean[] selected = new boolean[trackGroup.length]; + for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + trackSupport[trackIndex] = + mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex); + boolean isTrackSelected = false; + for (int i = 0; i < rendererTrackSelections.size(); i++) { + TrackSelection trackSelection = rendererTrackSelections.get(i); + if (trackSelection.getTrackGroup().equals(trackGroup) + && trackSelection.indexOf(trackIndex) != C.INDEX_UNSET) { + isTrackSelected = true; + break; + } + } + selected[trackIndex] = isTrackSelected; + } + trackGroupInfos.add( + new TrackGroupInfo(trackGroup, adaptiveSupported, trackSupport, selected)); + } + } + TrackGroupArray unmappedTrackGroups = mappedTrackInfo.getUnmappedTrackGroups(); + for (int groupIndex = 0; groupIndex < unmappedTrackGroups.length; groupIndex++) { + TrackGroup trackGroup = unmappedTrackGroups.get(groupIndex); + @C.FormatSupport int[] trackSupport = new int[trackGroup.length]; + Arrays.fill(trackSupport, C.FORMAT_UNSUPPORTED_TYPE); + boolean[] selected = new boolean[trackGroup.length]; + trackGroupInfos.add( + new TrackGroupInfo(trackGroup, /* adaptiveSupported= */ false, trackSupport, selected)); + } + return new TracksInfo(trackGroupInfos.build()); + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java index 07b32ea19b..2fb6ea5ae3 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java @@ -17,8 +17,6 @@ package androidx.media3.exoplayer.trackselection; import androidx.annotation.Nullable; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelection; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; @@ -28,6 +26,7 @@ import androidx.media3.exoplayer.Renderer; import androidx.media3.exoplayer.RendererCapabilities; import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.upstream.BandwidthMeter; /** diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java index 841a11ed26..f84eedabc9 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java @@ -31,7 +31,6 @@ import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Player.PlaybackSuppressionReason; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroup; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; import androidx.media3.common.util.Log; @@ -286,19 +285,18 @@ public class EventLogger implements AnalyticsListener { } // TODO: Replace this with an override of onMediaMetadataChanged. // Log metadata for at most one of the selected tracks. - for (int groupIndex = 0; groupIndex < trackGroupInfos.size(); groupIndex++) { - TracksInfo.TrackGroupInfo trackGroupInfo = trackGroupInfos.get(groupIndex); - TrackGroup trackGroup = trackGroupInfo.getTrackGroup(); - for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (!trackGroupInfo.isTrackSelected(trackIndex)) { - continue; - } - @Nullable Metadata metadata = trackGroup.getFormat(trackIndex).metadata; - if (metadata != null) { - logd(" Metadata ["); - printMetadata(metadata, " "); - logd(" ]"); - break; + boolean loggedMetadata = false; + for (int groupIndex = 0; !loggedMetadata && groupIndex < trackGroupInfos.size(); groupIndex++) { + TracksInfo.TrackGroupInfo trackGroup = trackGroupInfos.get(groupIndex); + for (int trackIndex = 0; !loggedMetadata && trackIndex < trackGroup.length; trackIndex++) { + if (trackGroup.isTrackSelected(trackIndex)) { + @Nullable Metadata metadata = trackGroup.getTrackFormat(trackIndex).metadata; + if (metadata != null && metadata.length() > 0) { + logd(" Metadata ["); + printMetadata(metadata, " "); + logd(" ]"); + loggedMetadata = true; + } } } } 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 d1114f4ca3..cf38dc6a13 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 @@ -129,7 +129,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private boolean codecHandlesHdr10PlusOutOfBandMetadata; @Nullable private Surface surface; - @Nullable private DummySurface dummySurface; + @Nullable private PlaceholderSurface placeholderSurface; private boolean haveReportedFirstFrameRenderedForCurrentSurface; private @C.VideoScalingMode int scalingMode; private boolean renderedFirstFrameAfterReset; @@ -515,7 +515,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { public boolean isReady() { if (super.isReady() && (renderedFirstFrameAfterReset - || (dummySurface != null && surface == dummySurface) + || (placeholderSurface != null && surface == placeholderSurface) || getCodec() == null || tunneling)) { // Ready. If we were joining then we've now joined, so clear the joining deadline. @@ -567,14 +567,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } - @TargetApi(17) // Needed for dummySurface usage. dummySurface is always null on API level 16. + @TargetApi(17) // Needed for placeholderSurface usage, as it is always null on API level 16. @Override protected void onReset() { try { super.onReset(); } finally { - if (dummySurface != null) { - releaseDummySurface(); + if (placeholderSurface != null) { + releasePlaceholderSurface(); } } } @@ -624,14 +624,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Nullable Surface surface = output instanceof Surface ? (Surface) output : null; if (surface == null) { - // Use a dummy surface if possible. - if (dummySurface != null) { - surface = dummySurface; + // Use a placeholder surface if possible. + if (placeholderSurface != null) { + surface = placeholderSurface; } else { MediaCodecInfo codecInfo = getCodecInfo(); if (codecInfo != null && shouldUseDummySurface(codecInfo)) { - dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); - surface = dummySurface; + placeholderSurface = PlaceholderSurface.newInstanceV17(context, codecInfo.secure); + surface = placeholderSurface; } } } @@ -652,7 +652,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maybeInitCodecOrBypass(); } } - if (surface != null && surface != dummySurface) { + if (surface != null && surface != placeholderSurface) { // If we know the video size, report it again immediately. maybeRenotifyVideoSizeChanged(); // We haven't rendered to the new surface yet. @@ -665,7 +665,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { clearReportedVideoSize(); clearRenderedFirstFrame(); } - } else if (surface != null && surface != dummySurface) { + } else if (surface != null && surface != placeholderSurface) { // The surface is set and unchanged. If we know the video size and/or have already rendered to // the surface, report these again immediately. maybeRenotifyVideoSizeChanged(); @@ -684,16 +684,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return tunneling && Util.SDK_INT < 23; } - @TargetApi(17) // Needed for dummySurface usage. dummySurface is always null on API level 16. + @TargetApi(17) // Needed for placeHolderSurface usage, as it is always null on API level 16. @Override protected MediaCodecAdapter.Configuration getMediaCodecConfiguration( MediaCodecInfo codecInfo, Format format, @Nullable MediaCrypto crypto, float codecOperatingRate) { - if (dummySurface != null && dummySurface.secure != codecInfo.secure) { + if (placeholderSurface != null && placeholderSurface.secure != codecInfo.secure) { // We can't re-use the current DummySurface instance with the new decoder. - releaseDummySurface(); + releasePlaceholderSurface(); } String codecMimeType = codecInfo.codecMimeType; codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); @@ -709,10 +709,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (!shouldUseDummySurface(codecInfo)) { throw new IllegalStateException(); } - if (dummySurface == null) { - dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); + if (placeholderSurface == null) { + placeholderSurface = PlaceholderSurface.newInstanceV17(context, codecInfo.secure); } - surface = dummySurface; + surface = placeholderSurface; } return MediaCodecAdapter.Configuration.createForVideoDecoding( codecInfo, mediaFormat, format, surface, crypto); @@ -949,7 +949,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { earlyUs -= elapsedRealtimeNowUs - elapsedRealtimeUs; } - if (surface == dummySurface) { + if (surface == placeholderSurface) { // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. if (isBufferLate(earlyUs)) { skipOutputBuffer(codec, bufferIndex, presentationTimeUs); @@ -1259,16 +1259,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Util.SDK_INT >= 23 && !tunneling && !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name) - && (!codecInfo.secure || DummySurface.isSecureSupported(context)); + && (!codecInfo.secure || PlaceholderSurface.isSecureSupported(context)); } @RequiresApi(17) - private void releaseDummySurface() { - if (surface == dummySurface) { + private void releasePlaceholderSurface() { + if (surface == placeholderSurface) { surface = null; } - dummySurface.release(); - dummySurface = null; + placeholderSurface.release(); + placeholderSurface = null; } private void setJoiningDeadlineMs() { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DummySurface.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaceholderSurface.java similarity index 82% rename from libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DummySurface.java rename to libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaceholderSurface.java index a7819ddd25..e9d1955435 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DummySurface.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaceholderSurface.java @@ -36,12 +36,12 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** A dummy {@link Surface}. */ +/** A placeholder {@link Surface}. */ @RequiresApi(17) @UnstableApi -public final class DummySurface extends Surface { +public final class PlaceholderSurface extends Surface { - private static final String TAG = "DummySurface"; + private static final String TAG = "PlaceholderSurface"; /** Whether the surface is secure. */ public final boolean secure; @@ -49,14 +49,14 @@ public final class DummySurface extends Surface { private static @SecureMode int secureMode; private static boolean secureModeInitialized; - private final DummySurfaceThread thread; + private final PlaceholderSurfaceThread thread; private boolean threadReleased; /** - * Returns whether the device supports secure dummy surfaces. + * Returns whether the device supports secure placeholder surfaces. * * @param context Any {@link Context}. - * @return Whether the device supports secure dummy surfaces. + * @return Whether the device supports secure placeholder surfaces. */ public static synchronized boolean isSecureSupported(Context context) { if (!secureModeInitialized) { @@ -67,8 +67,8 @@ public final class DummySurface extends Surface { } /** - * Returns a newly created dummy surface. The surface must be released by calling {@link #release} - * when it's no longer required. + * Returns a newly created placeholder surface. The surface must be released by calling {@link + * #release} when it's no longer required. * *

    Must only be called if {@link Util#SDK_INT} is 17 or higher. * @@ -78,13 +78,14 @@ public final class DummySurface extends Surface { * @throws IllegalStateException If a secure surface is requested on a device for which {@link * #isSecureSupported(Context)} returns {@code false}. */ - public static DummySurface newInstanceV17(Context context, boolean secure) { + public static PlaceholderSurface newInstanceV17(Context context, boolean secure) { Assertions.checkState(!secure || isSecureSupported(context)); - DummySurfaceThread thread = new DummySurfaceThread(); + PlaceholderSurfaceThread thread = new PlaceholderSurfaceThread(); return thread.init(secure ? secureMode : SECURE_MODE_NONE); } - private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) { + private PlaceholderSurface( + PlaceholderSurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) { super(surfaceTexture); this.thread = thread; this.secure = secure; @@ -121,7 +122,7 @@ public final class DummySurface extends Surface { } } - private static class DummySurfaceThread extends HandlerThread implements Handler.Callback { + private static class PlaceholderSurfaceThread extends HandlerThread implements Handler.Callback { private static final int MSG_INIT = 1; private static final int MSG_RELEASE = 2; @@ -130,13 +131,13 @@ public final class DummySurface extends Surface { private @MonotonicNonNull Handler handler; @Nullable private Error initError; @Nullable private RuntimeException initException; - @Nullable private DummySurface surface; + @Nullable private PlaceholderSurface surface; - public DummySurfaceThread() { - super("ExoPlayer:DummySurface"); + public PlaceholderSurfaceThread() { + super("ExoPlayer:PlaceholderSurface"); } - public DummySurface init(@SecureMode int secureMode) { + public PlaceholderSurface init(@SecureMode int secureMode) { start(); handler = new Handler(getLooper(), /* callback= */ this); eglSurfaceTexture = new EGLSurfaceTexture(handler); @@ -176,10 +177,10 @@ public final class DummySurface extends Surface { try { initInternal(/* secureMode= */ msg.arg1); } catch (RuntimeException e) { - Log.e(TAG, "Failed to initialize dummy surface", e); + Log.e(TAG, "Failed to initialize placeholder surface", e); initException = e; } catch (Error e) { - Log.e(TAG, "Failed to initialize dummy surface", e); + Log.e(TAG, "Failed to initialize placeholder surface", e); initError = e; } finally { synchronized (this) { @@ -191,7 +192,7 @@ public final class DummySurface extends Surface { try { releaseInternal(); } catch (Throwable e) { - Log.e(TAG, "Failed to release dummy surface", e); + Log.e(TAG, "Failed to release placeholder surface", e); } finally { quit(); } @@ -205,7 +206,7 @@ public final class DummySurface extends Surface { Assertions.checkNotNull(eglSurfaceTexture); eglSurfaceTexture.init(secureMode); this.surface = - new DummySurface( + new PlaceholderSurface( this, eglSurfaceTexture.getSurfaceTexture(), secureMode != SECURE_MODE_NONE); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoFrameReleaseHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoFrameReleaseHelper.java index 00aac0e00e..bfac6433f6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoFrameReleaseHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoFrameReleaseHelper.java @@ -168,7 +168,7 @@ public final class VideoFrameReleaseHelper { * @param surface The new {@link Surface}, or {@code null} if the renderer does not have one. */ public void onSurfaceChanged(@Nullable Surface surface) { - if (surface instanceof DummySurface) { + if (surface instanceof PlaceholderSurface) { // We don't care about dummy surfaces for release timing, since they're not visible. surface = null; } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultLoadControlTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultLoadControlTest.java index b0fa141e12..2700ab4f85 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultLoadControlTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/DefaultLoadControlTest.java @@ -18,9 +18,9 @@ package androidx.media3.exoplayer; import static com.google.common.truth.Truth.assertThat; import androidx.media3.common.C; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.DefaultLoadControl.Builder; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.test.ext.junit.runners.AndroidJUnit4; 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 67300997f5..a30b75a4ed 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -108,9 +108,8 @@ import androidx.media3.common.Player.PositionInfo; import androidx.media3.common.Timeline; import androidx.media3.common.Timeline.Window; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TracksInfo; +import androidx.media3.common.TracksInfo.TrackGroupInfo; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Clock; import androidx.media3.common.util.Util; @@ -127,6 +126,7 @@ import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.source.SinglePeriodTimeline; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.upstream.Allocation; @@ -277,8 +277,15 @@ public final class ExoPlayerTest { argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)); inOrder .verify(mockListener) - .onTracksChanged( - eq(new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT))), any()); + .onTracksInfoChanged( + eq( + new TracksInfo( + ImmutableList.of( + new TrackGroupInfo( + new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT), + /* adaptiveSupported= */ false, + new int[] {C.FORMAT_HANDLED}, + /* tracksSelected= */ new boolean[] {true}))))); inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt()); inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt()); assertThat(renderer.getFormatsRead()).containsExactly(ExoPlayerTestRunner.VIDEO_FORMAT); @@ -649,8 +656,15 @@ public final class ExoPlayerTest { argThat(noUid(thirdTimeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)); inOrder .verify(mockPlayerListener) - .onTracksChanged( - eq(new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT))), any()); + .onTracksInfoChanged( + eq( + new TracksInfo( + ImmutableList.of( + new TrackGroupInfo( + new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT), + /* adaptiveSupported= */ false, + new int[] {C.FORMAT_HANDLED}, + /* tracksSelected= */ new boolean[] {true}))))); assertThat(renderer.isEnded).isTrue(); } @@ -784,12 +798,6 @@ public final class ExoPlayerTest { fakeMediaSource.setNewSourceInfo(adErrorTimeline); player.play(); runUntilPlaybackState(player, Player.STATE_ENDED); - Timeline.Window window = - player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, new Timeline.Window()); - Timeline.Period period = - player - .getCurrentTimeline() - .getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true); player.release(); // Content to content transition is ignored. @@ -3418,8 +3426,7 @@ public final class ExoPlayerTest { .waitForPendingPlayerCommands() .play() .build(); - List trackGroupsList = new ArrayList<>(); - List trackSelectionsList = new ArrayList<>(); + List tracksInfoList = new ArrayList<>(); new ExoPlayerTestRunner.Builder(context) .setMediaSources(mediaSource) .setSupportedFormats(ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT) @@ -3427,25 +3434,21 @@ public final class ExoPlayerTest { .setPlayerListener( new Player.Listener() { @Override - public void onTracksChanged( - TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - trackGroupsList.add(trackGroups); - trackSelectionsList.add(trackSelections); + public void onTracksInfoChanged(TracksInfo tracksInfo) { + tracksInfoList.add(tracksInfo); } }) .build() .start() .blockUntilEnded(TIMEOUT_MS); - assertThat(trackGroupsList).hasSize(3); + assertThat(tracksInfoList).hasSize(3); // First track groups of the 1st period are reported. - // Then the seek to an unprepared period will result in empty track groups and selections being - // returned. + // Then the seek to an unprepared period will result in empty track groups being returned. // Then the track groups of the 2nd period are reported. - assertThat(trackGroupsList.get(0).get(0).getFormat(0)) + assertThat(tracksInfoList.get(0).getTrackGroupInfos().get(0).getTrackFormat(0)) .isEqualTo(ExoPlayerTestRunner.VIDEO_FORMAT); - assertThat(trackGroupsList.get(1)).isEqualTo(TrackGroupArray.EMPTY); - assertThat(trackSelectionsList.get(1).get(0)).isNull(); - assertThat(trackGroupsList.get(2).get(0).getFormat(0)) + assertThat(tracksInfoList.get(1)).isEqualTo(TracksInfo.EMPTY); + assertThat(tracksInfoList.get(2).getTrackGroupInfos().get(0).getTrackFormat(0)) .isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT); } @@ -7991,7 +7994,6 @@ public final class ExoPlayerTest { }; AtomicReference timelineAfterError = new AtomicReference<>(); AtomicReference trackInfosAfterError = new AtomicReference<>(); - AtomicReference trackSelectionsAfterError = new AtomicReference<>(); AtomicInteger mediaItemIndexAfterError = new AtomicInteger(); ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG) @@ -8005,7 +8007,6 @@ public final class ExoPlayerTest { public void onPlayerError(EventTime eventTime, PlaybackException error) { timelineAfterError.set(player.getCurrentTimeline()); trackInfosAfterError.set(player.getCurrentTracksInfo()); - trackSelectionsAfterError.set(player.getCurrentTrackSelections()); mediaItemIndexAfterError.set(player.getCurrentMediaItemIndex()); } }); @@ -8037,8 +8038,8 @@ public final class ExoPlayerTest { assertThat(trackInfosAfterError.get().getTrackGroupInfos()).hasSize(1); assertThat(trackInfosAfterError.get().getTrackGroupInfos().get(0).getTrackFormat(0)) .isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT); - assertThat(trackSelectionsAfterError.get().get(0)).isNull(); // Video renderer. - assertThat(trackSelectionsAfterError.get().get(1)).isNotNull(); // Audio renderer. + assertThat(trackInfosAfterError.get().isTypeSelected(C.TRACK_TYPE_VIDEO)).isFalse(); + assertThat(trackInfosAfterError.get().isTypeSelected(C.TRACK_TYPE_AUDIO)).isTrue(); } @Test @@ -10422,7 +10423,7 @@ public final class ExoPlayerTest { verify(listener, atLeastOnce()).onShuffleModeEnabledChanged(anyBoolean()); verify(listener, atLeastOnce()).onPlaybackStateChanged(anyInt()); verify(listener, atLeastOnce()).onIsLoadingChanged(anyBoolean()); - verify(listener, atLeastOnce()).onTracksChanged(any(), any()); + verify(listener, atLeastOnce()).onTracksInfoChanged(any()); verify(listener, atLeastOnce()).onMediaMetadataChanged(any()); verify(listener, atLeastOnce()).onPlayWhenReadyChanged(anyBoolean(), anyInt()); verify(listener, atLeastOnce()).onIsPlayingChanged(anyBoolean()); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MetadataRetrieverTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MetadataRetrieverTest.java index 9567b90184..b9b5756503 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MetadataRetrieverTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MetadataRetrieverTest.java @@ -26,7 +26,7 @@ import android.net.Uri; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; -import androidx.media3.common.TrackGroupArray; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.extractor.metadata.mp4.MdtaMetadataEntry; import androidx.media3.extractor.metadata.mp4.MotionPhotoMetadata; import androidx.media3.extractor.metadata.mp4.SlowMotionData; diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java index ab60bd2bc4..35f99a3f6e 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java @@ -192,10 +192,18 @@ public final class DefaultAnalyticsCollectorTest { private EventWindowAndPeriodId window0Period1Seq0; private EventWindowAndPeriodId window1Period0Seq1; + /** + * Verify that {@link DefaultAnalyticsCollector} explicitly overrides all {@link Player.Listener} + * methods. + */ @Test public void defaultAnalyticsCollector_overridesAllPlayerListenerMethods() throws Exception { - // Verify that AnalyticsCollector forwards all Player.Listener methods to AnalyticsListener. for (Method method : Player.Listener.class.getDeclaredMethods()) { + if (method.isSynthetic()) { + // JaCoCo inserts synthetic methods. See "My code uses reflection. Why does it fail when I + // execute it with JaCoCo?": https://www.eclemma.org/jacoco/trunk/doc/faq.html + continue; + } assertThat( DefaultAnalyticsCollector.class .getMethod(method.getName(), method.getParameterTypes()) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapterTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapterTest.java index 0401f95e1d..98707a3226 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapterTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapterTest.java @@ -54,8 +54,7 @@ public class AsynchronousMediaCodecAdapterTest { new AsynchronousMediaCodecAdapter.Factory( /* callbackThreadSupplier= */ () -> callbackThread, /* queueingThreadSupplier= */ () -> queueingThread, - /* synchronizeCodecInteractionsWithQueueing= */ false, - /* enableImmediateCodecStartAfterFlush= */ false) + /* synchronizeCodecInteractionsWithQueueing= */ false) .createAdapter(configuration); bufferInfo = new MediaCodec.BufferInfo(); // After starting the MediaCodec, the ShadowMediaCodec offers input buffer 0. We advance the diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallbackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallbackTest.java index fea229347e..81df9fd6fb 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallbackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallbackTest.java @@ -94,7 +94,7 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0); asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1); callbackHandler.post(() -> beforeFlushCompletes.set(true)); - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); callbackHandler.post(() -> flushCompleted.set(true)); while (!beforeFlushCompletes.get()) { shadowCallbackLooper.runOneTask(); @@ -113,7 +113,7 @@ public class AsynchronousMediaCodecCallbackTest { // Send two input buffers to the callback and then flush(). asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0); asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1); - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback thread so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -132,7 +132,7 @@ public class AsynchronousMediaCodecCallbackTest { // another input buffer. asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 0); asynchronousMediaCodecCallback.onInputBufferAvailable(codec, 1); - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback thread to complete flush. shadowOf(callbackThread.getLooper()).idle(); @@ -207,7 +207,7 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo); callbackHandler.post(() -> beforeFlushCompletes.set(true)); - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); callbackHandler.post(() -> flushCompleted.set(true)); while (beforeFlushCompletes.get()) { shadowCallbackLooper.runOneTask(); @@ -227,7 +227,7 @@ public class AsynchronousMediaCodecCallbackTest { MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo); - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -248,7 +248,7 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputFormatChanged(codec, createMediaFormat("format0")); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 0, bufferInfo); asynchronousMediaCodecCallback.onOutputBufferAvailable(codec, 1, bufferInfo); - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -275,7 +275,7 @@ public class AsynchronousMediaCodecCallbackTest { MediaFormat pendingMediaFormat = new MediaFormat(); asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat); // flush() should not discard the last format. - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -302,7 +302,7 @@ public class AsynchronousMediaCodecCallbackTest { MediaFormat pendingMediaFormat = new MediaFormat(); asynchronousMediaCodecCallback.onOutputFormatChanged(codec, pendingMediaFormat); // flush() should not discard the last format. - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -367,7 +367,7 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputFormatChanged(codec, format); asynchronousMediaCodecCallback.dequeueOutputBufferIndex(new MediaCodec.BufferInfo()); - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the callback looper so that flush() completes. shadowOf(callbackThreadLooper).idle(); @@ -390,7 +390,7 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputFormatChanged(codec, createMediaFormat("format1")); asynchronousMediaCodecCallback.onOutputBufferAvailable( codec, /* index= */ 1, new MediaCodec.BufferInfo()); - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); new Handler(callbackThreadLooper).post(() -> flushCompleted.set(true)); // Progress the looper so that flush is completed shadowCallbackLooper.idle(); @@ -419,11 +419,11 @@ public class AsynchronousMediaCodecCallbackTest { asynchronousMediaCodecCallback.onOutputBufferAvailable( codec, /* index= */ 0, new MediaCodec.BufferInfo()); // Flush and progress the looper so that flush is completed. - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); callbackThreadHandler.post(flushCompleted::incrementAndGet); shadowCallbackLooper.idle(); // Flush again, the pending format from the first flush should remain as pending. - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); callbackThreadHandler.post(flushCompleted::incrementAndGet); shadowCallbackLooper.idle(); asynchronousMediaCodecCallback.onOutputBufferAvailable( @@ -441,7 +441,7 @@ public class AsynchronousMediaCodecCallbackTest { public void flush_withPendingError_resetsError() throws Exception { asynchronousMediaCodecCallback.onError(codec, createCodecException()); // Calling flush should clear any pending error. - asynchronousMediaCodecCallback.flush(/* codec= */ null); + asynchronousMediaCodecCallback.flush(); assertThat(asynchronousMediaCodecCallback.dequeueInputBufferIndex()) .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactoryTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactoryTest.java index 33c2c5bba1..2dcc9fdae3 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactoryTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactoryTest.java @@ -18,7 +18,7 @@ package androidx.media3.exoplayer.offline; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -35,7 +35,7 @@ public final class DefaultDownloaderFactoryTest { CacheDataSource.Factory cacheDataSourceFactory = new CacheDataSource.Factory() .setCache(Mockito.mock(Cache.class)) - .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + .setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY); DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java index 68b6769fa8..477763d395 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java @@ -15,6 +15,7 @@ */ package androidx.media3.exoplayer.offline; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; @@ -26,12 +27,14 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; +import androidx.media3.common.TrackSelectionOverride; +import androidx.media3.common.TrackSelectionParameters; import androidx.media3.exoplayer.Renderer; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.offline.DownloadHelper.Callback; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo; @@ -41,7 +44,7 @@ import androidx.media3.test.utils.FakeMediaSource; import androidx.media3.test.utils.FakeRenderer; import androidx.media3.test.utils.FakeTimeline; import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition; -import androidx.test.core.app.ApplicationProvider; +import androidx.media3.test.utils.TestUtil; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.IOException; import java.util.ArrayList; @@ -64,12 +67,8 @@ public class DownloadHelperTest { new Object[] {TEST_MANIFEST}, new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ new Object())); - private static final Format VIDEO_FORMAT_LOW = createVideoFormat(/* bitrate= */ 200_000); - private static final Format VIDEO_FORMAT_HIGH = createVideoFormat(/* bitrate= */ 800_000); - - private static final TrackGroup TRACK_GROUP_VIDEO_BOTH = - new TrackGroup(VIDEO_FORMAT_LOW, VIDEO_FORMAT_HIGH); - private static final TrackGroup TRACK_GROUP_VIDEO_SINGLE = new TrackGroup(VIDEO_FORMAT_LOW); + private static TrackGroup trackGroupVideoLow; + private static TrackGroup trackGroupVideoLowAndHigh; private static TrackGroup trackGroupAudioUs; private static TrackGroup trackGroupAudioZh; private static TrackGroup trackGroupTextUs; @@ -81,25 +80,29 @@ public class DownloadHelperTest { @BeforeClass public static void staticSetUp() { - Format audioFormatUs = createAudioFormat(/* language= */ "US"); - Format audioFormatZh = createAudioFormat(/* language= */ "ZH"); - Format textFormatUs = createTextFormat(/* language= */ "US"); - Format textFormatZh = createTextFormat(/* language= */ "ZH"); + Format videoFormatLow = createVideoFormat(/* bitrate= */ 200_000); + Format videoFormatHigh = createVideoFormat(/* bitrate= */ 800_000); + Format audioFormatEn = createAudioFormat(/* language= */ "en"); + Format audioFormatDe = createAudioFormat(/* language= */ "de"); + Format textFormatEn = createTextFormat(/* language= */ "en"); + Format textFormatDe = createTextFormat(/* language= */ "de"); - trackGroupAudioUs = new TrackGroup(audioFormatUs); - trackGroupAudioZh = new TrackGroup(audioFormatZh); - trackGroupTextUs = new TrackGroup(textFormatUs); - trackGroupTextZh = new TrackGroup(textFormatZh); + trackGroupVideoLow = new TrackGroup(videoFormatLow); + trackGroupVideoLowAndHigh = new TrackGroup(videoFormatLow, videoFormatHigh); + trackGroupAudioUs = new TrackGroup(audioFormatEn); + trackGroupAudioZh = new TrackGroup(audioFormatDe); + trackGroupTextUs = new TrackGroup(textFormatEn); + trackGroupTextZh = new TrackGroup(textFormatDe); TrackGroupArray trackGroupArrayAll = new TrackGroupArray( - TRACK_GROUP_VIDEO_BOTH, + trackGroupVideoLowAndHigh, trackGroupAudioUs, trackGroupAudioZh, trackGroupTextUs, trackGroupTextZh); TrackGroupArray trackGroupArraySingle = - new TrackGroupArray(TRACK_GROUP_VIDEO_SINGLE, trackGroupAudioUs); + new TrackGroupArray(trackGroupVideoLow, trackGroupAudioUs); trackGroupArrays = new TrackGroupArray[] {trackGroupArrayAll, trackGroupArraySingle}; testMediaItem = @@ -127,18 +130,14 @@ public class DownloadHelperTest { public void getManifest_returnsManifest() throws Exception { prepareDownloadHelper(downloadHelper); - Object manifest = downloadHelper.getManifest(); - - assertThat(manifest).isEqualTo(TEST_MANIFEST); + assertThat(downloadHelper.getManifest()).isEqualTo(TEST_MANIFEST); } @Test public void getPeriodCount_returnsPeriodCount() throws Exception { prepareDownloadHelper(downloadHelper); - int periodCount = downloadHelper.getPeriodCount(); - - assertThat(periodCount).isEqualTo(2); + assertThat(downloadHelper.getPeriodCount()).isEqualTo(2); } @Test @@ -175,7 +174,7 @@ public class DownloadHelperTest { assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 1)) .isEqualTo(trackGroupAudioZh); assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 2).get(/* index= */ 0)) - .isEqualTo(TRACK_GROUP_VIDEO_BOTH); + .isEqualTo(trackGroupVideoLowAndHigh); assertThat(mappedTracks1.getRendererCount()).isEqualTo(3); assertThat(mappedTracks1.getRendererType(/* rendererIndex= */ 0)).isEqualTo(C.TRACK_TYPE_TEXT); @@ -187,7 +186,7 @@ public class DownloadHelperTest { assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 0)) .isEqualTo(trackGroupAudioUs); assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 2).get(/* index= */ 0)) - .isEqualTo(TRACK_GROUP_VIDEO_SINGLE); + .isEqualTo(trackGroupVideoLow); } @Test @@ -209,11 +208,11 @@ public class DownloadHelperTest { assertSingleTrackSelectionEquals(selectedText0, trackGroupTextUs, 0); assertSingleTrackSelectionEquals(selectedAudio0, trackGroupAudioUs, 0); - assertSingleTrackSelectionEquals(selectedVideo0, TRACK_GROUP_VIDEO_BOTH, 1); + assertSingleTrackSelectionEquals(selectedVideo0, trackGroupVideoLowAndHigh, 1); assertThat(selectedText1).isEmpty(); assertSingleTrackSelectionEquals(selectedAudio1, trackGroupAudioUs, 0); - assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0); + assertSingleTrackSelectionEquals(selectedVideo1, trackGroupVideoLow, 0); } @Test @@ -242,7 +241,7 @@ public class DownloadHelperTest { // Verify assertThat(selectedText1).isEmpty(); assertSingleTrackSelectionEquals(selectedAudio1, trackGroupAudioUs, 0); - assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0); + assertSingleTrackSelectionEquals(selectedVideo1, trackGroupVideoLow, 0); } @Test @@ -250,9 +249,9 @@ public class DownloadHelperTest { throws Exception { prepareDownloadHelper(downloadHelper); DefaultTrackSelector.Parameters parameters = - new DefaultTrackSelector.ParametersBuilder(ApplicationProvider.getApplicationContext()) - .setPreferredAudioLanguage("ZH") - .setPreferredTextLanguage("ZH") + new DefaultTrackSelector.ParametersBuilder(getApplicationContext()) + .setPreferredAudioLanguage("de") + .setPreferredTextLanguage("de") .setRendererDisabled(/* rendererIndex= */ 2, true) .build(); @@ -277,7 +276,7 @@ public class DownloadHelperTest { assertThat(selectedText1).isEmpty(); assertSingleTrackSelectionEquals(selectedAudio1, trackGroupAudioUs, 0); - assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0); + assertSingleTrackSelectionEquals(selectedVideo1, trackGroupVideoLow, 0); } @Test @@ -286,10 +285,10 @@ public class DownloadHelperTest { prepareDownloadHelper(downloadHelper); // Select parameters to require some merging of track groups because the new parameters add // all video tracks to initial video single track selection. - DefaultTrackSelector.Parameters parameters = - new DefaultTrackSelector.ParametersBuilder(ApplicationProvider.getApplicationContext()) - .setPreferredAudioLanguage("ZH") - .setPreferredTextLanguage("US") + TrackSelectionParameters parameters = + new TrackSelectionParameters.Builder(getApplicationContext()) + .setPreferredAudioLanguage("de") + .setPreferredTextLanguage("en") .build(); // Add only to one period selection to verify second period selection is untouched. @@ -311,11 +310,11 @@ public class DownloadHelperTest { assertThat(selectedAudio0).hasSize(2); assertTrackSelectionEquals(selectedAudio0.get(0), trackGroupAudioUs, 0); assertTrackSelectionEquals(selectedAudio0.get(1), trackGroupAudioZh, 0); - assertSingleTrackSelectionEquals(selectedVideo0, TRACK_GROUP_VIDEO_BOTH, 0, 1); + assertSingleTrackSelectionEquals(selectedVideo0, trackGroupVideoLowAndHigh, 0, 1); assertThat(selectedText1).isEmpty(); assertSingleTrackSelectionEquals(selectedAudio1, trackGroupAudioUs, 0); - assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0); + assertSingleTrackSelectionEquals(selectedVideo1, trackGroupVideoLow, 0); } @Test @@ -326,7 +325,7 @@ public class DownloadHelperTest { downloadHelper.clearTrackSelections(/* periodIndex= */ 1); // Add a non-default language, and a non-existing language (which will select the default). - downloadHelper.addAudioLanguagesToSelection("ZH", "Klingonese"); + downloadHelper.addAudioLanguagesToSelection("de", "Klingonese"); List selectedText0 = downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0); List selectedAudio0 = @@ -360,7 +359,7 @@ public class DownloadHelperTest { // Add a non-default language, and a non-existing language (which will select the default). downloadHelper.addTextLanguagesToSelection( - /* selectUndeterminedTextLanguage= */ true, "ZH", "Klingonese"); + /* selectUndeterminedTextLanguage= */ true, "de", "Klingonese"); List selectedText0 = downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0); List selectedAudio0 = @@ -390,14 +389,13 @@ public class DownloadHelperTest { prepareDownloadHelper(downloadHelper); // Ensure we have track groups with multiple indices, renderers with multiple track groups and // also renderers without any track groups. - DefaultTrackSelector.Parameters parameters = - new DefaultTrackSelector.ParametersBuilder(ApplicationProvider.getApplicationContext()) - .setPreferredAudioLanguage("ZH") - .setPreferredTextLanguage("US") + TrackSelectionParameters parameters = + new TrackSelectionParameters.Builder(getApplicationContext()) + .setPreferredAudioLanguage("de") + .setPreferredTextLanguage("en") .build(); downloadHelper.addTrackSelection(/* periodIndex= */ 0, parameters); - byte[] data = new byte[10]; - Arrays.fill(data, (byte) 123); + byte[] data = TestUtil.buildTestData(10); DownloadRequest downloadRequest = downloadHelper.getDownloadRequest(data); @@ -408,13 +406,37 @@ public class DownloadHelperTest { assertThat(downloadRequest.data).isEqualTo(data); assertThat(downloadRequest.streamKeys) .containsExactly( - new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 0, /* trackIndex= */ 0), - new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 0, /* trackIndex= */ 1), - new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 0), - new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 2, /* trackIndex= */ 0), - new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 3, /* trackIndex= */ 0), - new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 0, /* trackIndex= */ 0), - new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 1, /* trackIndex= */ 0)); + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 0, /* streamIndex= */ 0), + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 0, /* streamIndex= */ 1), + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* streamIndex= */ 0), + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 2, /* streamIndex= */ 0), + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 3, /* streamIndex= */ 0), + new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 0, /* streamIndex= */ 0), + new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 1, /* streamIndex= */ 0)); + } + + @Test + public void getDownloadRequest_createsDownloadRequest_withMultipleOverridesOfSameType() + throws Exception { + prepareDownloadHelper(downloadHelper); + + TrackSelectionParameters parameters = + new TrackSelectionParameters.Builder(getApplicationContext()) + .addOverride(new TrackSelectionOverride(trackGroupAudioUs, /* trackIndex= */ 0)) + .addOverride(new TrackSelectionOverride(trackGroupAudioZh, /* trackIndex= */ 0)) + .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, /* disabled= */ true) + .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, /* disabled= */ true) + .build(); + + downloadHelper.replaceTrackSelections(/* periodIndex= */ 0, parameters); + downloadHelper.clearTrackSelections(/* periodIndex= */ 1); + + DownloadRequest downloadRequest = downloadHelper.getDownloadRequest(/* data= */ null); + + assertThat(downloadRequest.streamKeys) + .containsExactly( + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* streamIndex= */ 0), + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 2, /* streamIndex= */ 0)); } private static void prepareDownloadHelper(DownloadHelper downloadHelper) throws Exception { diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/MergingMediaPeriodTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/MergingMediaPeriodTest.java index 22e21b5d54..02b35f9295 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/MergingMediaPeriodTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/MergingMediaPeriodTest.java @@ -23,7 +23,6 @@ import static com.google.common.truth.Truth.assertThat; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.exoplayer.FormatHolder; import androidx.media3.exoplayer.drm.DrmSessionEventListener; diff --git a/libraries/common/src/test/java/androidx/media3/common/TrackGroupArrayTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/TrackGroupArrayTest.java similarity index 91% rename from libraries/common/src/test/java/androidx/media3/common/TrackGroupArrayTest.java rename to libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/TrackGroupArrayTest.java index 26078cb679..4557f251b9 100644 --- a/libraries/common/src/test/java/androidx/media3/common/TrackGroupArrayTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/TrackGroupArrayTest.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.media3.common; +package androidx.media3.exoplayer.source; import static com.google.common.truth.Truth.assertThat; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.TrackGroup; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelectionTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelectionTest.java index b8cc647d9f..6c7061b414 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelectionTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelectionTest.java @@ -24,7 +24,6 @@ import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackSelection; import androidx.media3.datasource.DataSpec; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.chunk.BaseMediaChunkIterator; 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 442725a2f5..2f53ce0f01 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 @@ -40,8 +40,6 @@ import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelection; import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.TracksInfo; import androidx.media3.common.util.Util; @@ -50,6 +48,7 @@ import androidx.media3.exoplayer.RendererCapabilities; import androidx.media3.exoplayer.RendererCapabilities.Capabilities; import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.Parameters; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.ParametersBuilder; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride; @@ -302,7 +301,7 @@ public final class DefaultTrackSelectorTest { trackSelector.setParameters( trackSelector .buildUponParameters() - .setOverrideForType(new TrackSelectionOverride(videoGroupH264)) + .setOverrideForType(new TrackSelectionOverride(videoGroupH264, /* trackIndex= */ 0)) .build()); TrackSelectorResult result = trackSelector.selectTracks( @@ -319,7 +318,7 @@ public final class DefaultTrackSelectorTest { trackSelector.setParameters( trackSelector .buildUponParameters() - .setOverrideForType(new TrackSelectionOverride(videoGroupAv1)) + .setOverrideForType(new TrackSelectionOverride(videoGroupAv1, /* trackIndex= */ 0)) .build()); result = trackSelector.selectTracks( @@ -350,7 +349,8 @@ public final class DefaultTrackSelectorTest { trackSelector.setParameters( trackSelector .buildUponParameters() - .setOverrideForType(new TrackSelectionOverride(audioGroupUnsupported)) + .setOverrideForType( + new TrackSelectionOverride(audioGroupUnsupported, /* trackIndex= */ 0)) .build()); TrackSelectorResult result = trackSelector.selectTracks( @@ -392,7 +392,9 @@ public final class DefaultTrackSelectorTest { public void selectVideoAudioTracks_withDisabledAudioType_onlyVideoIsSelected() throws ExoPlaybackException { trackSelector.setParameters( - defaultParameters.buildUpon().setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_AUDIO))); + defaultParameters + .buildUpon() + .setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, /* disabled= */ true)); TrackSelectorResult result = trackSelector.selectTracks( @@ -407,11 +409,12 @@ public final class DefaultTrackSelectorTest { /** Tests that a disabled track type can be enabled again. */ @Test + @SuppressWarnings("deprecation") public void selectTracks_withClearedDisabledTrackType_selectsAll() throws ExoPlaybackException { trackSelector.setParameters( trackSelector .buildUponParameters() - .setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_AUDIO)) + .setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, /* disabled= */ true) .setDisabledTrackTypes(ImmutableSet.of())); TrackSelectorResult result = @@ -426,7 +429,9 @@ public final class DefaultTrackSelectorTest { public void selectTracks_withDisabledNoneTracksAndNoSampleRenderer_disablesNoSampleRenderer() throws ExoPlaybackException { trackSelector.setParameters( - defaultParameters.buildUpon().setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_NONE))); + defaultParameters + .buildUpon() + .setTrackTypeDisabled(C.TRACK_TYPE_NONE, /* disabled= */ true)); TrackSelectorResult result = trackSelector.selectTracks( @@ -1125,7 +1130,7 @@ public final class DefaultTrackSelectorTest { // selected. trackGroups = wrapFormats(defaultOnly, noFlag, forcedOnly, forcedDefault); trackSelector.setParameters( - defaultParameters.buildUpon().setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT)); + defaultParameters.buildUpon().setIgnoredTextSelectionFlags(C.SELECTION_FLAG_DEFAULT)); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections[0]); @@ -1136,8 +1141,7 @@ public final class DefaultTrackSelectorTest { trackSelector .getParameters() .buildUpon() - .setDisabledTextTrackSelectionFlags( - C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED)); + .setIgnoredTextSelectionFlags(C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED)); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections[0]); @@ -1155,7 +1159,7 @@ public final class DefaultTrackSelectorTest { trackSelector .getParameters() .buildUpon() - .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT)); + .setIgnoredTextSelectionFlags(C.SELECTION_FLAG_DEFAULT)); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections[0], trackGroups, noFlag); } @@ -2366,7 +2370,7 @@ public final class DefaultTrackSelectorTest { .setPreferredTextLanguages("de", "en") .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION) .setSelectUndeterminedTextLanguage(true) - .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_AUTOSELECT) + .setIgnoredTextSelectionFlags(C.SELECTION_FLAG_AUTOSELECT) // General .setForceLowestBitrate(false) .setForceHighestSupportedBitrate(true) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/MappingTrackSelectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/MappingTrackSelectorTest.java index 6badef1484..2adf524191 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/MappingTrackSelectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/MappingTrackSelectorTest.java @@ -16,7 +16,6 @@ package androidx.media3.exoplayer.trackselection; import static androidx.media3.common.MimeTypes.AUDIO_AAC; -import static androidx.media3.common.MimeTypes.VIDEO_H264; import static com.google.common.truth.Truth.assertThat; import android.util.Pair; @@ -25,10 +24,6 @@ import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelection; -import androidx.media3.common.TracksInfo; -import androidx.media3.common.TracksInfo.TrackGroupInfo; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.RendererCapabilities; @@ -36,9 +31,9 @@ import androidx.media3.exoplayer.RendererCapabilities.AdaptiveSupport; import androidx.media3.exoplayer.RendererCapabilities.Capabilities; import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.test.utils.FakeTimeline; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.common.collect.ImmutableList; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; @@ -140,64 +135,6 @@ public final class MappingTrackSelectorTest { return new TrackGroup(new Format.Builder().setSampleMimeType(sampleMimeType).build()); } - @Test - public void buildTrackInfos_withTestValues_isAsExpected() { - MappingTrackSelector.MappedTrackInfo mappedTrackInfo = - new MappingTrackSelector.MappedTrackInfo( - new String[] {"1", "2"}, - new int[] {C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_VIDEO}, - new TrackGroupArray[] { - new TrackGroupArray( - new TrackGroup("0", new Format.Builder().setSampleMimeType(AUDIO_AAC).build()), - new TrackGroup("1", new Format.Builder().setSampleMimeType(AUDIO_AAC).build())), - new TrackGroupArray( - new TrackGroup( - "2", - new Format.Builder().setSampleMimeType(VIDEO_H264).build(), - new Format.Builder().setSampleMimeType(VIDEO_H264).build())) - }, - new int[] { - RendererCapabilities.ADAPTIVE_SEAMLESS, RendererCapabilities.ADAPTIVE_NOT_SUPPORTED - }, - new int[][][] { - new int[][] {new int[] {C.FORMAT_HANDLED}, new int[] {C.FORMAT_UNSUPPORTED_SUBTYPE}}, - new int[][] {new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_EXCEEDS_CAPABILITIES}} - }, - new TrackGroupArray(new TrackGroup(new Format.Builder().build()))); - TrackSelection[] selections = - new TrackSelection[] { - new FixedTrackSelection(mappedTrackInfo.getTrackGroups(0).get(1), 0), - new FixedTrackSelection(mappedTrackInfo.getTrackGroups(1).get(0), 1) - }; - - TracksInfo tracksInfo = MappingTrackSelector.buildTracksInfo(selections, mappedTrackInfo); - - ImmutableList trackGroupInfos = tracksInfo.getTrackGroupInfos(); - assertThat(trackGroupInfos).hasSize(4); - assertThat(trackGroupInfos.get(0).getTrackGroup()) - .isEqualTo(mappedTrackInfo.getTrackGroups(0).get(0)); - assertThat(trackGroupInfos.get(1).getTrackGroup()) - .isEqualTo(mappedTrackInfo.getTrackGroups(0).get(1)); - assertThat(trackGroupInfos.get(2).getTrackGroup()) - .isEqualTo(mappedTrackInfo.getTrackGroups(1).get(0)); - assertThat(trackGroupInfos.get(3).getTrackGroup()) - .isEqualTo(mappedTrackInfo.getUnmappedTrackGroups().get(0)); - assertThat(trackGroupInfos.get(0).getTrackSupport(0)).isEqualTo(C.FORMAT_HANDLED); - assertThat(trackGroupInfos.get(1).getTrackSupport(0)).isEqualTo(C.FORMAT_UNSUPPORTED_SUBTYPE); - assertThat(trackGroupInfos.get(2).getTrackSupport(0)).isEqualTo(C.FORMAT_UNSUPPORTED_DRM); - assertThat(trackGroupInfos.get(2).getTrackSupport(1)).isEqualTo(C.FORMAT_EXCEEDS_CAPABILITIES); - assertThat(trackGroupInfos.get(3).getTrackSupport(0)).isEqualTo(C.FORMAT_UNSUPPORTED_TYPE); - assertThat(trackGroupInfos.get(0).isTrackSelected(0)).isFalse(); - assertThat(trackGroupInfos.get(1).isTrackSelected(0)).isTrue(); - assertThat(trackGroupInfos.get(2).isTrackSelected(0)).isFalse(); - assertThat(trackGroupInfos.get(2).isTrackSelected(1)).isTrue(); - assertThat(trackGroupInfos.get(3).isTrackSelected(0)).isFalse(); - assertThat(trackGroupInfos.get(0).getTrackType()).isEqualTo(C.TRACK_TYPE_AUDIO); - assertThat(trackGroupInfos.get(1).getTrackType()).isEqualTo(C.TRACK_TYPE_AUDIO); - assertThat(trackGroupInfos.get(2).getTrackType()).isEqualTo(C.TRACK_TYPE_VIDEO); - assertThat(trackGroupInfos.get(3).getTrackType()).isEqualTo(C.TRACK_TYPE_UNKNOWN); - } - /** * A {@link MappingTrackSelector} that stashes the {@link MappedTrackInfo} passed to {@link * #selectTracks(MappedTrackInfo, int[][][], int[], MediaPeriodId, Timeline)}. diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/TrackSelectionUtilTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/TrackSelectionUtilTest.java new file mode 100644 index 0000000000..2491df3a54 --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/TrackSelectionUtilTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.trackselection; + +import static androidx.media3.common.C.FORMAT_EXCEEDS_CAPABILITIES; +import static androidx.media3.common.C.FORMAT_HANDLED; +import static androidx.media3.common.C.FORMAT_UNSUPPORTED_DRM; +import static androidx.media3.common.C.FORMAT_UNSUPPORTED_SUBTYPE; +import static androidx.media3.common.C.FORMAT_UNSUPPORTED_TYPE; +import static androidx.media3.common.C.TRACK_TYPE_AUDIO; +import static androidx.media3.common.C.TRACK_TYPE_UNKNOWN; +import static androidx.media3.common.C.TRACK_TYPE_VIDEO; +import static androidx.media3.common.MimeTypes.AUDIO_AAC; +import static androidx.media3.common.MimeTypes.AUDIO_OPUS; +import static androidx.media3.common.MimeTypes.VIDEO_H264; +import static androidx.media3.exoplayer.RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; +import static androidx.media3.exoplayer.RendererCapabilities.ADAPTIVE_SEAMLESS; +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.Format; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.TracksInfo; +import androidx.media3.common.TracksInfo.TrackGroupInfo; +import androidx.media3.exoplayer.source.TrackGroupArray; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link TrackSelectionUtil}. */ +@RunWith(AndroidJUnit4.class) +public class TrackSelectionUtilTest { + + @Test + public void buildTrackInfos_withTestValues_isAsExpected() { + MappingTrackSelector.MappedTrackInfo mappedTrackInfo = + new MappingTrackSelector.MappedTrackInfo( + new String[] {"rendererName1", "rendererName2"}, + new int[] {TRACK_TYPE_AUDIO, TRACK_TYPE_VIDEO}, + new TrackGroupArray[] { + new TrackGroupArray( + new TrackGroup("0", new Format.Builder().setSampleMimeType(AUDIO_AAC).build()), + new TrackGroup("1", new Format.Builder().setSampleMimeType(AUDIO_OPUS).build())), + new TrackGroupArray( + new TrackGroup( + "2", + new Format.Builder().setSampleMimeType(VIDEO_H264).build(), + new Format.Builder().setSampleMimeType(VIDEO_H264).build())) + }, + new int[] {ADAPTIVE_SEAMLESS, ADAPTIVE_NOT_SUPPORTED}, + new int[][][] { + new int[][] {new int[] {FORMAT_HANDLED}, new int[] {FORMAT_UNSUPPORTED_SUBTYPE}}, + new int[][] {new int[] {FORMAT_UNSUPPORTED_DRM, FORMAT_EXCEEDS_CAPABILITIES}} + }, + new TrackGroupArray(new TrackGroup(new Format.Builder().build()))); + TrackSelection[] selections = + new TrackSelection[] { + new FixedTrackSelection(mappedTrackInfo.getTrackGroups(0).get(1), 0), + new FixedTrackSelection(mappedTrackInfo.getTrackGroups(1).get(0), 1) + }; + + TracksInfo tracksInfo = TrackSelectionUtil.buildTracksInfo(mappedTrackInfo, selections); + + ImmutableList trackGroupInfos = tracksInfo.getTrackGroupInfos(); + assertThat(trackGroupInfos).hasSize(4); + assertThat(trackGroupInfos.get(0).getTrackGroup()) + .isEqualTo(mappedTrackInfo.getTrackGroups(0).get(0)); + assertThat(trackGroupInfos.get(1).getTrackGroup()) + .isEqualTo(mappedTrackInfo.getTrackGroups(0).get(1)); + assertThat(trackGroupInfos.get(2).getTrackGroup()) + .isEqualTo(mappedTrackInfo.getTrackGroups(1).get(0)); + assertThat(trackGroupInfos.get(3).getTrackGroup()) + .isEqualTo(mappedTrackInfo.getUnmappedTrackGroups().get(0)); + assertThat(trackGroupInfos.get(0).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED); + assertThat(trackGroupInfos.get(1).getTrackSupport(0)).isEqualTo(FORMAT_UNSUPPORTED_SUBTYPE); + assertThat(trackGroupInfos.get(2).getTrackSupport(0)).isEqualTo(FORMAT_UNSUPPORTED_DRM); + assertThat(trackGroupInfos.get(2).getTrackSupport(1)).isEqualTo(FORMAT_EXCEEDS_CAPABILITIES); + assertThat(trackGroupInfos.get(3).getTrackSupport(0)).isEqualTo(FORMAT_UNSUPPORTED_TYPE); + assertThat(trackGroupInfos.get(0).isTrackSelected(0)).isFalse(); + assertThat(trackGroupInfos.get(1).isTrackSelected(0)).isTrue(); + assertThat(trackGroupInfos.get(2).isTrackSelected(0)).isFalse(); + assertThat(trackGroupInfos.get(2).isTrackSelected(1)).isTrue(); + assertThat(trackGroupInfos.get(3).isTrackSelected(0)).isFalse(); + assertThat(trackGroupInfos.get(0).getTrackType()).isEqualTo(TRACK_TYPE_AUDIO); + assertThat(trackGroupInfos.get(1).getTrackType()).isEqualTo(TRACK_TYPE_AUDIO); + assertThat(trackGroupInfos.get(2).getTrackType()).isEqualTo(TRACK_TYPE_VIDEO); + assertThat(trackGroupInfos.get(3).getTrackType()).isEqualTo(TRACK_TYPE_UNKNOWN); + } + + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) // Initialization of array of Lists. + public void buildTrackInfos_withMultipleSelectionForRenderer_isAsExpected() { + MappingTrackSelector.MappedTrackInfo mappedTrackInfo = + new MappingTrackSelector.MappedTrackInfo( + new String[] {"rendererName1", "rendererName2"}, + new int[] {TRACK_TYPE_AUDIO, TRACK_TYPE_VIDEO}, + new TrackGroupArray[] { + new TrackGroupArray( + new TrackGroup("0", new Format.Builder().setSampleMimeType(AUDIO_AAC).build()), + new TrackGroup( + "1", + new Format.Builder().setSampleMimeType(AUDIO_OPUS).setSampleRate(1).build(), + new Format.Builder().setSampleMimeType(AUDIO_OPUS).setSampleRate(2).build())), + new TrackGroupArray() + }, + new int[] {ADAPTIVE_SEAMLESS, ADAPTIVE_SEAMLESS}, + new int[][][] { + new int[][] {new int[] {FORMAT_HANDLED}, new int[] {FORMAT_HANDLED, FORMAT_HANDLED}}, + new int[][] {new int[0]} + }, + new TrackGroupArray()); + + List[] selections = + new List[] { + ImmutableList.of( + new FixedTrackSelection(mappedTrackInfo.getTrackGroups(0).get(0), 0), + new FixedTrackSelection(mappedTrackInfo.getTrackGroups(0).get(1), 1)), + ImmutableList.of() + }; + + TracksInfo tracksInfo = TrackSelectionUtil.buildTracksInfo(mappedTrackInfo, selections); + + ImmutableList trackGroupInfos = tracksInfo.getTrackGroupInfos(); + assertThat(trackGroupInfos).hasSize(2); + assertThat(trackGroupInfos.get(0).getTrackGroup()) + .isEqualTo(mappedTrackInfo.getTrackGroups(0).get(0)); + assertThat(trackGroupInfos.get(1).getTrackGroup()) + .isEqualTo(mappedTrackInfo.getTrackGroups(0).get(1)); + assertThat(trackGroupInfos.get(0).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED); + assertThat(trackGroupInfos.get(1).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED); + assertThat(trackGroupInfos.get(1).getTrackSupport(1)).isEqualTo(FORMAT_HANDLED); + assertThat(trackGroupInfos.get(0).isTrackSelected(0)).isTrue(); + assertThat(trackGroupInfos.get(1).isTrackSelected(0)).isFalse(); + assertThat(trackGroupInfos.get(1).isTrackSelected(1)).isTrue(); + assertThat(trackGroupInfos.get(0).getTrackType()).isEqualTo(TRACK_TYPE_AUDIO); + assertThat(trackGroupInfos.get(1).getTrackType()).isEqualTo(TRACK_TYPE_AUDIO); + } +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/TrackSelectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/TrackSelectorTest.java index 29f705a099..a1551d0200 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/TrackSelectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/TrackSelectorTest.java @@ -20,10 +20,10 @@ import static org.junit.Assert.fail; import androidx.annotation.Nullable; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroupArray; import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.RendererCapabilities; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.TrackSelector.InvalidationListener; import androidx.media3.exoplayer.upstream.BandwidthMeter; import androidx.test.ext.junit.runners.AndroidJUnit4; diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java index 23b9262cf0..0021106f10 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java @@ -28,7 +28,6 @@ import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Util; import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.SeekParameters; @@ -50,6 +49,7 @@ import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher; import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.SequenceableLoader; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.source.chunk.ChunkSampleStream; import androidx.media3.exoplayer.source.chunk.ChunkSampleStream.EmbeddedSampleStream; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashUtil.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashUtil.java index c1a92d22d3..91465ef5ad 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashUtil.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashUtil.java @@ -24,7 +24,6 @@ import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSpec; -import androidx.media3.datasource.HttpDataSource; import androidx.media3.exoplayer.dash.manifest.DashManifest; import androidx.media3.exoplayer.dash.manifest.DashManifestParser; import androidx.media3.exoplayer.dash.manifest.Period; @@ -85,7 +84,7 @@ public final class DashUtil { /** * Loads a DASH manifest. * - * @param dataSource The {@link HttpDataSource} from which the manifest should be read. + * @param dataSource The {@link DataSource} from which the manifest should be read. * @param uri The {@link Uri} of the manifest to be read. * @return An instance of {@link DashManifest}. * @throws IOException Thrown when there is an error while loading. @@ -97,7 +96,7 @@ public final class DashUtil { /** * Loads a {@link Format} for acquiring keys for a given period in a DASH manifest. * - * @param dataSource The {@link HttpDataSource} from which data should be loaded. + * @param dataSource The {@link DataSource} from which data should be loaded. * @param period The {@link Period}. * @return The loaded {@link Format}, or null if none is defined. * @throws IOException Thrown when there is an error while loading. diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java index 6ee1ab5d6c..9ac84cb9b6 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java @@ -1189,41 +1189,41 @@ public class DashManifestParser extends DefaultHandler xpp.nextToken(); while (!XmlPullParserUtil.isEndTag(xpp, "Event")) { switch (xpp.getEventType()) { - case (XmlPullParser.START_DOCUMENT): + case XmlPullParser.START_DOCUMENT: xmlSerializer.startDocument(null, false); break; - case (XmlPullParser.END_DOCUMENT): + case XmlPullParser.END_DOCUMENT: xmlSerializer.endDocument(); break; - case (XmlPullParser.START_TAG): + case XmlPullParser.START_TAG: xmlSerializer.startTag(xpp.getNamespace(), xpp.getName()); for (int i = 0; i < xpp.getAttributeCount(); i++) { xmlSerializer.attribute( xpp.getAttributeNamespace(i), xpp.getAttributeName(i), xpp.getAttributeValue(i)); } break; - case (XmlPullParser.END_TAG): + case XmlPullParser.END_TAG: xmlSerializer.endTag(xpp.getNamespace(), xpp.getName()); break; - case (XmlPullParser.TEXT): + case XmlPullParser.TEXT: xmlSerializer.text(xpp.getText()); break; - case (XmlPullParser.CDSECT): + case XmlPullParser.CDSECT: xmlSerializer.cdsect(xpp.getText()); break; - case (XmlPullParser.ENTITY_REF): + case XmlPullParser.ENTITY_REF: xmlSerializer.entityRef(xpp.getText()); break; - case (XmlPullParser.IGNORABLE_WHITESPACE): + case XmlPullParser.IGNORABLE_WHITESPACE: xmlSerializer.ignorableWhitespace(xpp.getText()); break; - case (XmlPullParser.PROCESSING_INSTRUCTION): + case XmlPullParser.PROCESSING_INSTRUCTION: xmlSerializer.processingInstruction(xpp.getText()); break; - case (XmlPullParser.COMMENT): + case XmlPullParser.COMMENT: xmlSerializer.comment(xpp.getText()); break; - case (XmlPullParser.DOCDECL): + case XmlPullParser.DOCDECL: xmlSerializer.docdecl(xpp.getText()); break; default: // fall out diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java index 1a518729ad..e7c836c0dc 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashMediaPeriodTest.java @@ -21,7 +21,6 @@ import android.net.Uri; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.dash.PlayerEmsgHandler.PlayerEmsgCallback; @@ -33,6 +32,7 @@ import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.source.CompositeSequenceableLoaderFactory; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSourceEventListener; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoaderErrorThrower; diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashUtilTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashUtilTest.java index 4afe48ee38..c1e6f1524e 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashUtilTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashUtilTest.java @@ -22,7 +22,7 @@ import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData.SchemeData; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.exoplayer.dash.manifest.AdaptationSet; import androidx.media3.exoplayer.dash.manifest.BaseUrl; import androidx.media3.exoplayer.dash.manifest.Period; @@ -43,28 +43,28 @@ public final class DashUtilTest { @Test public void loadDrmInitDataFromManifest() throws Exception { Period period = newPeriod(newAdaptationSet(newRepresentation(newDrmInitData()))); - Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period); + Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period); assertThat(format.drmInitData).isEqualTo(newDrmInitData()); } @Test public void loadDrmInitDataMissing() throws Exception { Period period = newPeriod(newAdaptationSet(newRepresentation(null /* no init data */))); - Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period); + Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period); assertThat(format.drmInitData).isNull(); } @Test public void loadDrmInitDataNoRepresentations() throws Exception { Period period = newPeriod(newAdaptationSet(/* no representation */ )); - Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period); + Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period); assertThat(format).isNull(); } @Test public void loadDrmInitDataNoAdaptationSets() throws Exception { Period period = newPeriod(/* no adaptation set */ ); - Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period); + Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period); assertThat(format).isNull(); } diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java index 46b90628c0..b42a77ee42 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java @@ -31,7 +31,7 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSpec; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.NoOpCacheEvictor; @@ -86,7 +86,7 @@ public class DashDownloaderTest { CacheDataSource.Factory cacheDataSourceFactory = new CacheDataSource.Factory() .setCache(Mockito.mock(Cache.class)) - .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + .setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY); DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run); @@ -96,7 +96,7 @@ public class DashDownloaderTest { .setMimeType(MimeTypes.APPLICATION_MPD) .setStreamKeys( Collections.singletonList( - new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0))) + new StreamKey(/* groupIndex= */ 0, /* streamIndex= */ 0))) .build()); assertThat(downloader).isInstanceOf(DashDownloader.class); } 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 0bf8baf717..101bb7a2ed 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 @@ -25,7 +25,6 @@ import androidx.media3.common.Metadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -45,6 +44,7 @@ import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher; import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.SequenceableLoader; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; @@ -646,7 +646,8 @@ public final class HlsMediaPeriod int numberOfVideoCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_VIDEO); int numberOfAudioCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_AUDIO); boolean codecsStringAllowsChunklessPreparation = - numberOfAudioCodecs <= 1 + (numberOfAudioCodecs == 1 + || (numberOfAudioCodecs == 0 && multivariantPlaylist.audios.isEmpty())) && numberOfVideoCodecs <= 1 && numberOfAudioCodecs + numberOfVideoCodecs > 0; @C.TrackType diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java index 01a1af2027..ac3f1a45cf 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java @@ -33,7 +33,6 @@ import androidx.media3.common.Metadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.ParserException; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableByteArray; @@ -53,6 +52,7 @@ import androidx.media3.exoplayer.source.SampleQueue.UpstreamFormatChangedListene import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.SampleStream.ReadFlags; import androidx.media3.exoplayer.source.SequenceableLoader; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.source.chunk.Chunk; import androidx.media3.exoplayer.source.chunk.MediaChunkIterator; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java index 775323ce32..4e239e3502 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java @@ -39,7 +39,7 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; import androidx.media3.common.util.Util; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.NoOpCacheEvictor; @@ -104,7 +104,7 @@ public class HlsDownloaderTest { CacheDataSource.Factory cacheDataSourceFactory = new CacheDataSource.Factory() .setCache(Mockito.mock(Cache.class)) - .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + .setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY); DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run); @@ -114,7 +114,7 @@ public class HlsDownloaderTest { .setMimeType(MimeTypes.APPLICATION_M3U8) .setStreamKeys( Collections.singletonList( - new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0))) + new StreamKey(/* groupIndex= */ 0, /* streamIndex= */ 0))) .build()); assertThat(downloader).isInstanceOf(HlsDownloader.class); } diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java index d4176e67fc..ebf18e1cab 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java @@ -84,12 +84,14 @@ import java.util.Map; private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = MediaLibraryInfo.VERSION; /** - * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is - * the interval recommended by the IMA documentation. + * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 200 ms is + * the interval recommended by the Media Rating Council (MRC) for minimum polling of viewable + * video impressions. + * http://www.mediaratingcouncil.org/063014%20Viewable%20Ad%20Impression%20Guideline_Final.pdf. * * @see VideoAdPlayer.VideoAdPlayerCallback */ - private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100; + private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 200; /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */ private static final long IMA_DURATION_UNSET = -1L; diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index 87499ea227..a6d2390860 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -18,11 +18,12 @@ package androidx.media3.exoplayer.ima; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.msToUs; -import static androidx.media3.common.util.Util.secToUs; import static androidx.media3.common.util.Util.sum; import static androidx.media3.common.util.Util.usToMs; import static androidx.media3.exoplayer.ima.ImaUtil.expandAdGroupPlaceholder; import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow; +import static androidx.media3.exoplayer.ima.ImaUtil.secToMsRounded; +import static androidx.media3.exoplayer.ima.ImaUtil.secToUsRounded; import static androidx.media3.exoplayer.ima.ImaUtil.splitAdPlaybackStateForPeriods; import static androidx.media3.exoplayer.ima.ImaUtil.updateAdDurationAndPropagate; import static androidx.media3.exoplayer.ima.ImaUtil.updateAdDurationInAdGroup; @@ -105,11 +106,7 @@ import java.util.Map; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * MediaSource for IMA server side inserted ad streams. - * - *

    TODO(bachinger) add code snippet from PlayerActivity - */ +/** MediaSource for IMA server side inserted ad streams. */ @UnstableApi public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSource { @@ -119,8 +116,6 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou * *

    Apps can use the {@link ImaServerSideAdInsertionMediaSource.Factory} to customized the * {@link DefaultMediaSourceFactory} that is used to build a player: - * - *

    TODO(bachinger) add code snippet from PlayerActivity */ public static final class Factory implements MediaSource.Factory { @@ -333,23 +328,31 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou @Override public Bundle toBundle() { Bundle bundle = new Bundle(); - bundle.putSerializable(keyForField(FIELD_AD_PLAYBACK_STATES), adPlaybackStates); + Bundle adPlaybackStatesBundle = new Bundle(); + for (Map.Entry entry : adPlaybackStates.entrySet()) { + adPlaybackStatesBundle.putBundle(entry.getKey(), entry.getValue().toBundle()); + } + bundle.putBundle(keyForField(FIELD_AD_PLAYBACK_STATES), adPlaybackStatesBundle); return bundle; } /** Object that can restore {@link AdsLoader.State} from a {@link Bundle}. */ public static final Bundleable.Creator CREATOR = State::fromBundle; - @SuppressWarnings("unchecked") private static State fromBundle(Bundle bundle) { @Nullable - Map adPlaybackStateMap = - (Map) - bundle.getSerializable(keyForField(FIELD_AD_PLAYBACK_STATES)); - return new State( - adPlaybackStateMap != null - ? ImmutableMap.copyOf(adPlaybackStateMap) - : ImmutableMap.of()); + ImmutableMap.Builder adPlaybackStateMap = + new ImmutableMap.Builder<>(); + Bundle adPlaybackStateBundle = + checkNotNull(bundle.getBundle(keyForField(FIELD_AD_PLAYBACK_STATES))); + for (String key : adPlaybackStateBundle.keySet()) { + AdPlaybackState adPlaybackState = + AdPlaybackState.CREATOR.fromBundle( + checkNotNull(adPlaybackStateBundle.getBundle(key))); + adPlaybackStateMap.put( + key, AdPlaybackState.fromAdPlaybackState(/* adsId= */ key, adPlaybackState)); + } + return new State(adPlaybackStateMap.buildOrThrow()); } private static String keyForField(@FieldNumber int field) { @@ -461,6 +464,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou @Nullable private IOException loadError; private @MonotonicNonNull Timeline contentTimeline; private AdPlaybackState adPlaybackState; + private int firstSeenAdIndexInAdGroup; private ImaServerSideAdInsertionMediaSource( MediaItem mediaItem, @@ -658,19 +662,29 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou private static AdPlaybackState setVodAdGroupPlaceholders( List cuePoints, AdPlaybackState adPlaybackState) { + // TODO(b/192231683) Use getEndTimeMs()/getStartTimeMs() after jar target was removed for (int i = 0; i < cuePoints.size(); i++) { CuePoint cuePoint = cuePoints.get(i); + long fromPositionUs = msToUs(secToMsRounded(cuePoint.getStartTime())); adPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, - /* fromPositionUs= */ secToUs(cuePoint.getStartTime()), + /* fromPositionUs= */ fromPositionUs, /* contentResumeOffsetUs= */ 0, - // TODO(b/192231683) Use getEndTimeMs()/getStartTimeMs() after jar target was removed - /* adDurationsUs...= */ secToUs(cuePoint.getEndTime() - cuePoint.getStartTime())); + /* adDurationsUs...= */ getAdDuration( + /* startTimeSeconds= */ cuePoint.getStartTime(), + /* endTimeSeconds= */ cuePoint.getEndTime())); } return adPlaybackState; } + private static long getAdDuration(double startTimeSeconds, double endTimeSeconds) { + // startTimeSeconds and endTimeSeconds that are coming from the SDK, only have a precision of + // milliseconds so everything that is below a millisecond can be safely considered as coming + // from rounding issues. + return msToUs(secToMsRounded(endTimeSeconds - startTimeSeconds)); + } + private static AdPlaybackState setVodAdInPlaceholder(Ad ad, AdPlaybackState adPlaybackState) { AdPodInfo adPodInfo = ad.getAdPodInfo(); // Handle post rolls that have a podIndex of -1. @@ -682,9 +696,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou adPlaybackState = expandAdGroupPlaceholder( adGroupIndex, - /* adGroupDurationUs= */ secToUs(adPodInfo.getMaxDuration()), + /* adGroupDurationUs= */ msToUs(secToMsRounded(adPodInfo.getMaxDuration())), adIndexInAdGroup, - /* adDurationUs= */ secToUs(ad.getDuration()), + /* adDurationUs= */ msToUs(secToMsRounded(ad.getDuration())), /* adsInAdGroupCount= */ adPodInfo.getTotalAds(), adPlaybackState); } else if (adIndexInAdGroup < adGroup.count - 1) { @@ -692,27 +706,30 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou updateAdDurationInAdGroup( adGroupIndex, adIndexInAdGroup, - /* adDurationUs= */ secToUs(ad.getDuration()), + /* adDurationUs= */ msToUs(secToMsRounded(ad.getDuration())), adPlaybackState); } return adPlaybackState; } - private static AdPlaybackState addLiveAdBreak( + private AdPlaybackState addLiveAdBreak( Ad ad, long currentPeriodPositionUs, AdPlaybackState adPlaybackState) { AdPodInfo adPodInfo = ad.getAdPodInfo(); - long adDurationUs = secToUs(ad.getDuration()); + long adDurationUs = secToUsRounded(ad.getDuration()); int adIndexInAdGroup = adPodInfo.getAdPosition() - 1; - // TODO(b/208398934) Support seeking backwards. if (adIndexInAdGroup == 0 || adPlaybackState.adGroupCount == 1) { + firstSeenAdIndexInAdGroup = adIndexInAdGroup; + // Adjust count and ad index in case we joined the live stream within an ad group. + int adCount = adPodInfo.getTotalAds() - firstSeenAdIndexInAdGroup; + adIndexInAdGroup -= firstSeenAdIndexInAdGroup; // First ad of group. Create a new group with all ads. long[] adDurationsUs = updateAdDurationAndPropagate( - new long[adPodInfo.getTotalAds()], + new long[adCount], adIndexInAdGroup, adDurationUs, - secToUs(adPodInfo.getMaxDuration())); + msToUs(secToMsRounded(adPodInfo.getMaxDuration()))); adPlaybackState = addAdGroupToAdPlaybackState( adPlaybackState, @@ -721,6 +738,11 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou /* adDurationsUs...= */ adDurationsUs); } else { int adGroupIndex = adPlaybackState.adGroupCount - 2; + adIndexInAdGroup -= firstSeenAdIndexInAdGroup; + if (adPodInfo.getTotalAds() == adPodInfo.getAdPosition()) { + // Reset the ad index whe we are at the last ad in the group. + firstSeenAdIndexInAdGroup = 0; + } adPlaybackState = updateAdDurationInAdGroup(adGroupIndex, adIndexInAdGroup, adDurationUs, adPlaybackState); AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); @@ -857,7 +879,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou long positionInWindowUs = timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period()) .positionInWindowUs; - long currentPeriodPosition = msToUs(player.getCurrentPosition()) - positionInWindowUs; + long currentPeriodPosition = msToUs(player.getContentPosition()) - positionInWindowUs; newAdPlaybackState = addLiveAdBreak( event.getAd(), diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java index cede70d9cb..5619c7f13e 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaUtil.java @@ -54,7 +54,10 @@ import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.math.DoubleMath; import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -398,17 +401,13 @@ import java.util.Set; long elapsedAdGroupAdDurationUs = 0; for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) { contentTimeline.getPeriod(j, period, /* setIds= */ true); - // TODO(b/192231683) Remove subtracted US from ad group time when we can upgrade the SDK. - // Subtract one microsecond to work around rounding errors with adGroup.timeUs. - if (totalElapsedContentDurationUs < adGroup.timeUs - 1) { + if (totalElapsedContentDurationUs < adGroup.timeUs) { // Period starts before the ad group, so it is a content period. adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState); totalElapsedContentDurationUs += period.durationUs; } else { long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs; - // TODO(b/192231683) Remove additional US when we can upgrade the SDK. - // Add one microsecond to work around rounding errors with adGroup.timeUs. - if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs + 1) { + if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs) { // The period ends before the end of the ad group, so it is an ad period (Note: A VOD ad // reported by the IMA SDK spans multiple periods before the LOADED event arrives). adPlaybackStates.put( @@ -490,16 +489,12 @@ import java.util.Set; long elapsedAdGroupAdDurationUs = 0; for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) { contentTimeline.getPeriod(j, period, /* setIds= */ true); - // TODO(b/192231683) Remove subtracted US from ad group time when we can upgrade the SDK. - // Subtract one microsecond to work around rounding errors with adGroup.timeUs. - if (totalElapsedContentDurationUs < adGroup.timeUs - 1) { + if (totalElapsedContentDurationUs < adGroup.timeUs) { // Period starts before the ad group, so it is a content period. totalElapsedContentDurationUs += period.durationUs; } else { long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs; - // TODO(b/192231683) Remove additional US when we can upgrade the SDK. - // Add one microsecond to work around rounding errors with adGroup.timeUs. - if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs + 1) { + if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs) { // The period ends before the end of the ad group, so it is an ad period. if (j == adPeriodIndex) { return new Pair<>(/* adGroupIndex= */ i, adIndexInAdGroup); @@ -518,5 +513,31 @@ import java.util.Set; throw new IllegalStateException(); } + /** + * Converts a time in seconds to the corresponding time in microseconds. + * + *

    Fractional values are rounded to the nearest microsecond using {@link RoundingMode#HALF_UP}. + * + * @param timeSec The time in seconds. + * @return The corresponding time in microseconds. + */ + public static long secToUsRounded(double timeSec) { + return DoubleMath.roundToLong( + BigDecimal.valueOf(timeSec).scaleByPowerOfTen(6).doubleValue(), RoundingMode.HALF_UP); + } + + /** + * Converts a time in seconds to the corresponding time in milliseconds. + * + *

    Fractional values are rounded to the nearest millisecond using {@link RoundingMode#HALF_UP}. + * + * @param timeSec The time in seconds. + * @return The corresponding time in milliseconds. + */ + public static long secToMsRounded(double timeSec) { + return DoubleMath.roundToLong( + BigDecimal.valueOf(timeSec).scaleByPowerOfTen(3).doubleValue(), RoundingMode.HALF_UP); + } + private ImaUtil() {} } diff --git a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/FakeExoPlayer.java b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/FakeExoPlayer.java index 6a9602c976..8974a062a1 100644 --- a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/FakeExoPlayer.java +++ b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/FakeExoPlayer.java @@ -23,13 +23,13 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.util.Clock; import androidx.media3.common.util.ListenerSet; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.exoplayer.trackselection.TrackSelectionArray; import androidx.media3.test.utils.StubExoPlayer; /** A fake {@link ExoPlayer} for testing content/ad playback. */ @@ -79,7 +79,7 @@ import androidx.media3.test.utils.StubExoPlayer; PositionInfo oldPosition = new PositionInfo( windowUid, - /* windowIndex= */ 0, + /* mediaItemIndex= */ 0, mediaItem, periodUid, /* periodIndex= */ 0, @@ -97,7 +97,7 @@ import androidx.media3.test.utils.StubExoPlayer; PositionInfo newPosition = new PositionInfo( windowUid, - /* windowIndex= */ 0, + /* mediaItemIndex= */ 0, mediaItem, periodUid, /* periodIndex= */ 0, @@ -128,7 +128,7 @@ import androidx.media3.test.utils.StubExoPlayer; PositionInfo oldPosition = new PositionInfo( windowUid, - /* windowIndex= */ 0, + /* mediaItemIndex= */ 0, mediaItem, periodUid, /* periodIndex= */ 0, @@ -146,7 +146,7 @@ import androidx.media3.test.utils.StubExoPlayer; PositionInfo newPosition = new PositionInfo( windowUid, - /* windowIndex= */ 0, + /* mediaItemIndex= */ 0, mediaItem, periodUid, /* periodIndex= */ 0, diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java index 2e97fca3d5..f174b61aa4 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java @@ -15,7 +15,10 @@ */ package androidx.media3.exoplayer.rtsp; +import static androidx.media3.common.util.Assertions.checkArgument; + import androidx.annotation.Nullable; +import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.UnstableApi; @@ -37,19 +40,35 @@ import java.util.Map; public final class RtpPayloadFormat { private static final String RTP_MEDIA_AC3 = "AC3"; + private static final String RTP_MEDIA_AMR = "AMR"; + private static final String RTP_MEDIA_AMR_WB = "AMR-WB"; + private static final String RTP_MEDIA_MPEG4_GENERIC = "MPEG4-GENERIC"; + private static final String RTP_MEDIA_MPEG4_VIDEO = "MP4V-ES"; private static final String RTP_MEDIA_H264 = "H264"; private static final String RTP_MEDIA_H265 = "H265"; - private static final String RTP_MEDIA_MPEG4_GENERIC = "MPEG4-GENERIC"; + private static final String RTP_MEDIA_PCM_L8 = "L8"; + private static final String RTP_MEDIA_PCM_L16 = "L16"; private static final String RTP_MEDIA_OPUS = "OPUS"; + private static final String RTP_MEDIA_PCMA = "PCMA"; + private static final String RTP_MEDIA_PCMU = "PCMU"; + private static final String RTP_MEDIA_VP8 = "VP8"; /** Returns whether the format of a {@link MediaDescription} is supported. */ public static boolean isFormatSupported(MediaDescription mediaDescription) { switch (Ascii.toUpperCase(mediaDescription.rtpMapAttribute.mediaEncoding)) { case RTP_MEDIA_AC3: + case RTP_MEDIA_AMR: + case RTP_MEDIA_AMR_WB: case RTP_MEDIA_H264: case RTP_MEDIA_H265: + case RTP_MEDIA_MPEG4_VIDEO: case RTP_MEDIA_MPEG4_GENERIC: case RTP_MEDIA_OPUS: + case RTP_MEDIA_PCM_L8: + case RTP_MEDIA_PCM_L16: + case RTP_MEDIA_PCMA: + case RTP_MEDIA_PCMU: + case RTP_MEDIA_VP8: return true; default: return false; @@ -67,19 +86,43 @@ public final class RtpPayloadFormat { switch (Ascii.toUpperCase(mediaType)) { case RTP_MEDIA_AC3: return MimeTypes.AUDIO_AC3; - case RTP_MEDIA_H264: - return MimeTypes.VIDEO_H264; - case RTP_MEDIA_H265: - return MimeTypes.VIDEO_H265; + case RTP_MEDIA_AMR: + return MimeTypes.AUDIO_AMR_NB; + case RTP_MEDIA_AMR_WB: + return MimeTypes.AUDIO_AMR_WB; case RTP_MEDIA_MPEG4_GENERIC: return MimeTypes.AUDIO_AAC; case RTP_MEDIA_OPUS: return MimeTypes.AUDIO_OPUS; + case RTP_MEDIA_PCM_L8: + case RTP_MEDIA_PCM_L16: + return MimeTypes.AUDIO_RAW; + case RTP_MEDIA_PCMA: + return MimeTypes.AUDIO_ALAW; + case RTP_MEDIA_PCMU: + return MimeTypes.AUDIO_MLAW; + case RTP_MEDIA_H264: + return MimeTypes.VIDEO_H264; + case RTP_MEDIA_H265: + return MimeTypes.VIDEO_H265; + case RTP_MEDIA_MPEG4_VIDEO: + return MimeTypes.VIDEO_MP4V; + case RTP_MEDIA_VP8: + return MimeTypes.VIDEO_VP8; default: throw new IllegalArgumentException(mediaType); } } + /** Returns the PCM encoding type for {@code mediaEncoding}. */ + public static @C.PcmEncoding int getRawPcmEncodingType(String mediaEncoding) { + checkArgument( + mediaEncoding.equals(RTP_MEDIA_PCM_L8) || mediaEncoding.equals(RTP_MEDIA_PCM_L16)); + return mediaEncoding.equals(RtpPayloadFormat.RTP_MEDIA_PCM_L8) + ? C.ENCODING_PCM_8BIT + : C.ENCODING_PCM_16BIT_BIG_ENDIAN; + } + /** The payload type associated with this format. */ public final int rtpPayloadType; /** The clock rate in Hertz, associated with the format. */ diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java index 28084cdef6..2d23137700 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java @@ -28,8 +28,6 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.StreamKey; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelection; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.decoder.DecoderInputBuffer; @@ -44,7 +42,9 @@ import androidx.media3.exoplayer.source.SampleQueue.UpstreamFormatChangedListene import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.SampleStream.ReadDataResult; import androidx.media3.exoplayer.source.SampleStream.ReadFlags; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; +import androidx.media3.exoplayer.trackselection.TrackSelection; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.Loader; import androidx.media3.exoplayer.upstream.Loader.Loadable; diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java index 87c8e79893..c04ad07125 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java @@ -25,6 +25,7 @@ import static androidx.media3.extractor.NalUnitUtil.NAL_START_CODE; import android.net.Uri; import android.util.Base64; +import android.util.Pair; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; @@ -44,17 +45,61 @@ import com.google.common.collect.ImmutableMap; // Format specific parameter names. private static final String PARAMETER_PROFILE_LEVEL_ID = "profile-level-id"; private static final String PARAMETER_SPROP_PARAMS = "sprop-parameter-sets"; + + private static final String PARAMETER_AMR_OCTET_ALIGN = "octet-align"; + private static final String PARAMETER_AMR_INTERLEAVING = "interleaving"; private static final String PARAMETER_H265_SPROP_SPS = "sprop-sps"; private static final String PARAMETER_H265_SPROP_PPS = "sprop-pps"; private static final String PARAMETER_H265_SPROP_VPS = "sprop-vps"; private static final String PARAMETER_H265_SPROP_MAX_DON_DIFF = "sprop-max-don-diff"; + private static final String PARAMETER_MP4V_CONFIG = "config"; /** Prefix for the RFC6381 codecs string for AAC formats. */ private static final String AAC_CODECS_PREFIX = "mp4a.40."; /** Prefix for the RFC6381 codecs string for AVC formats. */ private static final String H264_CODECS_PREFIX = "avc1."; + /** Prefix for the RFC6416 codecs string for MPEG4V-ES formats. */ + private static final String MPEG4_CODECS_PREFIX = "mp4v."; private static final String GENERIC_CONTROL_ATTR = "*"; + /** + * Default height for MP4V. + * + *

    RFC6416 does not mandate codec specific data (like width and height) in the fmtp attribute. + * These values are taken from Android's software MP4V decoder. + */ + private static final int DEFAULT_MP4V_WIDTH = 352; + + /** + * Default height for MP4V. + * + *

    RFC6416 does not mandate codec specific data (like width and height) in the fmtp attribute. + * These values are taken from Android's software MP4V decoder. + */ + private static final int DEFAULT_MP4V_HEIGHT = 288; + + /** + * Default width for VP8. + * + *

    RFC7741 never uses codec specific data (like width and height) in the fmtp attribute. These + * values are taken from Android's + * software VP8 decoder. + */ + private static final int DEFAULT_VP8_WIDTH = 320; + /** + * Default height for VP8. + * + *

    RFC7741 never uses codec specific data (like width and height) in the fmtp attribute. These + * values are taken from Android's + * software VP8 decoder. + */ + private static final int DEFAULT_VP8_HEIGHT = 240; /** RFC7587 Section 6.1 Sampling rate for OPUS is fixed at 48KHz. */ private static final int OPUS_SAMPLING_RATE = 48000; @@ -105,8 +150,9 @@ import com.google.common.collect.ImmutableMap; } int rtpPayloadType = mediaDescription.rtpMapAttribute.payloadType; + String mediaEncoding = mediaDescription.rtpMapAttribute.mediaEncoding; - String mimeType = getMimeTypeFromRtpMediaType(mediaDescription.rtpMapAttribute.mediaEncoding); + String mimeType = getMimeTypeFromRtpMediaType(mediaEncoding); formatBuilder.setSampleMimeType(mimeType); int clockRate = mediaDescription.rtpMapAttribute.clockRate; @@ -124,12 +170,28 @@ import com.google.common.collect.ImmutableMap; checkArgument(!fmtpParameters.isEmpty()); processAacFmtpAttribute(formatBuilder, fmtpParameters, channelCount, clockRate); break; + case MimeTypes.AUDIO_AMR_NB: + case MimeTypes.AUDIO_AMR_WB: + checkArgument(channelCount == 1, "Multi channel AMR is not currently supported."); + checkArgument( + !fmtpParameters.isEmpty(), + "fmtp parameters must include " + PARAMETER_AMR_OCTET_ALIGN + "."); + checkArgument( + fmtpParameters.containsKey(PARAMETER_AMR_OCTET_ALIGN), + "Only octet aligned mode is currently supported."); + checkArgument( + !fmtpParameters.containsKey(PARAMETER_AMR_INTERLEAVING), + "Interleaving mode is not currently supported."); + break; case MimeTypes.AUDIO_OPUS: checkArgument(channelCount != C.INDEX_UNSET); // RFC7587 Section 6.1. // the RTP timestamp is incremented with a 48000 Hz clock rate // for all modes of Opus and all sampling rates. checkArgument(clockRate == OPUS_SAMPLING_RATE, "Invalid sampling rate"); + case MimeTypes.VIDEO_MP4V: + checkArgument(!fmtpParameters.isEmpty()); + processMPEG4FmtpAttribute(formatBuilder, fmtpParameters); break; case MimeTypes.VIDEO_H264: checkArgument(!fmtpParameters.isEmpty()); @@ -139,8 +201,18 @@ import com.google.common.collect.ImmutableMap; checkArgument(!fmtpParameters.isEmpty()); processH265FmtpAttribute(formatBuilder, fmtpParameters); break; + case MimeTypes.VIDEO_VP8: + // VP8 never uses fmtp width and height attributes (RFC7741 Section 6.2), setting default + // width and height. + formatBuilder.setWidth(DEFAULT_VP8_WIDTH).setHeight(DEFAULT_VP8_HEIGHT); + break; + case MimeTypes.AUDIO_RAW: + formatBuilder.setPcmEncoding(RtpPayloadFormat.getRawPcmEncodingType(mediaEncoding)); + break; case MimeTypes.AUDIO_AC3: - // AC3 does not require a FMTP attribute. Fall through. + case MimeTypes.AUDIO_ALAW: + case MimeTypes.AUDIO_MLAW: + // Does not require a fmtp attribute. Fall through. default: // Do nothing. } @@ -179,6 +251,23 @@ import com.google.common.collect.ImmutableMap; AacUtil.buildAacLcAudioSpecificConfig(sampleRate, channelCount))); } + private static void processMPEG4FmtpAttribute( + Format.Builder formatBuilder, ImmutableMap fmtpAttributes) { + @Nullable String configInput = fmtpAttributes.get(PARAMETER_MP4V_CONFIG); + if (configInput != null) { + byte[] configBuffer = Util.getBytesFromHexString(configInput); + formatBuilder.setInitializationData(ImmutableList.of(configBuffer)); + Pair resolution = + CodecSpecificDataUtil.getVideoResolutionFromMpeg4VideoConfig(configBuffer); + formatBuilder.setWidth(resolution.first).setHeight(resolution.second); + } else { + // set the default width and height + formatBuilder.setWidth(DEFAULT_MP4V_WIDTH).setHeight(DEFAULT_MP4V_HEIGHT); + } + @Nullable String profileLevel = fmtpAttributes.get(PARAMETER_PROFILE_LEVEL_ID); + formatBuilder.setCodecs(MPEG4_CODECS_PREFIX + (profileLevel == null ? "1" : profileLevel)); + } + /** Returns H264/H265 initialization data from the RTP parameter set. */ private static byte[] getInitializationDataFromParameterSet(String parameterSet) { byte[] decodedParameterNalData = Base64.decode(parameterSet, Base64.DEFAULT); diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspSessionTiming.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspSessionTiming.java index 60ff5a9101..a3493e9525 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspSessionTiming.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspSessionTiming.java @@ -38,8 +38,9 @@ import java.util.regex.Pattern; new RtspSessionTiming(/* startTimeMs= */ 0, /* stopTimeMs= */ C.TIME_UNSET); // We only support npt=xxx-[xxx], but not npt=-xxx. See RFC2326 Section 3.6. + // Supports both npt= and npt: identifier. private static final Pattern NPT_RANGE_PATTERN = - Pattern.compile("npt=([.\\d]+|now)\\s?-\\s?([.\\d]+)?"); + Pattern.compile("npt[:=]([.\\d]+|now)\\s?-\\s?([.\\d]+)?"); private static final String START_TIMING_NTP_FORMAT = "npt=%.3f-"; private static final long LIVE_START_TIME = 0; diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java index cc78aaf1ec..dce19d96f3 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java @@ -36,12 +36,23 @@ import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; return new RtpAc3Reader(payloadFormat); case MimeTypes.AUDIO_AAC: return new RtpAacReader(payloadFormat); + case MimeTypes.AUDIO_AMR_NB: + case MimeTypes.AUDIO_AMR_WB: + return new RtpAmrReader(payloadFormat); case MimeTypes.AUDIO_OPUS: return new RtpOpusReader(payloadFormat); + case MimeTypes.AUDIO_RAW: + case MimeTypes.AUDIO_ALAW: + case MimeTypes.AUDIO_MLAW: + return new RtpPcmReader(payloadFormat); case MimeTypes.VIDEO_H264: return new RtpH264Reader(payloadFormat); case MimeTypes.VIDEO_H265: return new RtpH265Reader(payloadFormat); + case MimeTypes.VIDEO_MP4V: + return new RtpMpeg4Reader(payloadFormat); + case MimeTypes.VIDEO_VP8: + return new RtpVp8Reader(payloadFormat); default: // No supported reader, returning null. } diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpAmrReader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpAmrReader.java new file mode 100644 index 0000000000..5f66337276 --- /dev/null +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpAmrReader.java @@ -0,0 +1,196 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.rtsp.reader; + +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import androidx.media3.common.C; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.TrackOutput; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses an AMR byte stream carried on RTP packets and extracts individual samples. Interleaving + * mode is not supported. Refer to RFC4867 for more details. + */ +/* package */ final class RtpAmrReader implements RtpPayloadReader { + private static final String TAG = "RtpAmrReader"; + /** + * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR-NB + * (narrow band). AMR-NB supports eight narrow band speech encoding modes with bit rates between + * 4.75 and 12.2 kbps defined in RFC4867 Section 3.1. Refer to table 1a in 3GPP TS 26.101 for the + * mapping definition. + */ + private static final int[] AMR_NB_FRAME_TYPE_INDEX_TO_FRAME_SIZE = { + 13, // 4.75kbps + 14, // 5.15kbps + 16, // 5.90kbps + 18, // 6.70kbps PDC-EFR + 20, // 7.40kbps TDMA-EFR + 21, // 7.95kbps + 27, // 10.2kbps + 32, // 12.2kbps GSM-EFR + 6, // AMR SID + 7, // GSM-EFR SID + 6, // TDMA-EFR SID + 6, // PDC-EFR SID + 1, // Future use + 1, // Future use + 1, // Future use + 1 // No data + }; + + /** + * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR-WB + * (wide band). AMR-WB supports nine wide band speech encoding modes with bit rates between 6.6 to + * 23.85 kbps defined in RFC4867 Section 3.2. Refer to table 1a in 3GPP TS 26.201. for the mapping + * definition. + */ + private static final int[] AMR_WB_FRAME_TYPE_INDEX_TO_FRAME_SIZE = { + 18, // 6.60kbps + 24, // 8.85kbps + 33, // 12.65kbps + 37, // 14.25kbps + 41, // 15.85kbps + 47, // 18.25kbps + 51, // 19.85kbps + 59, // 23.05kbps + 61, // 23.85kbps + 6, // AMR-WB SID + 1, // Future use + 1, // Future use + 1, // Future use + 1, // Future use + 1, // speech lost + 1 // No data + }; + + private final RtpPayloadFormat payloadFormat; + private final boolean isWideBand; + private final int sampleRate; + + private @MonotonicNonNull TrackOutput trackOutput; + private long firstReceivedTimestamp; + private long startTimeOffsetUs; + private int previousSequenceNumber; + + public RtpAmrReader(RtpPayloadFormat payloadFormat) { + this.payloadFormat = payloadFormat; + this.isWideBand = + MimeTypes.AUDIO_AMR_WB.equals(checkNotNull(payloadFormat.format.sampleMimeType)); + this.sampleRate = payloadFormat.clockRate; + this.firstReceivedTimestamp = C.TIME_UNSET; + this.previousSequenceNumber = C.INDEX_UNSET; + // Start time offset must be 0 before the first seek. + this.startTimeOffsetUs = 0; + } + + // RtpPayloadReader implementation. + + @Override + public void createTracks(ExtractorOutput extractorOutput, int trackId) { + trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_AUDIO); + trackOutput.format(payloadFormat.format); + } + + @Override + public void onReceivingFirstPacket(long timestamp, int sequenceNumber) { + this.firstReceivedTimestamp = timestamp; + } + + @Override + public void consume( + ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) { + checkStateNotNull(trackOutput); + // Check that this packet is in the sequence of the previous packet. + if (previousSequenceNumber != C.INDEX_UNSET) { + int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); + if (sequenceNumber != expectedSequenceNumber) { + Log.w( + TAG, + Util.formatInvariant( + "Received RTP packet with unexpected sequence number. Expected: %d; received: %d.", + expectedSequenceNumber, sequenceNumber)); + } + } + // + // AMR as RTP payload (RFC4867 Section 4.2). + // + // +----------------+-------------------+---------------- + // | payload header | table of contents | speech data ... + // +----------------+-------------------+---------------- + // + // Payload header (RFC4867 Section 4.4.1). + // + // The header won't contain ILL and ILP, as interleaving is not currently supported. + // +-+-+-+-+-+-+-+- - - - - - - - + // | CMR |R|R|R|R| ILL | ILP | + // +-+-+-+-+-+-+-+- - - - - - - - + // + // Skip CMR and reserved bits. + data.skipBytes(1); + // Loop over sampleSize to send multiple frames along with appropriate timestamp when compound + // payload support is added. + int frameType = (data.peekUnsignedByte() >> 3) & 0x0f; + int frameSize = getFrameSize(frameType, isWideBand); + int sampleSize = data.bytesLeft(); + checkArgument(sampleSize == frameSize, "compound payload not supported currently"); + trackOutput.sampleData(data, sampleSize); + long sampleTimeUs = + toSampleTimeUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp, sampleRate); + trackOutput.sampleMetadata( + sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, /* cryptoData= */ null); + + previousSequenceNumber = sequenceNumber; + } + + @Override + public void seek(long nextRtpTimestamp, long timeUs) { + firstReceivedTimestamp = nextRtpTimestamp; + startTimeOffsetUs = timeUs; + } + + // Internal methods. + + public static int getFrameSize(int frameType, boolean isWideBand) { + checkArgument( + // Valid frame types are defined in RFC4867 Section 4.3.1. + (frameType >= 0 && frameType <= 8) || frameType == 15, + "Illegal AMR " + (isWideBand ? "WB" : "NB") + " frame type " + frameType); + + return isWideBand + ? AMR_WB_FRAME_TYPE_INDEX_TO_FRAME_SIZE[frameType] + : AMR_NB_FRAME_TYPE_INDEX_TO_FRAME_SIZE[frameType]; + } + + /** Returns the correct sample time from RTP timestamp, accounting for the AMR sampling rate. */ + private static long toSampleTimeUs( + long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp, int sampleRate) { + return startTimeOffsetUs + + Util.scaleLargeTimestamp( + rtpTimestamp - firstReceivedRtpTimestamp, + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ sampleRate); + } +} diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpMpeg4Reader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpMpeg4Reader.java new file mode 100644 index 0000000000..4aa2da52be --- /dev/null +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpMpeg4Reader.java @@ -0,0 +1,150 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.rtsp.reader; + +import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static androidx.media3.common.util.Util.castNonNull; + +import androidx.media3.common.C; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.TrackOutput; +import com.google.common.primitives.Bytes; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses an MPEG4 byte stream carried on RTP packets, and extracts MPEG4 Access Units. Refer to + * RFC6416 for more details. + */ +@UnstableApi +/* package */ final class RtpMpeg4Reader implements RtpPayloadReader { + private static final String TAG = "RtpMpeg4Reader"; + + private static final long MEDIA_CLOCK_FREQUENCY = 90_000; + + /** VOP (Video Object Plane) unit type. */ + private static final int I_VOP = 0; + + private final RtpPayloadFormat payloadFormat; + private @MonotonicNonNull TrackOutput trackOutput; + private @C.BufferFlags int bufferFlags; + + /** + * First received RTP timestamp. All RTP timestamps are dimension-less, the time base is defined + * by {@link #MEDIA_CLOCK_FREQUENCY}. + */ + private long firstReceivedTimestamp; + + private int previousSequenceNumber; + private long startTimeOffsetUs; + private int sampleLength; + + /** Creates an instance. */ + public RtpMpeg4Reader(RtpPayloadFormat payloadFormat) { + this.payloadFormat = payloadFormat; + firstReceivedTimestamp = C.TIME_UNSET; + previousSequenceNumber = C.INDEX_UNSET; + sampleLength = 0; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, int trackId) { + trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_VIDEO); + castNonNull(trackOutput).format(payloadFormat.format); + } + + @Override + public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {} + + @Override + public void consume( + ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) { + checkStateNotNull(trackOutput); + // Check that this packet is in the sequence of the previous packet. + if (previousSequenceNumber != C.INDEX_UNSET) { + int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); + if (sequenceNumber != expectedSequenceNumber) { + Log.w( + TAG, + Util.formatInvariant( + "Received RTP packet with unexpected sequence number. Expected: %d; received: %d." + + " Dropping packet.", + expectedSequenceNumber, sequenceNumber)); + } + } + + // Parse VOP Type and get the buffer flags + int limit = data.bytesLeft(); + trackOutput.sampleData(data, limit); + if (sampleLength == 0) { + bufferFlags = getBufferFlagsFromVop(data); + } + sampleLength += limit; + + // RTP marker indicates the last packet carrying a VOP. + if (rtpMarker) { + if (firstReceivedTimestamp == C.TIME_UNSET) { + firstReceivedTimestamp = timestamp; + } + + long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp); + trackOutput.sampleMetadata(timeUs, bufferFlags, sampleLength, 0, null); + sampleLength = 0; + } + previousSequenceNumber = sequenceNumber; + } + + @Override + public void seek(long nextRtpTimestamp, long timeUs) { + firstReceivedTimestamp = nextRtpTimestamp; + startTimeOffsetUs = timeUs; + sampleLength = 0; + } + + // Internal methods. + + /** + * Returns VOP (Video Object Plane) Coding type. + * + *

    Sets {@link #bufferFlags} according to the VOP Coding type. + */ + private static @C.BufferFlags int getBufferFlagsFromVop(ParsableByteArray data) { + // search for VOP_START_CODE (00 00 01 B6) + byte[] inputData = data.getData(); + byte[] startCode = new byte[] {0x0, 0x0, 0x1, (byte) 0xB6}; + int vopStartCodePos = Bytes.indexOf(inputData, startCode); + if (vopStartCodePos != -1) { + data.setPosition(vopStartCodePos + 4); + int vopType = data.peekUnsignedByte() >> 6; + return vopType == I_VOP ? C.BUFFER_FLAG_KEY_FRAME : 0; + } + return 0; + } + + private static long toSampleUs( + long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) { + return startTimeOffsetUs + + Util.scaleLargeTimestamp( + (rtpTimestamp - firstReceivedRtpTimestamp), + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ MEDIA_CLOCK_FREQUENCY); + } +} diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpPcmReader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpPcmReader.java new file mode 100644 index 0000000000..3c82f3e5be --- /dev/null +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpPcmReader.java @@ -0,0 +1,106 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.rtsp.reader; + +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.util.Log; +import androidx.media3.common.C; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.TrackOutput; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses byte stream carried on RTP packets, and extracts PCM frames. Refer to RFC3551 for more + * details. + */ +@UnstableApi +/* package */ public final class RtpPcmReader implements RtpPayloadReader { + + private static final String TAG = "RtpPcmReader"; + private final RtpPayloadFormat payloadFormat; + + private @MonotonicNonNull TrackOutput trackOutput; + private long firstReceivedTimestamp; + private long startTimeOffsetUs; + private int previousSequenceNumber; + + public RtpPcmReader(RtpPayloadFormat payloadFormat) { + this.payloadFormat = payloadFormat; + firstReceivedTimestamp = C.TIME_UNSET; + // Start time offset must be 0 before the first seek. + startTimeOffsetUs = 0; + previousSequenceNumber = C.INDEX_UNSET; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, int trackId) { + trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_AUDIO); + trackOutput.format(payloadFormat.format); + } + + @Override + public void onReceivingFirstPacket(long timestamp, int sequenceNumber) { + firstReceivedTimestamp = timestamp; + } + + @Override + public void consume( + ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) { + checkNotNull(trackOutput); + if (previousSequenceNumber != C.INDEX_UNSET) { + int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); + if (sequenceNumber != expectedSequenceNumber) { + Log.w( + TAG, + Util.formatInvariant( + "Received RTP packet with unexpected sequence number. Expected: %d; received: %d.", + expectedSequenceNumber, sequenceNumber)); + } + } + + long sampleTimeUs = + toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp, payloadFormat.clockRate); + int size = data.bytesLeft(); + trackOutput.sampleData(data, size); + trackOutput.sampleMetadata( + sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* cryptoData= */ null); + + previousSequenceNumber = sequenceNumber; + } + + @Override + public void seek(long nextRtpTimestamp, long timeUs) { + // TODO(b/198620566) Rename firstReceivedTimestamp to timestampBase for all RtpPayloadReaders. + firstReceivedTimestamp = nextRtpTimestamp; + startTimeOffsetUs = timeUs; + } + + /** Returns the correct sample time from RTP timestamp, accounting for the given clock rate. */ + private static long toSampleUs( + long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp, int clockRate) { + return startTimeOffsetUs + + Util.scaleLargeTimestamp( + rtpTimestamp - firstReceivedRtpTimestamp, + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ clockRate); + } +} diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java new file mode 100644 index 0000000000..72b739edd2 --- /dev/null +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java @@ -0,0 +1,206 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.rtsp.reader; + +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import androidx.media3.common.C; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.TrackOutput; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses a VP8 byte stream carried on RTP packets, and extracts VP8 individual video frames as + * defined in RFC7741. + */ +/* package */ final class RtpVp8Reader implements RtpPayloadReader { + private static final String TAG = "RtpVP8Reader"; + + /** VP9 uses a 90 KHz media clock (RFC7741 Section 4.1). */ + private static final long MEDIA_CLOCK_FREQUENCY = 90_000; + + private final RtpPayloadFormat payloadFormat; + + private @MonotonicNonNull TrackOutput trackOutput; + + /** + * First received RTP timestamp. All RTP timestamps are dimension-less, the time base is defined + * by {@link #MEDIA_CLOCK_FREQUENCY}. + */ + private long firstReceivedTimestamp; + + private int previousSequenceNumber; + /** The combined size of a sample that is fragmented into multiple RTP packets. */ + private int fragmentedSampleSizeBytes; + + private long startTimeOffsetUs; + /** + * Whether the first packet of one VP8 frame is received. A VP8 frame can be split into two RTP + * packets. + */ + private boolean gotFirstPacketOfVp8Frame; + + private boolean isKeyFrame; + private boolean isOutputFormatSet; + + /** Creates an instance. */ + public RtpVp8Reader(RtpPayloadFormat payloadFormat) { + this.payloadFormat = payloadFormat; + firstReceivedTimestamp = C.TIME_UNSET; + previousSequenceNumber = C.INDEX_UNSET; + fragmentedSampleSizeBytes = C.LENGTH_UNSET; + // The start time offset must be 0 until the first seek. + startTimeOffsetUs = 0; + gotFirstPacketOfVp8Frame = false; + isKeyFrame = false; + isOutputFormatSet = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, int trackId) { + trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_VIDEO); + trackOutput.format(payloadFormat.format); + } + + @Override + public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {} + + @Override + public void consume( + ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) { + checkStateNotNull(trackOutput); + + boolean isValidVP8Descriptor = validateVp8Descriptor(data, sequenceNumber); + if (isValidVP8Descriptor) { + // VP8 Payload Header is defined in RFC7741 Section 4.3. + if (fragmentedSampleSizeBytes == C.LENGTH_UNSET && gotFirstPacketOfVp8Frame) { + isKeyFrame = (data.peekUnsignedByte() & 0x01) == 0; + } + if (!isOutputFormatSet) { + // Parsing frame data to get width and height, RFC6386 Section 19.1. + int currPosition = data.getPosition(); + // Skips the frame_tag and start_code. + data.setPosition(currPosition + 6); + // RFC6386 Section 19.1 specifically uses little endian. + int width = data.readLittleEndianUnsignedShort() & 0x3fff; + int height = data.readLittleEndianUnsignedShort() & 0x3fff; + data.setPosition(currPosition); + + if (width != payloadFormat.format.width || height != payloadFormat.format.height) { + trackOutput.format( + payloadFormat.format.buildUpon().setWidth(width).setHeight(height).build()); + } + isOutputFormatSet = true; + } + + int fragmentSize = data.bytesLeft(); + trackOutput.sampleData(data, fragmentSize); + fragmentedSampleSizeBytes += fragmentSize; + + if (rtpMarker) { + if (firstReceivedTimestamp == C.TIME_UNSET) { + firstReceivedTimestamp = timestamp; + } + long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp); + trackOutput.sampleMetadata( + timeUs, + isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0, + fragmentedSampleSizeBytes, + /* offset= */ 0, + /* cryptoData= */ null); + fragmentedSampleSizeBytes = C.LENGTH_UNSET; + gotFirstPacketOfVp8Frame = false; + } + previousSequenceNumber = sequenceNumber; + } + } + + @Override + public void seek(long nextRtpTimestamp, long timeUs) { + firstReceivedTimestamp = nextRtpTimestamp; + fragmentedSampleSizeBytes = C.LENGTH_UNSET; + startTimeOffsetUs = timeUs; + } + + /** + * Returns {@code true} and sets the {@link ParsableByteArray#getPosition() payload.position} to + * the end of the descriptor, if a valid VP8 descriptor is present. + */ + private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSequenceNumber) { + // VP8 Payload Descriptor is defined in RFC7741 Section 4.2. + int header = payload.readUnsignedByte(); + if (!gotFirstPacketOfVp8Frame) { + // TODO(b/198620566) Consider using ParsableBitArray. + // For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2. + if ((header & 0x10) != 0x1 || (header & 0x07) != 0) { + Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping."); + return false; + } + gotFirstPacketOfVp8Frame = true; + } else { + // Check that this packet is in the sequence of the previous packet. + int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); + if (packetSequenceNumber != expectedSequenceNumber) { + Log.w( + TAG, + Util.formatInvariant( + "Received RTP packet with unexpected sequence number. Expected: %d; received: %d." + + " Dropping packet.", + expectedSequenceNumber, packetSequenceNumber)); + return false; + } + } + + // Check if optional X header is present. + if ((header & 0x80) != 0) { + int xHeader = payload.readUnsignedByte(); + + // Check if optional I header is present. + if ((xHeader & 0x80) != 0) { + int iHeader = payload.readUnsignedByte(); + // Check if I header's M bit is present. + if ((iHeader & 0x80) != 0) { + payload.skipBytes(1); + } + } + + // Check if optional L header is present. + if ((xHeader & 0x40) != 0) { + payload.skipBytes(1); + } + + // Check if optional T or K header(s) is present. + if ((xHeader & 0x20) != 0 || (xHeader & 0x10) != 0) { + payload.skipBytes(1); + } + } + return true; + } + + private static long toSampleUs( + long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) { + return startTimeOffsetUs + + Util.scaleLargeTimestamp( + (rtpTimestamp - firstReceivedRtpTimestamp), + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ MEDIA_CLOCK_FREQUENCY); + } +} diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspSessionTimingTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspSessionTimingTest.java index ad7c05fee0..e9ac8cf287 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspSessionTimingTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspSessionTimingTest.java @@ -54,6 +54,13 @@ public class RtspSessionTimingTest { assertThat(sessionTiming.isLive()).isFalse(); } + @Test + public void parseTiming_withRangeTimingAndColonSeparator() throws Exception { + RtspSessionTiming sessionTiming = RtspSessionTiming.parseTiming("npt:0.000-32.054"); + assertThat(sessionTiming.getDurationMs()).isEqualTo(32054); + assertThat(sessionTiming.isLive()).isFalse(); + } + @Test public void parseTiming_withInvalidRangeTiming_throwsIllegalArgumentException() { assertThrows( diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpPcmReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpPcmReaderTest.java new file mode 100644 index 0000000000..bba419e3d6 --- /dev/null +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpPcmReaderTest.java @@ -0,0 +1,183 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.rtsp.reader; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.ParsableByteArray; +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.TestUtil; +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 RtpPcmReader}. */ +@RunWith(AndroidJUnit4.class) +public final class RtpPcmReaderTest { + + // A typical RTP payload type for audio. + private static final int RTP_PAYLOAD_TYPE = 97; + private static final byte[] FRAME_1_PAYLOAD = TestUtil.buildTestData(/* length= */ 4); + private static final byte[] FRAME_2_PAYLOAD = TestUtil.buildTestData(/* length= */ 4); + + private static final RtpPacket PACKET_1 = + createRtpPacket(/* timestamp= */ 2599168056L, /* sequenceNumber= */ 40289, FRAME_1_PAYLOAD); + private static final RtpPacket PACKET_2 = + createRtpPacket(/* timestamp= */ 2599169592L, /* sequenceNumber= */ 40290, FRAME_2_PAYLOAD); + + private ParsableByteArray packetData; + private FakeExtractorOutput extractorOutput; + private RtpPcmReader pcmReader; + + @Before + public void setUp() { + packetData = new ParsableByteArray(); + extractorOutput = new FakeExtractorOutput(); + } + + @Test + public void consume_twoDualChannelWav8bitPackets() { + pcmReader = + new RtpPcmReader( + new RtpPayloadFormat( + new Format.Builder() + .setChannelCount(2) + .setSampleMimeType(MimeTypes.AUDIO_WAV) + .setPcmEncoding(C.ENCODING_PCM_8BIT) + .setSampleRate(48_000) + .build(), + /* rtpPayloadType= */ RTP_PAYLOAD_TYPE, + /* clockRate= */ 48_000, + /* fmtpParameters= */ ImmutableMap.of())); + + pcmReader.createTracks(extractorOutput, /* trackId= */ 0); + pcmReader.onReceivingFirstPacket(PACKET_1.timestamp, PACKET_1.sequenceNumber); + consume(PACKET_1); + consume(PACKET_2); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_1_PAYLOAD); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_PAYLOAD); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(32000); + } + + @Test + public void consume_twoSingleChannelWav16bitPackets() { + pcmReader = + new RtpPcmReader( + new RtpPayloadFormat( + new Format.Builder() + .setChannelCount(1) + .setSampleMimeType(MimeTypes.AUDIO_WAV) + .setPcmEncoding(C.ENCODING_PCM_16BIT_BIG_ENDIAN) + .setSampleRate(60_000) + .build(), + /* rtpPayloadType= */ RTP_PAYLOAD_TYPE, + /* clockRate= */ 60_000, + /* fmtpParameters= */ ImmutableMap.of())); + + pcmReader.createTracks(extractorOutput, /* trackId= */ 0); + pcmReader.onReceivingFirstPacket(PACKET_1.timestamp, PACKET_1.sequenceNumber); + consume(PACKET_1); + consume(PACKET_2); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_1_PAYLOAD); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_PAYLOAD); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(25600); + } + + @Test + public void consume_twoDualChannelAlawPackets() { + pcmReader = + new RtpPcmReader( + new RtpPayloadFormat( + new Format.Builder() + .setChannelCount(2) + .setSampleMimeType(MimeTypes.AUDIO_ALAW) + .setSampleRate(16_000) + .build(), + /* rtpPayloadType= */ RTP_PAYLOAD_TYPE, + /* clockRate= */ 16_000, + /* fmtpParameters= */ ImmutableMap.of())); + + pcmReader.createTracks(extractorOutput, /* trackId= */ 0); + pcmReader.onReceivingFirstPacket(PACKET_1.timestamp, PACKET_1.sequenceNumber); + consume(PACKET_1); + consume(PACKET_2); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_1_PAYLOAD); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_PAYLOAD); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(96000); + } + + @Test + public void consume_twoDualChannelMlawPackets() { + pcmReader = + new RtpPcmReader( + new RtpPayloadFormat( + new Format.Builder() + .setChannelCount(2) + .setSampleMimeType(MimeTypes.AUDIO_MLAW) + .setSampleRate(24_000) + .build(), + /* rtpPayloadType= */ RTP_PAYLOAD_TYPE, + /* clockRate= */ 24_000, + /* fmtpParameters= */ ImmutableMap.of())); + + pcmReader.createTracks(extractorOutput, /* trackId= */ 0); + pcmReader.onReceivingFirstPacket(PACKET_1.timestamp, PACKET_1.sequenceNumber); + consume(PACKET_1); + consume(PACKET_2); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_1_PAYLOAD); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_PAYLOAD); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(64000); + } + + private static RtpPacket createRtpPacket(long timestamp, int sequenceNumber, byte[] payloadData) { + return new RtpPacket.Builder() + .setTimestamp(timestamp) + .setSequenceNumber(sequenceNumber) + // RFC3551 Section 4.1. + .setMarker(false) + .setPayloadData(payloadData) + .build(); + } + + private void consume(RtpPacket frame) { + packetData.reset(frame.payloadData); + pcmReader.consume(packetData, frame.timestamp, frame.sequenceNumber, frame.marker); + } +} diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java index a634fbd54f..0ff9a96246 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java @@ -20,7 +20,6 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.StreamKey; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.datasource.TransferListener; import androidx.media3.exoplayer.SeekParameters; import androidx.media3.exoplayer.drm.DrmSessionEventListener; @@ -31,6 +30,7 @@ import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.SequenceableLoader; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.source.chunk.ChunkSampleStream; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.Allocator; diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloaderTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloaderTest.java index fcaa18605d..dc73b98a2f 100644 --- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloaderTest.java +++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloaderTest.java @@ -20,7 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.exoplayer.offline.DefaultDownloaderFactory; @@ -42,7 +42,7 @@ public final class SsDownloaderTest { CacheDataSource.Factory cacheDataSourceFactory = new CacheDataSource.Factory() .setCache(Mockito.mock(Cache.class)) - .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + .setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY); DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run); @@ -52,7 +52,7 @@ public final class SsDownloaderTest { .setMimeType(MimeTypes.APPLICATION_SS) .setStreamKeys( Collections.singletonList( - new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0))) + new StreamKey(/* groupIndex= */ 0, /* streamIndex= */ 0))) .build()); assertThat(downloader).isInstanceOf(SsDownloader.class); } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java index b795b79fed..c61d7eaba5 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java @@ -31,6 +31,21 @@ public final class NalUnitUtil { private static final String TAG = "NalUnitUtil"; + /** Coded slice of a non-IDR picture. */ + public static final int NAL_UNIT_TYPE_NON_IDR = 1; + /** Coded slice data partition A. */ + public static final int NAL_UNIT_TYPE_PARTITION_A = 2; + /** Coded slice of an IDR picture. */ + public static final int NAL_UNIT_TYPE_IDR = 5; + /** Supplemental enhancement information. */ + public static final int NAL_UNIT_TYPE_SEI = 6; + /** Sequence parameter set. */ + public static final int NAL_UNIT_TYPE_SPS = 7; + /** Picture parameter set. */ + public static final int NAL_UNIT_TYPE_PPS = 8; + /** Access unit delimiter. */ + public static final int NAL_UNIT_TYPE_AUD = 9; + /** Holds data parsed from a H.264 sequence parameter set NAL unit. */ public static final class SpsData { @@ -38,6 +53,7 @@ public final class NalUnitUtil { public final int constraintsFlagsAndReservedZero2Bits; public final int levelIdc; public final int seqParameterSetId; + public final int maxNumRefFrames; public final int width; public final int height; public final float pixelWidthHeightRatio; @@ -53,6 +69,7 @@ public final class NalUnitUtil { int constraintsFlagsAndReservedZero2Bits, int levelIdc, int seqParameterSetId, + int maxNumRefFrames, int width, int height, float pixelWidthHeightRatio, @@ -66,6 +83,7 @@ public final class NalUnitUtil { this.constraintsFlagsAndReservedZero2Bits = constraintsFlagsAndReservedZero2Bits; this.levelIdc = levelIdc; this.seqParameterSetId = seqParameterSetId; + this.maxNumRefFrames = maxNumRefFrames; this.width = width; this.height = height; this.pixelWidthHeightRatio = pixelWidthHeightRatio; @@ -372,7 +390,7 @@ public final class NalUnitUtil { data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i] } } - data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames + int maxNumRefFrames = data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames data.skipBit(); // gaps_in_frame_num_value_allowed_flag int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1; @@ -432,6 +450,7 @@ public final class NalUnitUtil { constraintsFlagsAndReservedZero2Bits, levelIdc, seqParameterSetId, + maxNumRefFrames, frameWidth, frameHeight, pixelWidthHeightRatio, diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java index bd34ba5970..56acf9513b 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java @@ -1116,6 +1116,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Nullable String codecs = null; @Nullable byte[] projectionData = null; @C.StereoMode int stereoMode = Format.NO_VALUE; + @Nullable EsdsData esdsData = null; // HDR related metadata. @C.ColorSpace int colorSpace = Format.NO_VALUE; @@ -1168,6 +1169,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } else if (childAtomType == Atom.TYPE_av1C) { ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null); mimeType = MimeTypes.VIDEO_AV1; + + int childAtomBodySize = childAtomSize - Atom.HEADER_SIZE; + byte[] onlyInitializationDataChunk = new byte[childAtomBodySize]; + parent.readBytes(onlyInitializationDataChunk, /* offset= */ 0, childAtomBodySize); + initializationData = ImmutableList.of(onlyInitializationDataChunk); } else if (childAtomType == Atom.TYPE_clli) { if (hdrStaticInfo == null) { hdrStaticInfo = allocateHdrStaticInfo(); @@ -1210,10 +1216,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; mimeType = MimeTypes.VIDEO_H263; } else if (childAtomType == Atom.TYPE_esds) { ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null); - Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationDataBytes = - parseEsdsFromParent(parent, childStartPosition); - mimeType = mimeTypeAndInitializationDataBytes.first; - @Nullable byte[] initializationDataBytes = mimeTypeAndInitializationDataBytes.second; + esdsData = parseEsdsFromParent(parent, childStartPosition); + mimeType = esdsData.mimeType; + @Nullable byte[] initializationDataBytes = esdsData.initializationData; if (initializationDataBytes != null) { initializationData = ImmutableList.of(initializationDataBytes); } @@ -1301,6 +1306,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; colorTransfer, hdrStaticInfo != null ? hdrStaticInfo.array() : null)); } + + if (esdsData != null) { + formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate); + } + out.format = formatBuilder.build(); } @@ -1391,6 +1401,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int sampleRateMlp = 0; @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; @Nullable String codecs = null; + @Nullable EsdsData esdsData = null; if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { channelCount = parent.readUnsignedShort(); @@ -1507,10 +1518,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ? childPosition : findBoxPosition(parent, Atom.TYPE_esds, childPosition, childAtomSize); if (esdsAtomPosition != C.POSITION_UNSET) { - Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationData = - parseEsdsFromParent(parent, esdsAtomPosition); - mimeType = mimeTypeAndInitializationData.first; - @Nullable byte[] initializationDataBytes = mimeTypeAndInitializationData.second; + esdsData = parseEsdsFromParent(parent, esdsAtomPosition); + mimeType = esdsData.mimeType; + @Nullable byte[] initializationDataBytes = esdsData.initializationData; if (initializationDataBytes != null) { if (MimeTypes.AUDIO_AAC.equals(mimeType)) { // Update sampleRate and channelCount from the AudioSpecificConfig initialization @@ -1591,7 +1601,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } if (out.format == null && mimeType != null) { - out.format = + Format.Builder formatBuilder = new Format.Builder() .setId(trackId) .setSampleMimeType(mimeType) @@ -1601,8 +1611,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; .setPcmEncoding(pcmEncoding) .setInitializationData(initializationData) .setDrmInitData(drmInitData) - .setLanguage(language) - .build(); + .setLanguage(language); + + if (esdsData != null) { + formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate); + } + + out.format = formatBuilder.build(); } } @@ -1637,8 +1652,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } /** Returns codec-specific initialization data contained in an esds box. */ - private static Pair<@NullableType String, byte @NullableType []> parseEsdsFromParent( - ParsableByteArray parent, int position) { + private static EsdsData parseEsdsFromParent(ParsableByteArray parent, int position) { parent.setPosition(position + Atom.HEADER_SIZE + 4); // Start of the ES_Descriptor (defined in ISO/IEC 14496-1) parent.skipBytes(1); // ES_Descriptor tag @@ -1666,17 +1680,29 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (MimeTypes.AUDIO_MPEG.equals(mimeType) || MimeTypes.AUDIO_DTS.equals(mimeType) || MimeTypes.AUDIO_DTS_HD.equals(mimeType)) { - return Pair.create(mimeType, null); + return new EsdsData( + mimeType, + /* initializationData= */ null, + /* bitrate= */ Format.NO_VALUE, + /* peakBitrate= */ Format.NO_VALUE); } - parent.skipBytes(12); + parent.skipBytes(4); + int peakBitrate = parent.readUnsignedIntToInt(); + int bitrate = parent.readUnsignedIntToInt(); // Start of the DecoderSpecificInfo. parent.skipBytes(1); // DecoderSpecificInfo tag int initializationDataSize = parseExpandableClassSize(parent); byte[] initializationData = new byte[initializationDataSize]; parent.readBytes(initializationData, 0, initializationDataSize); - return Pair.create(mimeType, initializationData); + + // Skipping zero values as unknown. + return new EsdsData( + mimeType, + /* initializationData= */ initializationData, + /* bitrate= */ bitrate > 0 ? bitrate : Format.NO_VALUE, + /* peakBitrate= */ peakBitrate > 0 ? peakBitrate : Format.NO_VALUE); } /** @@ -1918,6 +1944,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } + /** Data parsed from an esds box. */ + private static final class EsdsData { + private final @NullableType String mimeType; + private final byte @NullableType [] initializationData; + private final int bitrate; + private final int peakBitrate; + + public EsdsData( + @NullableType String mimeType, + byte @NullableType [] initializationData, + int bitrate, + int peakBitrate) { + this.mimeType = mimeType; + this.initializationData = initializationData; + this.bitrate = bitrate; + this.peakBitrate = peakBitrate; + } + } + /** A box containing sample sizes (e.g. stsz, stz2). */ private interface SampleSizeBox { 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 361871f51d..800c601059 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 @@ -668,14 +668,23 @@ public class FragmentedMp4Extractor implements Extractor { emsgTrackOutput.sampleData(encodedEventMessage, sampleSize); } - // Output the sample metadata. This is made a little complicated because emsg-v0 atoms - // have presentation time *delta* while v1 atoms have absolute presentation time. + // Output the sample metadata. if (sampleTimeUs == C.TIME_UNSET) { - // We need the first sample timestamp in the segment before we can output the metadata. + // We're processing a v0 emsg atom, which contains a presentation time delta, and cannot yet + // calculate its absolute sample timestamp. Defer outputting the metadata until we can. pendingMetadataSampleInfos.addLast( - new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); + new MetadataSampleInfo( + presentationTimeDeltaUs, /* sampleTimeIsRelative= */ true, sampleSize)); + pendingMetadataSampleBytes += sampleSize; + } else if (!pendingMetadataSampleInfos.isEmpty()) { + // We also need to defer outputting metadata if pendingMetadataSampleInfos is non-empty, else + // we will output metadata for samples in the wrong order. See: + // https://github.com/google/ExoPlayer/issues/9996. + pendingMetadataSampleInfos.addLast( + new MetadataSampleInfo(sampleTimeUs, /* sampleTimeIsRelative= */ false, sampleSize)); pendingMetadataSampleBytes += sampleSize; } else { + // We can output the sample metadata immediately. if (timestampAdjuster != null) { sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); } @@ -1461,19 +1470,30 @@ public class FragmentedMp4Extractor implements Extractor { return true; } + /** + * Called immediately after outputting a non-metadata sample, to output any pending metadata + * samples. + * + * @param sampleTimeUs The timestamp of the non-metadata sample that was just output. + */ private void outputPendingMetadataSamples(long sampleTimeUs) { while (!pendingMetadataSampleInfos.isEmpty()) { - MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); - pendingMetadataSampleBytes -= sampleInfo.size; - long metadataTimeUs = sampleTimeUs + sampleInfo.presentationTimeDeltaUs; + MetadataSampleInfo metadataSampleInfo = pendingMetadataSampleInfos.removeFirst(); + pendingMetadataSampleBytes -= metadataSampleInfo.size; + long metadataSampleTimeUs = metadataSampleInfo.sampleTimeUs; + if (metadataSampleInfo.sampleTimeIsRelative) { + // The metadata sample timestamp is relative to the timestamp of the non-metadata sample + // that was just output. Make it absolute. + metadataSampleTimeUs += sampleTimeUs; + } if (timestampAdjuster != null) { - metadataTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataTimeUs); + metadataSampleTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataSampleTimeUs); } for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { emsgTrackOutput.sampleMetadata( - metadataTimeUs, + metadataSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, - sampleInfo.size, + metadataSampleInfo.size, pendingMetadataSampleBytes, null); } @@ -1579,11 +1599,13 @@ public class FragmentedMp4Extractor implements Extractor { /** Holds data corresponding to a metadata sample. */ private static final class MetadataSampleInfo { - public final long presentationTimeDeltaUs; + public final long sampleTimeUs; + public final boolean sampleTimeIsRelative; public final int size; - public MetadataSampleInfo(long presentationTimeDeltaUs, int size) { - this.presentationTimeDeltaUs = presentationTimeDeltaUs; + public MetadataSampleInfo(long sampleTimeUs, boolean sampleTimeIsRelative, int size) { + this.sampleTimeUs = sampleTimeUs; + this.sampleTimeIsRelative = sampleTimeIsRelative; this.size = size; } } 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 e839cf1e9e..4d9c5ec4cc 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 @@ -15,7 +15,6 @@ */ package androidx.media3.extractor.mp4; -import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.extractor.mp4.AtomParsers.parseTraks; import static androidx.media3.extractor.mp4.Sniffer.BRAND_HEIC; @@ -165,8 +164,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { private int sampleCurrentNalBytesRemaining; // Extractor outputs. - private @MonotonicNonNull ExtractorOutput extractorOutput; - private Mp4Track @MonotonicNonNull [] tracks; + private ExtractorOutput extractorOutput; + private Mp4Track[] tracks; private long @MonotonicNonNull [][] accumulatedSampleSizes; private int firstVideoTrackIndex; @@ -197,6 +196,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { nalLength = new ParsableByteArray(4); scratch = new ParsableByteArray(); sampleTrackIndex = C.INDEX_UNSET; + extractorOutput = ExtractorOutput.PLACEHOLDER; + tracks = new Mp4Track[0]; } @Override @@ -227,7 +228,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { sefReader.reset(); slowMotionMetadataEntries.clear(); } - } else if (tracks != null) { + } else { for (Mp4Track track : tracks) { updateSampleIndex(track, timeUs); if (track.trueHdSampleRechunker != null) { @@ -280,7 +281,23 @@ public final class Mp4Extractor implements Extractor, SeekMap { @Override public SeekPoints getSeekPoints(long timeUs) { - if (checkNotNull(tracks).length == 0) { + return getSeekPoints(timeUs, /* trackId= */ C.INDEX_UNSET); + } + + // Non-inherited public methods. + + /** + * Equivalent to {@link SeekMap#getSeekPoints(long)}, except it adds the {@code trackId} + * parameter. + * + * @param timeUs A seek time in microseconds. + * @param trackId The id of the track on which to seek for {@link SeekPoints}. May be {@link + * C#INDEX_UNSET} if the extractor is expected to define the strategy for generating {@link + * SeekPoints}. + * @return The corresponding seek points. + */ + public SeekPoints getSeekPoints(long timeUs, int trackId) { + if (tracks.length == 0) { return new SeekPoints(SeekPoint.START); } @@ -289,9 +306,11 @@ public final class Mp4Extractor implements Extractor, SeekMap { long secondTimeUs = C.TIME_UNSET; long secondOffset = C.POSITION_UNSET; + // Note that the id matches the index in tracks. + int mainTrackIndex = trackId != C.INDEX_UNSET ? trackId : firstVideoTrackIndex; // If we have a video track, use it to establish one or two seek points. - if (firstVideoTrackIndex != C.INDEX_UNSET) { - TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable; + if (mainTrackIndex != C.INDEX_UNSET) { + TrackSampleTable sampleTable = tracks[mainTrackIndex].sampleTable; int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs); if (sampleIndex == C.INDEX_UNSET) { return new SeekPoints(SeekPoint.START); @@ -311,13 +330,15 @@ public final class Mp4Extractor implements Extractor, SeekMap { firstOffset = Long.MAX_VALUE; } - // Take into account other tracks. - for (int i = 0; i < tracks.length; i++) { - if (i != firstVideoTrackIndex) { - TrackSampleTable sampleTable = tracks[i].sampleTable; - firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset); - if (secondTimeUs != C.TIME_UNSET) { - secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset); + if (trackId == C.INDEX_UNSET) { + // Take into account other tracks, but only if the caller has not specified a trackId. + for (int i = 0; i < tracks.length; i++) { + if (i != firstVideoTrackIndex) { + TrackSampleTable sampleTable = tracks[i].sampleTable; + firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset); + if (secondTimeUs != C.TIME_UNSET) { + secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset); + } } } } @@ -502,7 +523,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { isQuickTime, /* modifyTrackFunction= */ track -> track); - ExtractorOutput extractorOutput = checkNotNull(this.extractorOutput); int trackCount = trackSampleTables.size(); for (int i = 0; i < trackCount; i++) { TrackSampleTable trackSampleTable = trackSampleTables.get(i); @@ -582,7 +602,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { return RESULT_END_OF_INPUT; } } - Mp4Track track = castNonNull(tracks)[sampleTrackIndex]; + Mp4Track track = tracks[sampleTrackIndex]; TrackOutput trackOutput = track.trackOutput; int sampleIndex = track.sampleIndex; long position = track.sampleTable.offsets[sampleIndex]; @@ -699,7 +719,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { long minAccumulatedBytes = Long.MAX_VALUE; boolean minAccumulatedBytesRequiresReload = true; int minAccumulatedBytesTrackIndex = C.INDEX_UNSET; - for (int trackIndex = 0; trackIndex < castNonNull(tracks).length; trackIndex++) { + for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) { Mp4Track track = tracks[trackIndex]; int sampleIndex = track.sampleIndex; if (sampleIndex == track.sampleTable.sampleCount) { @@ -744,7 +764,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { private void processEndOfStreamReadingAtomHeader() { if (fileType == FILE_TYPE_HEIC && (flags & FLAG_READ_MOTION_PHOTO_METADATA) != 0) { // Add image track and prepare media. - ExtractorOutput extractorOutput = checkNotNull(this.extractorOutput); TrackOutput trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_IMAGE); @Nullable Metadata metadata = motionPhotoMetadata == null ? null : new Metadata(motionPhotoMetadata); diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H264Reader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H264Reader.java index 33f4e99547..cd4d3416d1 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H264Reader.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H264Reader.java @@ -44,10 +44,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @UnstableApi public final class H264Reader implements ElementaryStreamReader { - private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information - private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set - private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set - private final SeiReader seiReader; private final boolean allowNonIdrKeyframes; private final boolean detectAccessUnits; @@ -85,9 +81,9 @@ public final class H264Reader implements ElementaryStreamReader { this.allowNonIdrKeyframes = allowNonIdrKeyframes; this.detectAccessUnits = detectAccessUnits; prefixFlags = new boolean[3]; - sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); - pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); - sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); + sps = new NalUnitTargetBuffer(NalUnitUtil.NAL_UNIT_TYPE_SPS, 128); + pps = new NalUnitTargetBuffer(NalUnitUtil.NAL_UNIT_TYPE_PPS, 128); + sei = new NalUnitTargetBuffer(NalUnitUtil.NAL_UNIT_TYPE_SEI, 128); pesTimeUs = C.TIME_UNSET; seiWrapper = new ParsableByteArray(); } @@ -266,11 +262,6 @@ public final class H264Reader implements ElementaryStreamReader { private static final int DEFAULT_BUFFER_SIZE = 128; - private static final int NAL_UNIT_TYPE_NON_IDR = 1; // Coded slice of a non-IDR picture - private static final int NAL_UNIT_TYPE_PARTITION_A = 2; // Coded slice data partition A - private static final int NAL_UNIT_TYPE_IDR = 5; // Coded slice of an IDR picture - private static final int NAL_UNIT_TYPE_AUD = 9; // Access unit delimiter - private final TrackOutput output; private final boolean allowNonIdrKeyframes; private final boolean detectAccessUnits; @@ -331,11 +322,11 @@ public final class H264Reader implements ElementaryStreamReader { nalUnitType = type; nalUnitTimeUs = pesTimeUs; nalUnitStartPosition = position; - if ((allowNonIdrKeyframes && nalUnitType == NAL_UNIT_TYPE_NON_IDR) + if ((allowNonIdrKeyframes && nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_NON_IDR) || (detectAccessUnits - && (nalUnitType == NAL_UNIT_TYPE_IDR - || nalUnitType == NAL_UNIT_TYPE_NON_IDR - || nalUnitType == NAL_UNIT_TYPE_PARTITION_A))) { + && (nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_IDR + || nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_NON_IDR + || nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_PARTITION_A))) { // Store the previous header and prepare to populate the new one. SliceHeaderData newSliceHeader = previousSliceHeader; previousSliceHeader = sliceHeader; @@ -425,7 +416,7 @@ public final class H264Reader implements ElementaryStreamReader { bottomFieldFlagPresent = true; } } - boolean idrPicFlag = nalUnitType == NAL_UNIT_TYPE_IDR; + boolean idrPicFlag = nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_IDR; int idrPicId = 0; if (idrPicFlag) { if (!bitArray.canReadExpGolombCodedNum()) { @@ -480,7 +471,7 @@ public final class H264Reader implements ElementaryStreamReader { public boolean endNalUnit( long position, int offset, boolean hasOutputFormat, boolean randomAccessIndicator) { - if (nalUnitType == NAL_UNIT_TYPE_AUD + if (nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_AUD || (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) { // If the NAL unit ending is the start of a new sample, output the previous one. if (hasOutputFormat && readingSample) { @@ -495,8 +486,8 @@ public final class H264Reader implements ElementaryStreamReader { boolean treatIFrameAsKeyframe = allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator; sampleIsKeyframe |= - nalUnitType == NAL_UNIT_TYPE_IDR - || (treatIFrameAsKeyframe && nalUnitType == NAL_UNIT_TYPE_NON_IDR); + nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_IDR + || (treatIFrameAsKeyframe && nalUnitType == NalUnitUtil.NAL_UNIT_TYPE_NON_IDR); return sampleIsKeyframe; } diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java index ebd2ccc45f..01d7fe15f9 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java @@ -126,6 +126,7 @@ public final class NalUnitUtilTest { public void parseSpsNalUnit() { NalUnitUtil.SpsData data = NalUnitUtil.parseSpsNalUnit(SPS_TEST_DATA, SPS_TEST_DATA_OFFSET, SPS_TEST_DATA.length); + assertThat(data.maxNumRefFrames).isEqualTo(4); assertThat(data.width).isEqualTo(640); assertThat(data.height).isEqualTo(360); assertThat(data.deltaPicOrderAlwaysZeroFlag).isFalse(); diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorTest.java index b42d0f670e..8b09e446bb 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorTest.java @@ -97,6 +97,12 @@ public final class Mp4ExtractorTest { Mp4Extractor::new, "media/mp4/sample_mpegh_mhm1.mp4", simulationConfig); } + @Test + public void mp4SampleWithAv1Track() throws Exception { + ExtractorAsserts.assertBehavior( + Mp4Extractor::new, "media/mp4/sample_av1.mp4", simulationConfig); + } + @Test public void mp4SampleWithColorInfo() throws Exception { ExtractorAsserts.assertBehavior( diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java b/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java index fff79f9eeb..a2085ccabd 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java @@ -64,8 +64,8 @@ import androidx.media3.common.util.Util; Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); intent.setComponent(new ComponentName(service, service.getClass())); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); - if (Util.SDK_INT >= 26 && command != COMMAND_PAUSE && command != COMMAND_STOP) { - return Api26.createPendingIntent(service, /* requestCode= */ keyCode, intent); + if (Util.SDK_INT >= 26 && command == COMMAND_PLAY) { + return Api26.createForegroundServicePendingIntent(service, keyCode, intent); } else { return PendingIntent.getService( service, @@ -80,16 +80,12 @@ import androidx.media3.common.util.Util; intent.setComponent(new ComponentName(service, service.getClass())); intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM, action); intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM_EXTRAS, extras); - if (Util.SDK_INT >= 26) { - return Api26.createPendingIntent( - service, /* requestCode= */ KeyEvent.KEYCODE_UNKNOWN, intent); - } else { - return PendingIntent.getService( - service, - /* requestCode= */ KeyEvent.KEYCODE_UNKNOWN, - intent, - Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0); - } + // Custom actions always start the service in the background. + return PendingIntent.getService( + service, + /* requestCode= */ KeyEvent.KEYCODE_UNKNOWN, + intent, + Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0); } /** Returns whether {@code intent} was part of a {@link #createMediaAction media action}. */ @@ -141,7 +137,8 @@ import androidx.media3.common.util.Util; private static final class Api26 { private Api26() {} - public static PendingIntent createPendingIntent(Service service, int keyCode, Intent intent) { + public static PendingIntent createForegroundServicePendingIntent( + Service service, int keyCode, Intent intent) { return PendingIntent.getForegroundService( service, /* requestCode= */ keyCode, intent, PendingIntent.FLAG_IMMUTABLE); } 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 1b308d425a..492a6a116e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -31,6 +31,7 @@ import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.graphics.drawable.IconCompat; import androidx.media3.common.MediaMetadata; +import androidx.media3.common.Player; import androidx.media3.common.util.Consumer; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; @@ -120,20 +121,21 @@ public final class DefaultMediaNotificationProvider implements MediaNotification IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_previous), context.getString(R.string.media3_controls_seek_to_previous_description), MediaNotification.ActionFactory.COMMAND_SKIP_TO_PREVIOUS)); - if (mediaController.getPlayWhenReady()) { - // Pause action. - builder.addAction( - actionFactory.createMediaAction( - IconCompat.createWithResource(context, R.drawable.media3_notification_pause), - context.getString(R.string.media3_controls_pause_description), - MediaNotification.ActionFactory.COMMAND_PAUSE)); - } else { + if (mediaController.getPlaybackState() == Player.STATE_ENDED + || !mediaController.getPlayWhenReady()) { // Play action. builder.addAction( actionFactory.createMediaAction( IconCompat.createWithResource(context, R.drawable.media3_notification_play), context.getString(R.string.media3_controls_play_description), MediaNotification.ActionFactory.COMMAND_PLAY)); + } else { + // Pause action. + builder.addAction( + actionFactory.createMediaAction( + IconCompat.createWithResource(context, R.drawable.media3_notification_pause), + context.getString(R.string.media3_controls_pause_description), + MediaNotification.ActionFactory.COMMAND_PAUSE)); } // Skip to next action. builder.addAction( diff --git a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java index 17c921eff4..4e469926c3 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java @@ -218,11 +218,24 @@ public final class LibraryResult implements Bundleable { * @param errorCode The error code. */ public static LibraryResult ofError(@Code int errorCode) { + return ofError(errorCode, /* params= */ null); + } + + /** + * Creates an instance with an unsuccessful {@link Code result code} and {@link LibraryParams} to + * describe the error. + * + *

    {@code errorCode} must not be {@link #RESULT_SUCCESS}. + * + * @param errorCode The error code. + * @param params The optional parameters to describe the error. + */ + public static LibraryResult ofError(@Code int errorCode, @Nullable LibraryParams params) { checkArgument(errorCode != RESULT_SUCCESS); return new LibraryResult<>( - errorCode, + /* resultCode= */ errorCode, SystemClock.elapsedRealtime(), - /* params= */ null, + /* params= */ params, /* value= */ null, VALUE_TYPE_ERROR); } @@ -266,6 +279,8 @@ public final class LibraryResult implements Bundleable { private static final int FIELD_VALUE = 3; private static final int FIELD_VALUE_TYPE = 4; + // Casting V to ImmutableList is safe if valueType == VALUE_TYPE_ITEM_LIST. + @SuppressWarnings("unchecked") @UnstableApi @Override public Bundle toBundle() { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java index 217757283d..9ad21b8784 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java @@ -113,6 +113,33 @@ public final class MediaConstants { */ public static final String MEDIA_URI_QUERY_URI = "uri"; + /** + * The extras key for the localized error resolution string. + * + *

    See {@link + * androidx.media.utils.MediaConstants#PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL}. + */ + public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT = + "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL"; + /** + * The extras key for the error resolution intent. + * + *

    See {@link + * androidx.media.utils.MediaConstants#PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT}. + */ + public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT = + "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT"; + + /** The legacy status code for successful execution. */ + public static final int STATUS_CODE_SUCCESS_COMPAT = -1; + + /** + * The legacy error code for expired authentication. + * + *

    See {@code PlaybackStateCompat#ERROR_CODE_AUTHENTICATION_EXPIRED}. + */ + public static final int ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT = 3; + /* package */ static final String SESSION_COMMAND_ON_EXTRAS_CHANGED = "androidx.media3.session.SESSION_COMMAND_ON_EXTRAS_CHANGED"; /* package */ static final String SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED = 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 53d7b912c1..ece6c99c6d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -49,8 +49,6 @@ import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Rating; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; @@ -1681,20 +1679,6 @@ public class MediaController implements Player { return isConnected() ? impl.getMediaMetadata() : MediaMetadata.EMPTY; } - /** Returns {@link TrackGroupArray#EMPTY}. */ - @UnstableApi - @Override - public TrackGroupArray getCurrentTrackGroups() { - return TrackGroupArray.EMPTY; - } - - /** Returns an empty {@link TrackSelectionArray}. */ - @UnstableApi - @Override - public TrackSelectionArray getCurrentTrackSelections() { - return new TrackSelectionArray(); - } - @Override public TracksInfo getCurrentTracksInfo() { return TracksInfo.EMPTY; // TODO(b/178486745) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java index 8e06166bdd..b56af6b58f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java @@ -198,7 +198,7 @@ import java.util.concurrent.atomic.AtomicReference; try { int page = options.getInt(EXTRA_PAGE); int pageSize = options.getInt(EXTRA_PAGE_SIZE); - if (page > 0 && pageSize > 0) { + if (page >= 0 && pageSize > 0) { // Requesting the list of children through pagination. @Nullable LibraryParams params = @@ -223,7 +223,7 @@ import java.util.concurrent.atomic.AtomicReference; parentId, /* page= */ 0, /* pageSize= */ Integer.MAX_VALUE, - /* extras= */ null); + /* params= */ null); sendLibraryResultWithMediaItemsWhenReady(result, future); }); } 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 f33347f961..5faa6bdc12 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -18,7 +18,10 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED; import static androidx.media3.session.LibraryResult.RESULT_SUCCESS; +import static androidx.media3.session.MediaConstants.ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT; +import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT; import android.app.PendingIntent; import android.content.Context; @@ -118,19 +121,17 @@ import java.util.concurrent.Future; public ListenableFuture> onGetLibraryRootOnHandler( ControllerInfo browser, @Nullable LibraryParams params) { - // onGetLibraryRoot is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. - return checkNotNull( - callback.onGetLibraryRoot(instance, browser, params), - "onGetLibraryRoot must return non-null future"); - } - - public ListenableFuture> onGetItemOnHandler( - ControllerInfo browser, String mediaId) { - // onGetItem is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. - return checkNotNull( - callback.onGetItem(instance, browser, mediaId), "onGetItem must return non-null future"); + ListenableFuture> future = + callback.onGetLibraryRoot(instance, browser, params); + future.addListener( + () -> { + @Nullable LibraryResult result = tryGetFutureResult(future); + if (result != null) { + maybeUpdateLegacyErrorState(result); + } + }, + MoreExecutors.directExecutor()); + return future; } public ListenableFuture>> onGetChildrenOnHandler( @@ -139,16 +140,13 @@ import java.util.concurrent.Future; int page, int pageSize, @Nullable LibraryParams params) { - // onGetChildren is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. ListenableFuture>> future = - checkNotNull( - callback.onGetChildren(instance, browser, parentId, page, pageSize, params), - "onGetChildren must return non-null future"); + callback.onGetChildren(instance, browser, parentId, page, pageSize, params); future.addListener( () -> { @Nullable LibraryResult> result = tryGetFutureResult(future); if (result != null) { + maybeUpdateLegacyErrorState(result); verifyResultItems(result, pageSize); } }, @@ -156,6 +154,21 @@ import java.util.concurrent.Future; return future; } + public ListenableFuture> onGetItemOnHandler( + ControllerInfo browser, String mediaId) { + ListenableFuture> future = + callback.onGetItem(instance, browser, mediaId); + future.addListener( + () -> { + @Nullable LibraryResult result = tryGetFutureResult(future); + if (result != null) { + maybeUpdateLegacyErrorState(result); + } + }, + MoreExecutors.directExecutor()); + return future; + } + public ListenableFuture> onSubscribeOnHandler( ControllerInfo browser, String parentId, @Nullable LibraryParams params) { ControllerCb controller = checkStateNotNull(browser.getControllerCb()); @@ -193,12 +206,8 @@ import java.util.concurrent.Future; public ListenableFuture> onUnsubscribeOnHandler( ControllerInfo browser, String parentId) { - // onUnsubscribe is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. ListenableFuture> future = - checkNotNull( - callback.onUnsubscribe(instance, browser, parentId), - "onUnsubscribe must return non-null future"); + callback.onUnsubscribe(instance, browser, parentId); future.addListener( () -> removeSubscription(checkStateNotNull(browser.getControllerCb()), parentId), @@ -209,11 +218,17 @@ import java.util.concurrent.Future; public ListenableFuture> onSearchOnHandler( ControllerInfo browser, String query, @Nullable LibraryParams params) { - // onSearch is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. - return checkNotNull( - callback.onSearch(instance, browser, query, params), - "onSearch must return non-null future"); + ListenableFuture> future = + callback.onSearch(instance, browser, query, params); + future.addListener( + () -> { + @Nullable LibraryResult result = tryGetFutureResult(future); + if (result != null) { + maybeUpdateLegacyErrorState(result); + } + }, + MoreExecutors.directExecutor()); + return future; } public ListenableFuture>> onGetSearchResultOnHandler( @@ -222,17 +237,13 @@ import java.util.concurrent.Future; int page, int pageSize, @Nullable LibraryParams params) { - // onGetSearchResult is defined to return a non-null result but it's implemented by - // applications, so we explicitly null-check the result to fail early if an app accidentally - // returns null. ListenableFuture>> future = - checkNotNull( - callback.onGetSearchResult(instance, browser, query, page, pageSize, params), - "onGetSearchResult must return non-null future"); + callback.onGetSearchResult(instance, browser, query, page, pageSize, params); future.addListener( () -> { @Nullable LibraryResult> result = tryGetFutureResult(future); if (result != null) { + maybeUpdateLegacyErrorState(result); verifyResultItems(result, pageSize); } }, @@ -277,6 +288,27 @@ import java.util.concurrent.Future; return true; } + private void maybeUpdateLegacyErrorState(LibraryResult result) { + PlayerWrapper playerWrapper = getPlayerWrapper(); + if (result.resultCode == RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED + && result.params != null + && result.params.extras.containsKey(EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT)) { + // Mapping this error to the legacy error state provides backwards compatibility for the + // Automotive OS sign-in. + MediaSessionCompat mediaSessionCompat = getSessionCompat(); + if (playerWrapper.getLegacyStatusCode() != RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED) { + playerWrapper.setLegacyErrorStatus( + ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT, + getContext().getString(R.string.authentication_required), + result.params.extras); + mediaSessionCompat.setPlaybackState(playerWrapper.createPlaybackStateCompat()); + } + } else if (playerWrapper.getLegacyStatusCode() != RESULT_SUCCESS) { + playerWrapper.clearLegacyErrorStatus(); + getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat()); + } + } + @Nullable private static T tryGetFutureResult(Future future) { checkState(future.isDone()); 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 c89ebe092d..397d0e2bcb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java @@ -15,6 +15,8 @@ */ package androidx.media3.session; +import static androidx.media3.common.util.Assertions.checkStateNotNull; + import android.app.Notification; import android.content.Intent; import android.os.Bundle; @@ -25,6 +27,7 @@ import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import androidx.media3.common.Player; import androidx.media3.common.util.Util; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.util.HashMap; import java.util.List; @@ -50,6 +53,7 @@ import java.util.concurrent.TimeoutException; private final Map> controllerMap; private int totalNotificationCount; + @Nullable private MediaNotification mediaNotification; public MediaNotificationManager( MediaSessionService mediaSessionService, @@ -122,13 +126,13 @@ import java.util.concurrent.TimeoutException; MediaController mediaController; try { - mediaController = controllerFuture.get(0, TimeUnit.MILLISECONDS); - } catch (ExecutionException | InterruptedException | TimeoutException e) { + mediaController = checkStateNotNull(Futures.getDone(controllerFuture)); + } catch (ExecutionException e) { // We should never reach this point. throw new IllegalStateException(e); } - int notificationSequence = ++this.totalNotificationCount; + int notificationSequence = ++totalNotificationCount; MediaNotification.Provider.Callback callback = notification -> mainExecutor.execute( @@ -141,45 +145,68 @@ import java.util.concurrent.TimeoutException; private void onNotificationUpdated( int notificationSequence, MediaSession session, MediaNotification mediaNotification) { - if (notificationSequence == this.totalNotificationCount) { + if (notificationSequence == totalNotificationCount) { updateNotification(session, mediaNotification); } } private void updateNotification(MediaSession session, MediaNotification mediaNotification) { - int id = mediaNotification.notificationId; - Notification notification = mediaNotification.notification; - if (Util.SDK_INT >= 21) { // Call Notification.MediaStyle#setMediaSession() indirectly. android.media.session.MediaSession.Token fwkToken = (android.media.session.MediaSession.Token) session.getSessionCompat().getSessionToken().getToken(); - notification.extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, fwkToken); + mediaNotification.notification.extras.putParcelable( + Notification.EXTRA_MEDIA_SESSION, fwkToken); } + this.mediaNotification = mediaNotification; Player player = session.getPlayer(); - if (player.getPlayWhenReady()) { + if (player.getPlayWhenReady() && canStartPlayback(player)) { ContextCompat.startForegroundService(mediaSessionService, startSelfIntent); - mediaSessionService.startForeground(id, notification); + mediaSessionService.startForeground( + mediaNotification.notificationId, mediaNotification.notification); } else { - stopForegroundServiceIfNeeded(); - notificationManagerCompat.notify(id, notification); + maybeStopForegroundService(/* removeNotifications= */ false); + notificationManagerCompat.notify( + mediaNotification.notificationId, mediaNotification.notification); } } - private void stopForegroundServiceIfNeeded() { + /** + * Stops the service from the foreground, if no player is actively playing content. + * + * @param removeNotifications Whether to remove notifications, if the service is stopped from the + * foreground. + */ + private void maybeStopForegroundService(boolean removeNotifications) { List sessions = mediaSessionService.getSessions(); for (int i = 0; i < sessions.size(); i++) { Player player = sessions.get(i).getPlayer(); - if (player.getPlayWhenReady()) { + if (player.getPlayWhenReady() && canStartPlayback(player)) { return; } } - // Calling stopForeground(true) is a workaround for pre-L devices which prevents - // the media notification from being undismissable. - boolean shouldRemoveNotification = Util.SDK_INT < 21; - mediaSessionService.stopForeground(shouldRemoveNotification); + // To hide the notification on all API levels, we need to call both Service.stopForeground(true) + // and notificationManagerCompat.cancelAll(). For pre-L devices, we must also call + // Service.stopForeground(true) anyway as a workaround that prevents the media notification from + // being undismissable. + mediaSessionService.stopForeground(removeNotifications || Util.SDK_INT < 21); + if (removeNotifications && mediaNotification != null) { + notificationManagerCompat.cancel(mediaNotification.notificationId); + // Update the notification count so that if a pending notification callback arrives (e.g., a + // bitmap is loaded), we don't show the notification. + totalNotificationCount++; + mediaNotification = null; + } + } + + /** + * Returns whether {@code player} can start playback and therefore we should present a + * notification for this player. + */ + private static boolean canStartPlayback(Player player) { + return player.getPlaybackState() != Player.STATE_IDLE && !player.getCurrentTimeline().isEmpty(); } private final class MediaControllerListener implements MediaController.Listener, Player.Listener { @@ -190,13 +217,24 @@ import java.util.concurrent.TimeoutException; } public void onConnected() { - updateNotification(session); + if (canStartPlayback(session.getPlayer())) { + updateNotification(session); + } } @Override public void onEvents(Player player, Player.Events events) { + if (!canStartPlayback(player)) { + maybeStopForegroundService(/* removeNotifications= */ true); + return; + } + + // Limit the events on which we may update the notification to ensure we don't update the + // notification too frequently, otherwise the system may suppress notifications. if (events.containsAny( - Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_MEDIA_METADATA_CHANGED)) { + Player.EVENT_PLAYBACK_STATE_CHANGED, + Player.EVENT_PLAY_WHEN_READY_CHANGED, + Player.EVENT_MEDIA_METADATA_CHANGED)) { updateNotification(session); } } @@ -204,7 +242,7 @@ import java.util.concurrent.TimeoutException; @Override public void onDisconnected(MediaController controller) { mediaSessionService.removeSession(session); - stopForegroundServiceIfNeeded(); + maybeStopForegroundService(/* removeNotifications= */ true); } } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 2aef615164..4121904955 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -28,6 +28,8 @@ import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH; import static androidx.media3.common.Player.COMMAND_STOP; +import static androidx.media3.common.Player.STATE_ENDED; +import static androidx.media3.common.Player.STATE_IDLE; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.postOrRun; @@ -231,7 +233,17 @@ import org.checkerframework.checker.initialization.qual.Initialized; } else { dispatchSessionTaskWithPlayerCommand( COMMAND_PLAY_PAUSE, - (controller) -> sessionImpl.getPlayerWrapper().play(), + (controller) -> { + PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); + @Player.State int playbackState = playerWrapper.getPlaybackState(); + if (playbackState == STATE_IDLE) { + playerWrapper.prepare(); + } else if (playbackState == STATE_ENDED) { + playerWrapper.seekTo( + playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); + } + playerWrapper.play(); + }, remoteUserInfo); } } @@ -285,7 +297,17 @@ import org.checkerframework.checker.initialization.qual.Initialized; public void onPlay() { dispatchSessionTaskWithPlayerCommand( COMMAND_PLAY_PAUSE, - controller -> sessionImpl.getPlayerWrapper().play(), + controller -> { + PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); + @Player.State int playbackState = playerWrapper.getPlaybackState(); + if (playbackState == Player.STATE_IDLE) { + playerWrapper.prepare(); + } else if (playbackState == Player.STATE_ENDED) { + playerWrapper.seekTo( + playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); + } + playerWrapper.play(); + }, sessionCompat.getCurrentControllerInfo()); } @@ -321,7 +343,15 @@ import org.checkerframework.checker.initialization.qual.Initialized; if (sessionImpl.onSetMediaUriOnHandler( controller, mediaUri, extras == null ? Bundle.EMPTY : extras) == RESULT_SUCCESS) { - sessionImpl.getPlayerWrapper().play(); + PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); + @Player.State int playbackState = playerWrapper.getPlaybackState(); + if (playbackState == Player.STATE_IDLE) { + playerWrapper.prepare(); + } else if (playbackState == STATE_ENDED) { + playerWrapper.seekTo( + playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); + } + playerWrapper.play(); } }); } 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 56c3c0e450..f2a0ca9fc3 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -321,11 +321,7 @@ public abstract class MediaSessionService extends Service { return START_STICKY; } - DefaultActionFactory actionFactory; - synchronized (lock) { - actionFactory = checkStateNotNull(this.actionFactory); - } - + DefaultActionFactory actionFactory = getActionFactory(); @Nullable Uri uri = intent.getData(); @Nullable MediaSession session = uri != null ? MediaSession.getSession(uri) : null; if (actionFactory.isMediaAction(intent)) { @@ -343,10 +339,19 @@ public abstract class MediaSessionService extends Service { } } else if (actionFactory.isCustomAction(intent)) { @Nullable String customAction = actionFactory.getCustomAction(intent); - if (session != null && customAction != null) { - Bundle customExtras = actionFactory.getCustomActionExtras(intent); - getMediaNotificationManager().onCustomAction(session, customAction, customExtras); + if (customAction == null) { + return START_STICKY; } + if (session == null) { + ControllerInfo controllerInfo = ControllerInfo.createLegacyControllerInfo(); + session = onGetSession(controllerInfo); + if (session == null) { + return START_STICKY; + } + addSession(session); + } + Bundle customExtras = actionFactory.getCustomActionExtras(intent); + getMediaNotificationManager().onCustomAction(session, customAction, customExtras); } return START_STICKY; } @@ -396,15 +401,23 @@ public abstract class MediaSessionService extends Service { if (mediaNotificationProvider == null) { mediaNotificationProvider = new DefaultMediaNotificationProvider(getApplicationContext()); } - actionFactory = new DefaultActionFactory(/* service= */ this); mediaNotificationManager = new MediaNotificationManager( - /* mediaSessionService= */ this, mediaNotificationProvider, actionFactory); + /* mediaSessionService= */ this, mediaNotificationProvider, getActionFactory()); } return mediaNotificationManager; } } + private DefaultActionFactory getActionFactory() { + synchronized (lock) { + if (actionFactory == null) { + actionFactory = new DefaultActionFactory(/* service= */ this); + } + return actionFactory; + } + } + private static final class MediaSessionServiceStub extends IMediaSessionService.Stub { private final WeakReference serviceReference; @@ -469,7 +482,7 @@ public abstract class MediaSessionService extends Service { remoteUserInfo, /* controllerVersion= */ request.version, isTrusted, - /* controllerCb= */ null, + /* cb= */ null, request.connectionHints); @Nullable MediaSession session; diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index c9ceca0f89..36fd98bfa2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -15,10 +15,13 @@ */ package androidx.media3.session; +import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.postOrRun; +import static androidx.media3.session.MediaConstants.STATUS_CODE_SUCCESS_COMPAT; import android.media.AudioManager; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; @@ -52,8 +55,46 @@ import java.util.List; */ /* package */ class PlayerWrapper extends ForwardingPlayer { + private int legacyStatusCode; + @Nullable private String legacyErrorMessage; + @Nullable private Bundle legacyErrorExtras; + public PlayerWrapper(Player player) { super(player); + legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; + } + + /** + * Sets the legacy error code. + * + *

    This sets the legacy {@link PlaybackStateCompat} to {@link PlaybackStateCompat#STATE_ERROR} + * and calls {@link PlaybackStateCompat.Builder#setErrorMessage(int, CharSequence)} and {@link + * PlaybackStateCompat.Builder#setExtras(Bundle)} with the given arguments. + * + *

    Use {@link #clearLegacyErrorStatus()} to clear the error state and to resume to the actual + * playback state reflecting the player. + * + * @param errorCode The legacy error code. + * @param errorMessage The legacy error message. + * @param extras The extras. + */ + public void setLegacyErrorStatus(int errorCode, String errorMessage, Bundle extras) { + checkState(errorCode != STATUS_CODE_SUCCESS_COMPAT); + legacyStatusCode = errorCode; + legacyErrorMessage = errorMessage; + legacyErrorExtras = extras; + } + + /** Returns the legacy status code. */ + public int getLegacyStatusCode() { + return legacyStatusCode; + } + + /** Clears the legacy error status. */ + public void clearLegacyErrorStatus() { + legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; + legacyErrorMessage = null; + legacyErrorExtras = null; } @Override @@ -702,6 +743,19 @@ import java.util.List; } public PlaybackStateCompat createPlaybackStateCompat() { + if (legacyStatusCode != STATUS_CODE_SUCCESS_COMPAT) { + return new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_ERROR, + /* position= */ PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, + /* playbackSpeed= */ 0, + /* updateTime= */ SystemClock.elapsedRealtime()) + .setActions(0) + .setBufferedPosition(0) + .setErrorMessage(legacyStatusCode, checkNotNull(legacyErrorMessage)) + .setExtras(checkNotNull(legacyErrorExtras)) + .build(); + } @Nullable PlaybackException playerError = getPlayerError(); int state = MediaUtils.convertToPlaybackStateCompatState( @@ -808,8 +862,8 @@ import java.util.List; return new PositionInfo( /* windowUid= */ null, getCurrentMediaItemIndex(), - /* periodUid= */ null, getCurrentMediaItem(), + /* periodUid= */ null, getCurrentPeriodIndex(), getCurrentPosition(), getContentPosition(), diff --git a/libraries/session/src/main/res/values/strings.xml b/libraries/session/src/main/res/values/strings.xml index 4b5b6c86e0..06eef42afe 100644 --- a/libraries/session/src/main/res/values/strings.xml +++ b/libraries/session/src/main/res/values/strings.xml @@ -28,4 +28,5 @@ Seek back Seek forward + Authentication required diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump index 78d2922cbf..804961f690 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 9529 sample count = 45 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump index cbfab8edfd..a974548364 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 7464 sample count = 33 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump index bd8df24015..19fd0f36d2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 4019 sample count = 18 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump index 791aea54f7..80ca2a76c7 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 470 sample count = 3 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump index 78d2922cbf..804961f690 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 9529 sample count = 45 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.0.dump new file mode 100644 index 0000000000..e9a5eb797e --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.0.dump @@ -0,0 +1,337 @@ +seekMap: + isSeekable = true + duration = 1089000 + getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(544500) = [[timeUs=0, position=48]] + getPosition(1089000) = [[timeUs=0, position=48]] +numberOfTracks = 2 +track 0: + total output bytes = 79444 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/av01 + maxInputSize = 54267 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 17, hash 54AC4E6D + sample 0: + time = 0 + flags = 1 + data = length 54237, hash 978897A5 + sample 1: + time = 33366 + flags = 0 + data = length 21903, hash D3A1A794 + sample 2: + time = 66733 + flags = 0 + data = length 65, hash 401C922E + sample 3: + time = 100100 + flags = 0 + data = length 3, hash D5E0 + sample 4: + time = 133466 + flags = 0 + data = length 161, hash 3BAF4398 + sample 5: + time = 166833 + flags = 0 + data = length 3, hash D610 + sample 6: + time = 200200 + flags = 0 + data = length 47, hash 1BF8FBF + sample 7: + time = 233566 + flags = 0 + data = length 3, hash D5D0 + sample 8: + time = 266933 + flags = 0 + data = length 287, hash AF180C67 + sample 9: + time = 300300 + flags = 0 + data = length 33, hash B4D41A8F + sample 10: + time = 333666 + flags = 0 + data = length 3, hash D5E0 + sample 11: + time = 367033 + flags = 0 + data = length 236, hash 4DEB22C9 + sample 12: + time = 400400 + flags = 0 + data = length 3, hash D600 + sample 13: + time = 433766 + flags = 0 + data = length 202, hash 6AF564D + sample 14: + time = 467133 + flags = 0 + data = length 3, hash D5C0 + sample 15: + time = 500500 + flags = 0 + data = length 1275, hash 9C2CCEA5 + sample 16: + time = 533866 + flags = 0 + data = length 103, hash AC226D96 + sample 17: + time = 567233 + flags = 0 + data = length 3, hash D5E0 + sample 18: + time = 600600 + flags = 0 + data = length 250, hash 1C73058F + sample 19: + time = 633966 + flags = 0 + data = length 3, hash D5F0 + sample 20: + time = 667333 + flags = 0 + data = length 39, hash 26EBA81E + sample 21: + time = 700700 + flags = 0 + data = length 3, hash D610 + sample 22: + time = 734066 + flags = 0 + data = length 289, hash 4E5480FB + sample 23: + time = 767433 + flags = 0 + data = length 45, hash 8A594F0A + sample 24: + time = 800800 + flags = 0 + data = length 3, hash D600 + sample 25: + time = 834166 + flags = 0 + data = length 116, hash 9D1150FF + sample 26: + time = 867533 + flags = 0 + data = length 3, hash D5F0 + sample 27: + time = 900900 + flags = 0 + data = length 33, hash C36E3AEF + sample 28: + time = 934266 + flags = 0 + data = length 26, hash 6119E4D3 + sample 29: + time = 967633 + flags = 536870912 + data = length 64, hash A8A201F0 +track 1: + total output bytes = 9529 + sample count = 45 + format 0: + averageBitrate = 72956 + peakBitrate = 74502 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 43000 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 66219 + flags = 1 + data = length 6, hash 31EC5206 + sample 2: + time = 89439 + flags = 1 + data = length 148, hash 894A176B + sample 3: + time = 112659 + flags = 1 + data = length 189, hash CEF235A1 + sample 4: + time = 135879 + flags = 1 + data = length 205, hash BBF5F7B0 + sample 5: + time = 159099 + flags = 1 + data = length 210, hash F278B193 + sample 6: + time = 182319 + flags = 1 + data = length 210, hash 82DA1589 + sample 7: + time = 205539 + flags = 1 + data = length 207, hash 5BE231DF + sample 8: + time = 228759 + flags = 1 + data = length 225, hash 18819EE1 + sample 9: + time = 251979 + flags = 1 + data = length 215, hash CA7FA67B + sample 10: + time = 275199 + flags = 1 + data = length 211, hash 581A1C18 + sample 11: + time = 298419 + flags = 1 + data = length 216, hash ADB88187 + sample 12: + time = 321639 + flags = 1 + data = length 229, hash 2E8BA4DC + sample 13: + time = 344859 + flags = 1 + data = length 232, hash 22F0C510 + sample 14: + time = 368079 + flags = 1 + data = length 235, hash 867AD0DC + sample 15: + time = 391299 + flags = 1 + data = length 231, hash 84E823A8 + sample 16: + time = 414519 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 17: + time = 437739 + flags = 1 + data = length 216, hash EAA345AE + sample 18: + time = 460959 + flags = 1 + data = length 229, hash 6957411F + sample 19: + time = 484179 + flags = 1 + data = length 219, hash 41275022 + sample 20: + time = 507399 + flags = 1 + data = length 241, hash 6495DF96 + sample 21: + time = 530619 + flags = 1 + data = length 228, hash 63D95906 + sample 22: + time = 553839 + flags = 1 + data = length 238, hash 34F676F9 + sample 23: + time = 577058 + flags = 1 + data = length 234, hash E5CBC045 + sample 24: + time = 600278 + flags = 1 + data = length 231, hash 5FC43661 + sample 25: + time = 623498 + flags = 1 + data = length 217, hash 682708ED + sample 26: + time = 646718 + flags = 1 + data = length 239, hash D43780FC + sample 27: + time = 669938 + flags = 1 + data = length 243, hash C5E17980 + sample 28: + time = 693158 + flags = 1 + data = length 231, hash AC5837BA + sample 29: + time = 716378 + flags = 1 + data = length 230, hash 169EE895 + sample 30: + time = 739598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 31: + time = 762818 + flags = 1 + data = length 225, hash 531E4599 + sample 32: + time = 786038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 33: + time = 809258 + flags = 1 + data = length 243, hash F8C94C7 + sample 34: + time = 832478 + flags = 1 + data = length 232, hash A646A7D0 + sample 35: + time = 855698 + flags = 1 + data = length 237, hash E8B787A5 + sample 36: + time = 878918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 37: + time = 902138 + flags = 1 + data = length 235, hash B9B33B0A + sample 38: + time = 925358 + flags = 1 + data = length 264, hash 71A4869E + sample 39: + time = 948578 + flags = 1 + data = length 257, hash D049B54C + sample 40: + time = 971798 + flags = 1 + data = length 227, hash 66757231 + sample 41: + time = 995018 + flags = 1 + data = length 227, hash BD374F1B + sample 42: + time = 1018238 + flags = 1 + data = length 235, hash 999477F6 + sample 43: + time = 1041458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 44: + time = 1064678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.1.dump new file mode 100644 index 0000000000..95bc8841af --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.1.dump @@ -0,0 +1,285 @@ +seekMap: + isSeekable = true + duration = 1089000 + getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(544500) = [[timeUs=0, position=48]] + getPosition(1089000) = [[timeUs=0, position=48]] +numberOfTracks = 2 +track 0: + total output bytes = 79444 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/av01 + maxInputSize = 54267 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 17, hash 54AC4E6D + sample 0: + time = 0 + flags = 1 + data = length 54237, hash 978897A5 + sample 1: + time = 33366 + flags = 0 + data = length 21903, hash D3A1A794 + sample 2: + time = 66733 + flags = 0 + data = length 65, hash 401C922E + sample 3: + time = 100100 + flags = 0 + data = length 3, hash D5E0 + sample 4: + time = 133466 + flags = 0 + data = length 161, hash 3BAF4398 + sample 5: + time = 166833 + flags = 0 + data = length 3, hash D610 + sample 6: + time = 200200 + flags = 0 + data = length 47, hash 1BF8FBF + sample 7: + time = 233566 + flags = 0 + data = length 3, hash D5D0 + sample 8: + time = 266933 + flags = 0 + data = length 287, hash AF180C67 + sample 9: + time = 300300 + flags = 0 + data = length 33, hash B4D41A8F + sample 10: + time = 333666 + flags = 0 + data = length 3, hash D5E0 + sample 11: + time = 367033 + flags = 0 + data = length 236, hash 4DEB22C9 + sample 12: + time = 400400 + flags = 0 + data = length 3, hash D600 + sample 13: + time = 433766 + flags = 0 + data = length 202, hash 6AF564D + sample 14: + time = 467133 + flags = 0 + data = length 3, hash D5C0 + sample 15: + time = 500500 + flags = 0 + data = length 1275, hash 9C2CCEA5 + sample 16: + time = 533866 + flags = 0 + data = length 103, hash AC226D96 + sample 17: + time = 567233 + flags = 0 + data = length 3, hash D5E0 + sample 18: + time = 600600 + flags = 0 + data = length 250, hash 1C73058F + sample 19: + time = 633966 + flags = 0 + data = length 3, hash D5F0 + sample 20: + time = 667333 + flags = 0 + data = length 39, hash 26EBA81E + sample 21: + time = 700700 + flags = 0 + data = length 3, hash D610 + sample 22: + time = 734066 + flags = 0 + data = length 289, hash 4E5480FB + sample 23: + time = 767433 + flags = 0 + data = length 45, hash 8A594F0A + sample 24: + time = 800800 + flags = 0 + data = length 3, hash D600 + sample 25: + time = 834166 + flags = 0 + data = length 116, hash 9D1150FF + sample 26: + time = 867533 + flags = 0 + data = length 3, hash D5F0 + sample 27: + time = 900900 + flags = 0 + data = length 33, hash C36E3AEF + sample 28: + time = 934266 + flags = 0 + data = length 26, hash 6119E4D3 + sample 29: + time = 967633 + flags = 536870912 + data = length 64, hash A8A201F0 +track 1: + total output bytes = 7235 + sample count = 32 + format 0: + averageBitrate = 72956 + peakBitrate = 74502 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 344859 + flags = 1 + data = length 232, hash 22F0C510 + sample 1: + time = 368079 + flags = 1 + data = length 235, hash 867AD0DC + sample 2: + time = 391299 + flags = 1 + data = length 231, hash 84E823A8 + sample 3: + time = 414519 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 4: + time = 437739 + flags = 1 + data = length 216, hash EAA345AE + sample 5: + time = 460959 + flags = 1 + data = length 229, hash 6957411F + sample 6: + time = 484179 + flags = 1 + data = length 219, hash 41275022 + sample 7: + time = 507399 + flags = 1 + data = length 241, hash 6495DF96 + sample 8: + time = 530619 + flags = 1 + data = length 228, hash 63D95906 + sample 9: + time = 553839 + flags = 1 + data = length 238, hash 34F676F9 + sample 10: + time = 577058 + flags = 1 + data = length 234, hash E5CBC045 + sample 11: + time = 600278 + flags = 1 + data = length 231, hash 5FC43661 + sample 12: + time = 623498 + flags = 1 + data = length 217, hash 682708ED + sample 13: + time = 646718 + flags = 1 + data = length 239, hash D43780FC + sample 14: + time = 669938 + flags = 1 + data = length 243, hash C5E17980 + sample 15: + time = 693158 + flags = 1 + data = length 231, hash AC5837BA + sample 16: + time = 716378 + flags = 1 + data = length 230, hash 169EE895 + sample 17: + time = 739598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 18: + time = 762818 + flags = 1 + data = length 225, hash 531E4599 + sample 19: + time = 786038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 20: + time = 809258 + flags = 1 + data = length 243, hash F8C94C7 + sample 21: + time = 832478 + flags = 1 + data = length 232, hash A646A7D0 + sample 22: + time = 855698 + flags = 1 + data = length 237, hash E8B787A5 + sample 23: + time = 878918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 24: + time = 902138 + flags = 1 + data = length 235, hash B9B33B0A + sample 25: + time = 925358 + flags = 1 + data = length 264, hash 71A4869E + sample 26: + time = 948578 + flags = 1 + data = length 257, hash D049B54C + sample 27: + time = 971798 + flags = 1 + data = length 227, hash 66757231 + sample 28: + time = 995018 + flags = 1 + data = length 227, hash BD374F1B + sample 29: + time = 1018238 + flags = 1 + data = length 235, hash 999477F6 + sample 30: + time = 1041458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 31: + time = 1064678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.2.dump new file mode 100644 index 0000000000..c814ddc79d --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.2.dump @@ -0,0 +1,221 @@ +seekMap: + isSeekable = true + duration = 1089000 + getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(544500) = [[timeUs=0, position=48]] + getPosition(1089000) = [[timeUs=0, position=48]] +numberOfTracks = 2 +track 0: + total output bytes = 79444 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/av01 + maxInputSize = 54267 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 17, hash 54AC4E6D + sample 0: + time = 0 + flags = 1 + data = length 54237, hash 978897A5 + sample 1: + time = 33366 + flags = 0 + data = length 21903, hash D3A1A794 + sample 2: + time = 66733 + flags = 0 + data = length 65, hash 401C922E + sample 3: + time = 100100 + flags = 0 + data = length 3, hash D5E0 + sample 4: + time = 133466 + flags = 0 + data = length 161, hash 3BAF4398 + sample 5: + time = 166833 + flags = 0 + data = length 3, hash D610 + sample 6: + time = 200200 + flags = 0 + data = length 47, hash 1BF8FBF + sample 7: + time = 233566 + flags = 0 + data = length 3, hash D5D0 + sample 8: + time = 266933 + flags = 0 + data = length 287, hash AF180C67 + sample 9: + time = 300300 + flags = 0 + data = length 33, hash B4D41A8F + sample 10: + time = 333666 + flags = 0 + data = length 3, hash D5E0 + sample 11: + time = 367033 + flags = 0 + data = length 236, hash 4DEB22C9 + sample 12: + time = 400400 + flags = 0 + data = length 3, hash D600 + sample 13: + time = 433766 + flags = 0 + data = length 202, hash 6AF564D + sample 14: + time = 467133 + flags = 0 + data = length 3, hash D5C0 + sample 15: + time = 500500 + flags = 0 + data = length 1275, hash 9C2CCEA5 + sample 16: + time = 533866 + flags = 0 + data = length 103, hash AC226D96 + sample 17: + time = 567233 + flags = 0 + data = length 3, hash D5E0 + sample 18: + time = 600600 + flags = 0 + data = length 250, hash 1C73058F + sample 19: + time = 633966 + flags = 0 + data = length 3, hash D5F0 + sample 20: + time = 667333 + flags = 0 + data = length 39, hash 26EBA81E + sample 21: + time = 700700 + flags = 0 + data = length 3, hash D610 + sample 22: + time = 734066 + flags = 0 + data = length 289, hash 4E5480FB + sample 23: + time = 767433 + flags = 0 + data = length 45, hash 8A594F0A + sample 24: + time = 800800 + flags = 0 + data = length 3, hash D600 + sample 25: + time = 834166 + flags = 0 + data = length 116, hash 9D1150FF + sample 26: + time = 867533 + flags = 0 + data = length 3, hash D5F0 + sample 27: + time = 900900 + flags = 0 + data = length 33, hash C36E3AEF + sample 28: + time = 934266 + flags = 0 + data = length 26, hash 6119E4D3 + sample 29: + time = 967633 + flags = 536870912 + data = length 64, hash A8A201F0 +track 1: + total output bytes = 3545 + sample count = 16 + format 0: + averageBitrate = 72956 + peakBitrate = 74502 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 716378 + flags = 1 + data = length 230, hash 169EE895 + sample 1: + time = 739598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 2: + time = 762818 + flags = 1 + data = length 225, hash 531E4599 + sample 3: + time = 786038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 4: + time = 809258 + flags = 1 + data = length 243, hash F8C94C7 + sample 5: + time = 832478 + flags = 1 + data = length 232, hash A646A7D0 + sample 6: + time = 855698 + flags = 1 + data = length 237, hash E8B787A5 + sample 7: + time = 878918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 8: + time = 902138 + flags = 1 + data = length 235, hash B9B33B0A + sample 9: + time = 925358 + flags = 1 + data = length 264, hash 71A4869E + sample 10: + time = 948578 + flags = 1 + data = length 257, hash D049B54C + sample 11: + time = 971798 + flags = 1 + data = length 227, hash 66757231 + sample 12: + time = 995018 + flags = 1 + data = length 227, hash BD374F1B + sample 13: + time = 1018238 + flags = 1 + data = length 235, hash 999477F6 + sample 14: + time = 1041458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 15: + time = 1064678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.3.dump new file mode 100644 index 0000000000..8dfd47721c --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.3.dump @@ -0,0 +1,161 @@ +seekMap: + isSeekable = true + duration = 1089000 + getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(544500) = [[timeUs=0, position=48]] + getPosition(1089000) = [[timeUs=0, position=48]] +numberOfTracks = 2 +track 0: + total output bytes = 79444 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/av01 + maxInputSize = 54267 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 17, hash 54AC4E6D + sample 0: + time = 0 + flags = 1 + data = length 54237, hash 978897A5 + sample 1: + time = 33366 + flags = 0 + data = length 21903, hash D3A1A794 + sample 2: + time = 66733 + flags = 0 + data = length 65, hash 401C922E + sample 3: + time = 100100 + flags = 0 + data = length 3, hash D5E0 + sample 4: + time = 133466 + flags = 0 + data = length 161, hash 3BAF4398 + sample 5: + time = 166833 + flags = 0 + data = length 3, hash D610 + sample 6: + time = 200200 + flags = 0 + data = length 47, hash 1BF8FBF + sample 7: + time = 233566 + flags = 0 + data = length 3, hash D5D0 + sample 8: + time = 266933 + flags = 0 + data = length 287, hash AF180C67 + sample 9: + time = 300300 + flags = 0 + data = length 33, hash B4D41A8F + sample 10: + time = 333666 + flags = 0 + data = length 3, hash D5E0 + sample 11: + time = 367033 + flags = 0 + data = length 236, hash 4DEB22C9 + sample 12: + time = 400400 + flags = 0 + data = length 3, hash D600 + sample 13: + time = 433766 + flags = 0 + data = length 202, hash 6AF564D + sample 14: + time = 467133 + flags = 0 + data = length 3, hash D5C0 + sample 15: + time = 500500 + flags = 0 + data = length 1275, hash 9C2CCEA5 + sample 16: + time = 533866 + flags = 0 + data = length 103, hash AC226D96 + sample 17: + time = 567233 + flags = 0 + data = length 3, hash D5E0 + sample 18: + time = 600600 + flags = 0 + data = length 250, hash 1C73058F + sample 19: + time = 633966 + flags = 0 + data = length 3, hash D5F0 + sample 20: + time = 667333 + flags = 0 + data = length 39, hash 26EBA81E + sample 21: + time = 700700 + flags = 0 + data = length 3, hash D610 + sample 22: + time = 734066 + flags = 0 + data = length 289, hash 4E5480FB + sample 23: + time = 767433 + flags = 0 + data = length 45, hash 8A594F0A + sample 24: + time = 800800 + flags = 0 + data = length 3, hash D600 + sample 25: + time = 834166 + flags = 0 + data = length 116, hash 9D1150FF + sample 26: + time = 867533 + flags = 0 + data = length 3, hash D5F0 + sample 27: + time = 900900 + flags = 0 + data = length 33, hash C36E3AEF + sample 28: + time = 934266 + flags = 0 + data = length 26, hash 6119E4D3 + sample 29: + time = 967633 + flags = 536870912 + data = length 64, hash A8A201F0 +track 1: + total output bytes = 6 + sample count = 1 + format 0: + averageBitrate = 72956 + peakBitrate = 74502 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 1064678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.unknown_length.dump new file mode 100644 index 0000000000..e9a5eb797e --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.unknown_length.dump @@ -0,0 +1,337 @@ +seekMap: + isSeekable = true + duration = 1089000 + getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(544500) = [[timeUs=0, position=48]] + getPosition(1089000) = [[timeUs=0, position=48]] +numberOfTracks = 2 +track 0: + total output bytes = 79444 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/av01 + maxInputSize = 54267 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 17, hash 54AC4E6D + sample 0: + time = 0 + flags = 1 + data = length 54237, hash 978897A5 + sample 1: + time = 33366 + flags = 0 + data = length 21903, hash D3A1A794 + sample 2: + time = 66733 + flags = 0 + data = length 65, hash 401C922E + sample 3: + time = 100100 + flags = 0 + data = length 3, hash D5E0 + sample 4: + time = 133466 + flags = 0 + data = length 161, hash 3BAF4398 + sample 5: + time = 166833 + flags = 0 + data = length 3, hash D610 + sample 6: + time = 200200 + flags = 0 + data = length 47, hash 1BF8FBF + sample 7: + time = 233566 + flags = 0 + data = length 3, hash D5D0 + sample 8: + time = 266933 + flags = 0 + data = length 287, hash AF180C67 + sample 9: + time = 300300 + flags = 0 + data = length 33, hash B4D41A8F + sample 10: + time = 333666 + flags = 0 + data = length 3, hash D5E0 + sample 11: + time = 367033 + flags = 0 + data = length 236, hash 4DEB22C9 + sample 12: + time = 400400 + flags = 0 + data = length 3, hash D600 + sample 13: + time = 433766 + flags = 0 + data = length 202, hash 6AF564D + sample 14: + time = 467133 + flags = 0 + data = length 3, hash D5C0 + sample 15: + time = 500500 + flags = 0 + data = length 1275, hash 9C2CCEA5 + sample 16: + time = 533866 + flags = 0 + data = length 103, hash AC226D96 + sample 17: + time = 567233 + flags = 0 + data = length 3, hash D5E0 + sample 18: + time = 600600 + flags = 0 + data = length 250, hash 1C73058F + sample 19: + time = 633966 + flags = 0 + data = length 3, hash D5F0 + sample 20: + time = 667333 + flags = 0 + data = length 39, hash 26EBA81E + sample 21: + time = 700700 + flags = 0 + data = length 3, hash D610 + sample 22: + time = 734066 + flags = 0 + data = length 289, hash 4E5480FB + sample 23: + time = 767433 + flags = 0 + data = length 45, hash 8A594F0A + sample 24: + time = 800800 + flags = 0 + data = length 3, hash D600 + sample 25: + time = 834166 + flags = 0 + data = length 116, hash 9D1150FF + sample 26: + time = 867533 + flags = 0 + data = length 3, hash D5F0 + sample 27: + time = 900900 + flags = 0 + data = length 33, hash C36E3AEF + sample 28: + time = 934266 + flags = 0 + data = length 26, hash 6119E4D3 + sample 29: + time = 967633 + flags = 536870912 + data = length 64, hash A8A201F0 +track 1: + total output bytes = 9529 + sample count = 45 + format 0: + averageBitrate = 72956 + peakBitrate = 74502 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 43000 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 66219 + flags = 1 + data = length 6, hash 31EC5206 + sample 2: + time = 89439 + flags = 1 + data = length 148, hash 894A176B + sample 3: + time = 112659 + flags = 1 + data = length 189, hash CEF235A1 + sample 4: + time = 135879 + flags = 1 + data = length 205, hash BBF5F7B0 + sample 5: + time = 159099 + flags = 1 + data = length 210, hash F278B193 + sample 6: + time = 182319 + flags = 1 + data = length 210, hash 82DA1589 + sample 7: + time = 205539 + flags = 1 + data = length 207, hash 5BE231DF + sample 8: + time = 228759 + flags = 1 + data = length 225, hash 18819EE1 + sample 9: + time = 251979 + flags = 1 + data = length 215, hash CA7FA67B + sample 10: + time = 275199 + flags = 1 + data = length 211, hash 581A1C18 + sample 11: + time = 298419 + flags = 1 + data = length 216, hash ADB88187 + sample 12: + time = 321639 + flags = 1 + data = length 229, hash 2E8BA4DC + sample 13: + time = 344859 + flags = 1 + data = length 232, hash 22F0C510 + sample 14: + time = 368079 + flags = 1 + data = length 235, hash 867AD0DC + sample 15: + time = 391299 + flags = 1 + data = length 231, hash 84E823A8 + sample 16: + time = 414519 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 17: + time = 437739 + flags = 1 + data = length 216, hash EAA345AE + sample 18: + time = 460959 + flags = 1 + data = length 229, hash 6957411F + sample 19: + time = 484179 + flags = 1 + data = length 219, hash 41275022 + sample 20: + time = 507399 + flags = 1 + data = length 241, hash 6495DF96 + sample 21: + time = 530619 + flags = 1 + data = length 228, hash 63D95906 + sample 22: + time = 553839 + flags = 1 + data = length 238, hash 34F676F9 + sample 23: + time = 577058 + flags = 1 + data = length 234, hash E5CBC045 + sample 24: + time = 600278 + flags = 1 + data = length 231, hash 5FC43661 + sample 25: + time = 623498 + flags = 1 + data = length 217, hash 682708ED + sample 26: + time = 646718 + flags = 1 + data = length 239, hash D43780FC + sample 27: + time = 669938 + flags = 1 + data = length 243, hash C5E17980 + sample 28: + time = 693158 + flags = 1 + data = length 231, hash AC5837BA + sample 29: + time = 716378 + flags = 1 + data = length 230, hash 169EE895 + sample 30: + time = 739598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 31: + time = 762818 + flags = 1 + data = length 225, hash 531E4599 + sample 32: + time = 786038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 33: + time = 809258 + flags = 1 + data = length 243, hash F8C94C7 + sample 34: + time = 832478 + flags = 1 + data = length 232, hash A646A7D0 + sample 35: + time = 855698 + flags = 1 + data = length 237, hash E8B787A5 + sample 36: + time = 878918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 37: + time = 902138 + flags = 1 + data = length 235, hash B9B33B0A + sample 38: + time = 925358 + flags = 1 + data = length 264, hash 71A4869E + sample 39: + time = 948578 + flags = 1 + data = length 257, hash D049B54C + sample 40: + time = 971798 + flags = 1 + data = length 227, hash 66757231 + sample 41: + time = 995018 + flags = 1 + data = length 227, hash BD374F1B + sample 42: + time = 1018238 + flags = 1 + data = length 235, hash 999477F6 + sample 43: + time = 1041458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 44: + time = 1064678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump index 1fca581047..cb3c2003f0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump @@ -139,6 +139,7 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump index 1fca581047..cb3c2003f0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump @@ -139,6 +139,7 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump index b96cd5335f..5165fb2222 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump @@ -142,6 +142,8 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + averageBitrate = 136736 + peakBitrate = 145976 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump index 508eb65d16..d64a12c6d4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump @@ -142,6 +142,8 @@ track 1: total output bytes = 13359 sample count = 31 format 0: + averageBitrate = 136736 + peakBitrate = 145976 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump index 4aa40d6145..98cc19fadf 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump @@ -142,6 +142,8 @@ track 1: total output bytes = 6804 sample count = 16 format 0: + averageBitrate = 136736 + peakBitrate = 145976 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump index 5196703b22..3fd459b246 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump @@ -142,6 +142,8 @@ track 1: total output bytes = 10 sample count = 1 format 0: + averageBitrate = 136736 + peakBitrate = 145976 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump index b96cd5335f..5165fb2222 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump @@ -142,6 +142,8 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + averageBitrate = 136736 + peakBitrate = 145976 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump index 91376322a8..cfc8c3c60c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump @@ -139,6 +139,7 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump index 91376322a8..cfc8c3c60c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump @@ -139,6 +139,7 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump index 0ce12c9a5c..321bc3a832 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 9529 sample count = 45 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump index 9dc2300bd9..4d8fe681ce 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 7464 sample count = 33 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump index d1d819d94a..a3e5cd60d0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 4019 sample count = 18 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump index 2241b26787..a498d93b5e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 470 sample count = 3 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump index 0ce12c9a5c..321bc3a832 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 9529 sample count = 45 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump index 9e58eea933..85e1af453c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump @@ -139,6 +139,8 @@ track 1: total output bytes = 10107 sample count = 45 format 0: + averageBitrate = 1254 + peakBitrate = 69000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump index 9e58eea933..85e1af453c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump @@ -139,6 +139,8 @@ track 1: total output bytes = 10107 sample count = 45 format 0: + averageBitrate = 1254 + peakBitrate = 69000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.0.dump index 5fd303aed3..4696001142 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.0.dump @@ -65,6 +65,7 @@ track 1: total output bytes = 5993 sample count = 15 format 0: + peakBitrate = 192000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.1.dump index fa9e2af42e..c8c6fd609a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.1.dump @@ -65,6 +65,7 @@ track 1: total output bytes = 4794 sample count = 11 format 0: + peakBitrate = 192000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.2.dump index 8b91ab1a5b..4b825cdc0a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.2.dump @@ -65,6 +65,7 @@ track 1: total output bytes = 3001 sample count = 7 format 0: + peakBitrate = 192000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.3.dump index 22b236d572..0e9f58b51c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.3.dump @@ -65,6 +65,7 @@ track 1: total output bytes = 1074 sample count = 3 format 0: + peakBitrate = 192000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.unknown_length.dump index 5fd303aed3..4696001142 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.unknown_length.dump @@ -65,6 +65,7 @@ track 1: total output bytes = 5993 sample count = 15 format 0: + peakBitrate = 192000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump index 8ef4f19b16..edf4002855 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump @@ -21,6 +21,8 @@ track 0: colorRange = 2 colorTransfer = 6 hdrStaticInfo = length 25, hash 423AFC35 + initializationData: + data = length 20, hash 4DF5B288 sample 0: time = 0 flags = 1 @@ -265,6 +267,8 @@ track 1: total output bytes = 16638 sample count = 44 format 0: + averageBitrate = 130279 + peakBitrate = 130279 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump index 1e1023afb0..2408a32712 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump @@ -21,6 +21,8 @@ track 0: colorRange = 2 colorTransfer = 6 hdrStaticInfo = length 25, hash 423AFC35 + initializationData: + data = length 20, hash 4DF5B288 sample 0: time = 0 flags = 1 @@ -265,6 +267,8 @@ track 1: total output bytes = 11156 sample count = 30 format 0: + averageBitrate = 130279 + peakBitrate = 130279 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump index 5b51396c83..98312fac0e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump @@ -21,6 +21,8 @@ track 0: colorRange = 2 colorTransfer = 6 hdrStaticInfo = length 25, hash 423AFC35 + initializationData: + data = length 20, hash 4DF5B288 sample 0: time = 0 flags = 1 @@ -265,6 +267,8 @@ track 1: total output bytes = 5567 sample count = 15 format 0: + averageBitrate = 130279 + peakBitrate = 130279 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump index d66f9234a1..95a2ed41bd 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump @@ -21,6 +21,8 @@ track 0: colorRange = 2 colorTransfer = 6 hdrStaticInfo = length 25, hash 423AFC35 + initializationData: + data = length 20, hash 4DF5B288 sample 0: time = 0 flags = 1 @@ -265,6 +267,8 @@ track 1: total output bytes = 374 sample count = 1 format 0: + averageBitrate = 130279 + peakBitrate = 130279 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump index 8ef4f19b16..edf4002855 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump @@ -21,6 +21,8 @@ track 0: colorRange = 2 colorTransfer = 6 hdrStaticInfo = length 25, hash 423AFC35 + initializationData: + data = length 20, hash 4DF5B288 sample 0: time = 0 flags = 1 @@ -265,6 +267,8 @@ track 1: total output bytes = 16638 sample count = 44 format 0: + averageBitrate = 130279 + peakBitrate = 130279 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame.png index fc51b716a0..340accdaea 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame.png and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_request_output_height.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_request_output_height.png new file mode 100644 index 0000000000..ed03445385 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_request_output_height.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_rotate90.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_rotate90.png index ac721443c9..767f3b8580 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_rotate90.png and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_rotate90.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_rotate_45_scale_to_fit.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_rotate_45_scale_to_fit.png new file mode 100644 index 0000000000..1dee08c552 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_rotate_45_scale_to_fit.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_rotate_then_translate.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_rotate_then_translate.png new file mode 100644 index 0000000000..5cd7a9e989 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_rotate_then_translate.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_scale_narrow.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_scale_narrow.png index 0c53022a09..b4b734bb27 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_scale_narrow.png and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_scale_narrow.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_translate_right.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_translate_right.png index 8ad95fe043..0d1905cca7 100644 Binary files a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_translate_right.png and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_translate_right.png differ diff --git a/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_translate_then_rotate.png b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_translate_then_rotate.png new file mode 100644 index 0000000000..a2efe9118c Binary files /dev/null and b/libraries/test_data/src/test/assets/media/bitmap/sample_mp4_first_frame_translate_then_rotate.png differ diff --git a/libraries/test_data/src/test/assets/media/mp4/sample_av1.mp4 b/libraries/test_data/src/test/assets/media/mp4/sample_av1.mp4 new file mode 100644 index 0000000000..eaa6a1ecf8 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/sample_av1.mp4 differ diff --git a/libraries/test_data/src/test/assets/media/mp4/sample_with_increasing_timestamps.mp4 b/libraries/test_data/src/test/assets/media/mp4/sample_with_increasing_timestamps.mp4 new file mode 100644 index 0000000000..84e40d7dd0 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/sample_with_increasing_timestamps.mp4 differ diff --git a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump index 7b6604be43..a38e2c887e 100644 --- a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump +++ b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump @@ -1,5 +1,6 @@ containerMimeType = video/mp4 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump index adc14a43a1..adbbb3a013 100644 --- a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump +++ b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump @@ -1,5 +1,6 @@ containerMimeType = video/mp4 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DashTestRunner.java b/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DashTestRunner.java index f2b3ec716c..16429406d3 100644 --- a/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DashTestRunner.java +++ b/libraries/test_exoplayer_playback/src/androidTest/java/androidx/media3/test/exoplayer/playback/gts/DashTestRunner.java @@ -27,7 +27,6 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; @@ -46,6 +45,7 @@ import androidx.media3.exoplayer.drm.MediaDrmCallback; import androidx.media3.exoplayer.drm.UnsupportedDrmException; import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.MappingTrackSelector; diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java index 6a132efbc1..b20ec27ab2 100644 --- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java +++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java @@ -36,6 +36,9 @@ public class MediaBrowserConstants { public static final String PARENT_ID_LONG_LIST = "parent_id_long_list"; public static final String PARENT_ID_NO_CHILDREN = "parent_id_no_children"; public static final String PARENT_ID_ERROR = "parent_id_error"; + public static final String PARENT_ID_AUTH_EXPIRED_ERROR = "parent_auth_expired_error"; + public static final String PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL = + "parent_auth_expired_error_label"; public static final List GET_CHILDREN_RESULT = new ArrayList<>(); public static final int CHILDREN_COUNT = 100; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java index 7bb315e509..aadc857a3c 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java @@ -32,6 +32,8 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_METADATA; import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID; +import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR; +import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_NO_CHILDREN; @@ -59,6 +61,7 @@ import android.support.v4.media.MediaBrowserCompat.MediaItem; import android.support.v4.media.MediaBrowserCompat.SearchCallback; import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback; import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.session.PlaybackStateCompat; import android.text.TextUtils; import androidx.media3.test.session.common.TestUtils; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -301,6 +304,47 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } + @Test + public void getChildren_authErrorResult() throws InterruptedException { + String testParentId = PARENT_ID_AUTH_EXPIRED_ERROR; + connectAndWait(); + CountDownLatch errorLatch = new CountDownLatch(1); + browserCompat.subscribe( + testParentId, + new SubscriptionCallback() { + @Override + public void onError(String parentId) { + assertThat(parentId).isEqualTo(testParentId); + errorLatch.countDown(); + } + }); + assertThat(errorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(lastReportedPlaybackStateCompat).isNotNull(); + assertThat(lastReportedPlaybackStateCompat.getState()) + .isEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat( + lastReportedPlaybackStateCompat + .getExtras() + .getString(MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT)) + .isEqualTo(PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL); + + CountDownLatch successLatch = new CountDownLatch(1); + browserCompat.subscribe( + PARENT_ID, + new SubscriptionCallback() { + @Override + public void onChildrenLoaded(String parentId, List children) { + assertThat(parentId).isEqualTo(PARENT_ID); + successLatch.countDown(); + } + }); + assertThat(successLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + // Any successful calls remove the error state, + assertThat(lastReportedPlaybackStateCompat.getState()) + .isNotEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat(lastReportedPlaybackStateCompat.getExtras()).isNull(); + } + @Test public void getChildren_emptyResult() throws InterruptedException { String testParentId = PARENT_ID_NO_CHILDREN; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java index 18315ca674..64db577d13 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java @@ -25,6 +25,8 @@ import android.content.ComponentName; import android.content.Context; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import androidx.annotation.Nullable; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.TestHandler; import androidx.test.core.app.ApplicationProvider; @@ -56,7 +58,9 @@ public class MediaBrowserCompatWithMediaSessionServiceTest { Context context; TestHandler handler; MediaBrowserCompat browserCompat; + @Nullable MediaControllerCompat controllerCompat; TestConnectionCallback connectionCallback; + @Nullable PlaybackStateCompat lastReportedPlaybackStateCompat; @Before public void setUp() throws Exception { @@ -117,6 +121,8 @@ public class MediaBrowserCompatWithMediaSessionServiceTest { public final CountDownLatch suspendedLatch = new CountDownLatch(1); public final CountDownLatch failedLatch = new CountDownLatch(1); + @Nullable MediaControllerCompat.Callback controllerCompatCallback; + TestConnectionCallback() { super(); } @@ -124,19 +130,38 @@ public class MediaBrowserCompatWithMediaSessionServiceTest { @Override public void onConnected() { super.onConnected(); + // Make browser's internal handler to be initialized with test thread. + controllerCompat = new MediaControllerCompat(context, browserCompat.getSessionToken()); + controllerCompatCallback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + lastReportedPlaybackStateCompat = state; + } + }; + controllerCompat.registerCallback(controllerCompatCallback); connectedLatch.countDown(); } @Override public void onConnectionSuspended() { super.onConnectionSuspended(); + unregisterControllerCallback(); suspendedLatch.countDown(); } @Override public void onConnectionFailed() { super.onConnectionFailed(); + unregisterControllerCallback(); failedLatch.countDown(); } + + private void unregisterControllerCallback() { + if (controllerCompat != null && controllerCompatCallback != null) { + controllerCompat.unregisterCallback(controllerCompatCallback); + } + controllerCompatCallback = null; + } } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionAndControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionAndControllerTest.java index 0ff9c10b0a..a31ea62039 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionAndControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionAndControllerTest.java @@ -181,7 +181,6 @@ public class MediaSessionAndControllerTest { MockPlayer player = new MockPlayer.Builder() .setApplicationLooper(threadTestRule.getHandler().getLooper()) - .setLatchCount(1) .build(); MediaSession session = sessionTestRule.ensureReleaseAfterTest( @@ -190,8 +189,7 @@ public class MediaSessionAndControllerTest { threadTestRule.getHandler().postAndSync(controller::play); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.playCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); } @Test diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java index 8f8d6e6c91..4eeb55c75c 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java @@ -74,7 +74,6 @@ public class MediaSessionCallbackTest { context = ApplicationProvider.getApplicationContext(); player = new MockPlayer.Builder() - .setLatchCount(1) .setApplicationLooper(threadTestRule.getHandler().getLooper()) .build(); } @@ -157,15 +156,14 @@ public class MediaSessionCallbackTest { controllerTestRule.createRemoteController(session.getToken()); controller.prepare(); - assertThat(player.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); - assertThat(player.prepareCalled).isFalse(); + Thread.sleep(NO_RESPONSE_TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); assertThat(commands).hasSize(1); assertThat(commands.get(0)).isEqualTo(Player.COMMAND_PREPARE); controller.play(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.playCalled).isTrue(); - assertThat(player.prepareCalled).isFalse(); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); assertThat(commands).hasSize(2); assertThat(commands.get(1)).isEqualTo(Player.COMMAND_PLAY_PAUSE); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java index f14252eb46..d328869726 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java @@ -94,8 +94,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { public void setUp() { context = ApplicationProvider.getApplicationContext(); handler = threadTestRule.getHandler(); - player = - new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); } @@ -206,8 +205,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().play(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.playCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); } @Test @@ -222,8 +221,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().pause(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.pauseCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS); } @Test @@ -238,8 +237,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().stop(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.stopCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS); } @Test @@ -254,8 +253,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().prepare(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.prepareCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); } @Test @@ -271,8 +270,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { long seekPosition = 12125L; controller.getTransportControls().seekTo(seekPosition); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO, TIMEOUT_MS); assertThat(player.seekPositionMs).isEqualTo(seekPosition); } @@ -289,8 +288,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { float testSpeed = 2.0f; controller.getTransportControls().setPlaybackSpeed(testSpeed); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setPlaybackSpeedCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_PLAYBACK_SPEED, TIMEOUT_MS); assertThat(player.playbackParameters.speed).isEqualTo(testSpeed); } @@ -316,8 +315,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { MediaDescriptionCompat desc = new MediaDescriptionCompat.Builder().setMediaId(mediaId).build(); controller.addQueueItem(desc); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.addMediaItemCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM, TIMEOUT_MS); assertThat(player.mediaItem.mediaId).isEqualTo(mediaId); } @@ -344,8 +342,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { MediaDescriptionCompat desc = new MediaDescriptionCompat.Builder().setMediaId(mediaId).build(); controller.addQueueItem(desc, testIndex); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.addMediaItemWithIndexCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM_WITH_INDEX, TIMEOUT_MS); assertThat(player.index).isEqualTo(testIndex); assertThat(player.mediaItem.mediaId).isEqualTo(mediaId); } @@ -375,8 +372,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { new MediaDescriptionCompat.Builder().setMediaId(targetItem.mediaId).build(); controller.removeQueueItem(desc); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.removeMediaItemCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_REMOVE_MEDIA_ITEM, TIMEOUT_MS); assertThat(player.index).isEqualTo(targetIndex); } @@ -392,8 +388,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().skipToPrevious(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToPreviousCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS); } @Test @@ -408,8 +404,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().skipToNext(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToNextCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS); } @Test @@ -434,8 +430,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { int targetIndex = 3; controller.getTransportControls().skipToQueueItem(queue.get(targetIndex).getQueueId()); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToDefaultPositionWithMediaItemIndexCalled).isTrue(); + player.awaitMethodCalled( + MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS); assertThat(player.seekMediaItemIndex).isEqualTo(targetIndex); } @@ -452,9 +448,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { @PlaybackStateCompat.ShuffleMode int testShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_GROUP; controller.getTransportControls().setShuffleMode(testShuffleMode); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setShuffleModeCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_SHUFFLE_MODE, TIMEOUT_MS); assertThat(player.shuffleModeEnabled).isTrue(); } @@ -471,9 +466,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { int testRepeatMode = Player.REPEAT_MODE_ALL; controller.getTransportControls().setRepeatMode(testRepeatMode); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setRepeatModeCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_REPEAT_MODE, TIMEOUT_MS); assertThat(player.repeatMode).isEqualTo(testRepeatMode); } @@ -488,7 +482,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); MockPlayer remotePlayer = - new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); handler.postAndSync( () -> { remotePlayer.deviceInfo = @@ -501,8 +495,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { int targetVolume = 50; controller.setVolumeTo(targetVolume, /* flags= */ 0); - assertThat(remotePlayer.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(remotePlayer.setDeviceVolumeCalled).isTrue(); + remotePlayer.awaitMethodCalled(MockPlayer.METHOD_SET_DEVICE_VOLUME, TIMEOUT_MS); assertThat(remotePlayer.deviceVolume).isEqualTo(targetVolume); } @@ -517,7 +510,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); MockPlayer remotePlayer = - new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); handler.postAndSync( () -> { remotePlayer.deviceInfo = @@ -529,8 +522,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.adjustVolume(AudioManager.ADJUST_RAISE, /* flags= */ 0); - assertThat(remotePlayer.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(remotePlayer.increaseDeviceVolumeCalled).isTrue(); + remotePlayer.awaitMethodCalled(MockPlayer.METHOD_INCREASE_DEVICE_VOLUME, TIMEOUT_MS); } @Test @@ -544,7 +536,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); MockPlayer remotePlayer = - new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); handler.postAndSync( () -> { remotePlayer.deviceInfo = @@ -556,8 +548,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.adjustVolume(AudioManager.ADJUST_LOWER, /* flags= */ 0); - assertThat(remotePlayer.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(remotePlayer.decreaseDeviceVolumeCalled).isTrue(); + remotePlayer.awaitMethodCalled(MockPlayer.METHOD_DECREASE_DEVICE_VOLUME, TIMEOUT_MS); } @Test @@ -704,7 +695,9 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + controller.sendCommand(testCommand, testArgs, /* cb= */ null); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } @@ -723,13 +716,15 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { .setId("controllerCallback_sessionRejects") .setSessionCallback(sessionCallback) .build(); - // Session will not accept the controller's commands. controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + controller.getTransportControls().play(); - assertThat(player.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + + Thread.sleep(NO_RESPONSE_TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse(); } @Test @@ -757,10 +752,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + controller.getTransportControls().prepareFromUri(mediaUri, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.prepareCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); } @Test @@ -788,10 +784,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + controller.getTransportControls().playFromUri(request, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.playCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); } @Test @@ -820,10 +817,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + controller.getTransportControls().prepareFromMediaId(request, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.prepareCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); } @Test @@ -852,10 +850,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + controller.getTransportControls().playFromMediaId(mediaId, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.playCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); } @Test @@ -884,10 +883,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + controller.getTransportControls().prepareFromSearch(query, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.prepareCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); } @Test @@ -916,10 +916,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + controller.getTransportControls().playFromSearch(query, bundle); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.playCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); } @Test @@ -944,7 +945,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { return Futures.immediateFuture(new SessionResult(RESULT_SUCCESS)); } }; - handler.postAndSync( () -> { List mediaItems = MediaTestUtils.createMediaItems(mediaId); @@ -958,7 +958,9 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + controller.getTransportControls().setRating(rating); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } @@ -991,16 +993,17 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().pause(); + assertThat(latchForPause.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); - assertThat(player.pauseCalled).isFalse(); + Thread.sleep(NO_RESPONSE_TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PAUSE)).isFalse(); assertThat(commands).hasSize(1); assertThat(commands.get(0)).isEqualTo(COMMAND_PLAY_PAUSE); controller.getTransportControls().prepare(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.prepareCalled).isTrue(); - assertThat(player.pauseCalled).isFalse(); + + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PAUSE)).isFalse(); assertThat(commands).hasSize(2); assertThat(commands.get(1)).isEqualTo(COMMAND_PREPARE); } @@ -1056,7 +1059,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { session = null; controller.getTransportControls().play(); - assertThat(player.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + Thread.sleep(NO_RESPONSE_TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse(); // Ensure that the controller cannot use newly create session with the same ID. // Recreated session has different session stub, so previously created controller @@ -1068,7 +1072,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { .build(); controller.getTransportControls().play(); - assertThat(player.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + Thread.sleep(NO_RESPONSE_TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse(); } private static class TestSessionCallback implements SessionCallback { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java index 22b3e8a3f5..b1b14a3aa5 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java @@ -89,8 +89,7 @@ public class MediaSessionKeyEventTest { Context context = ApplicationProvider.getApplicationContext(); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); handler = threadTestRule.getHandler(); - player = - new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); sessionCallback = new TestSessionCallback(); session = new MediaSession.Builder(context, player).setSessionCallback(sessionCallback).build(); @@ -120,7 +119,7 @@ public class MediaSessionKeyEventTest { } @After - public void cleanUp() throws Exception { + public void tearDown() throws Exception { handler.postAndSync( () -> { if (mediaPlayer != null) { @@ -131,55 +130,46 @@ public class MediaSessionKeyEventTest { session.release(); } - private void dispatchMediaKeyEvent(int keyCode, boolean doubleTap) { - audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); - audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); - if (doubleTap) { - audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); - audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); - } - } - @Test public void playKeyEvent() throws Exception { dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY, false); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.playCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); } @Test public void pauseKeyEvent() throws Exception { dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PAUSE, false); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.pauseCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS); } @Test public void nextKeyEvent() throws Exception { dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT, false); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToNextCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS); } @Test public void previousKeyEvent() throws Exception { dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS, false); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToPreviousCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS); } @Test public void stopKeyEvent() throws Exception { dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP, false); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.stopCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS); } @Test public void playPauseKeyEvent_play() throws Exception { dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.playCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); } @Test @@ -188,18 +178,28 @@ public class MediaSessionKeyEventTest { () -> { player.playWhenReady = true; }); + dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.pauseCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS); } @Test public void playPauseKeyEvent_doubleTapIsTranslatedToSkipToNext() throws Exception { dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, true); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToNextCalled).isTrue(); - assertThat(player.playCalled).isFalse(); - assertThat(player.pauseCalled).isFalse(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PAUSE)).isFalse(); + } + + private void dispatchMediaKeyEvent(int keyCode, boolean doubleTap) { + audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); + if (doubleTap) { + audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); + } } private static class TestSessionCallback implements MediaSession.SessionCallback { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java index 2213f0700c..f0f129a49f 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java @@ -88,7 +88,7 @@ public class MediaSessionPermissionTest { } @After - public void cleanUp() { + public void tearDown() { if (session != null) { session.release(); session = null; @@ -97,55 +97,6 @@ public class MediaSessionPermissionTest { callback = null; } - private void createSessionWithAvailableCommands( - SessionCommands sessionCommands, Player.Commands playerCommands) { - player = - new MockPlayer.Builder() - .setLatchCount(1) - .setApplicationLooper(threadTestRule.getHandler().getLooper()) - .build(); - callback = - new MySessionCallback() { - @Override - public MediaSession.ConnectionResult onConnect( - MediaSession session, ControllerInfo controller) { - if (!TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())) { - return MediaSession.ConnectionResult.reject(); - } - return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands); - } - }; - if (this.session != null) { - this.session.release(); - } - this.session = - new MediaSession.Builder(context, player) - .setId(SESSION_ID) - .setSessionCallback(callback) - .build(); - } - - private SessionCommands createSessionCommandsWith(SessionCommand command) { - return new SessionCommands.Builder().add(command).build(); - } - - private void testOnCommandRequest(int commandCode, PermissionTestTask runnable) throws Exception { - createSessionWithAvailableCommands( - SessionCommands.EMPTY, createPlayerCommandsWith(commandCode)); - runnable.run(controllerTestRule.createRemoteController(session.getToken())); - - assertThat(callback.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(callback.onCommandRequestCalled).isTrue(); - assertThat(callback.command).isEqualTo(commandCode); - - createSessionWithAvailableCommands( - SessionCommands.EMPTY, createPlayerCommandsWithout(commandCode)); - runnable.run(controllerTestRule.createRemoteController(session.getToken())); - - assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); - assertThat(callback.onCommandRequestCalled).isFalse(); - } - @Test public void play() throws Exception { testOnCommandRequest(COMMAND_PLAY_PAUSE, RemoteMediaController::play); @@ -409,4 +360,52 @@ public class MediaSessionPermissionTest { return Futures.immediateFuture(new SessionResult(RESULT_SUCCESS)); } } + + private void createSessionWithAvailableCommands( + SessionCommands sessionCommands, Player.Commands playerCommands) { + player = + new MockPlayer.Builder() + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .build(); + callback = + new MySessionCallback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, ControllerInfo controller) { + if (!TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())) { + return MediaSession.ConnectionResult.reject(); + } + return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands); + } + }; + if (this.session != null) { + this.session.release(); + } + this.session = + new MediaSession.Builder(context, player) + .setId(SESSION_ID) + .setSessionCallback(callback) + .build(); + } + + private SessionCommands createSessionCommandsWith(SessionCommand command) { + return new SessionCommands.Builder().add(command).build(); + } + + private void testOnCommandRequest(int commandCode, PermissionTestTask runnable) throws Exception { + createSessionWithAvailableCommands( + SessionCommands.EMPTY, createPlayerCommandsWith(commandCode)); + runnable.run(controllerTestRule.createRemoteController(session.getToken())); + + assertThat(callback.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(callback.onCommandRequestCalled).isTrue(); + assertThat(callback.command).isEqualTo(commandCode); + + createSessionWithAvailableCommands( + SessionCommands.EMPTY, createPlayerCommandsWithout(commandCode)); + runnable.run(controllerTestRule.createRemoteController(session.getToken())); + + assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); + assertThat(callback.onCommandRequestCalled).isFalse(); + } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java index 68dea9d90d..442a84f3c2 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java @@ -16,10 +16,8 @@ package androidx.media3.session; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; -import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; -import static java.util.concurrent.TimeUnit.MILLISECONDS; import androidx.media3.common.DeviceInfo; import androidx.media3.common.MediaItem; @@ -63,7 +61,6 @@ public class MediaSessionPlayerTest { public void setUp() throws Exception { player = new MockPlayer.Builder() - .setLatchCount(1) .setApplicationLooper(threadTestRule.getHandler().getLooper()) .setMediaItems(/* itemCount= */ 5) .build(); @@ -87,62 +84,64 @@ public class MediaSessionPlayerTest { } @After - public void cleanUp() { - if (session != null) { - session.release(); - } + public void tearDown() throws Exception { + controller.release(); + session.release(); } @Test public void play() throws Exception { controller.play(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.playCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); } @Test public void pause() throws Exception { controller.pause(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.pauseCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS); } @Test public void prepare() throws Exception { controller.prepare(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.prepareCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); } @Test public void stop() throws Exception { controller.stop(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.stopCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS); } @Test public void setPlayWhenReady() throws Exception { boolean testPlayWhenReady = true; + controller.setPlayWhenReady(testPlayWhenReady); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setPlayWhenReadyCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_PLAY_WHEN_READY, TIMEOUT_MS); assertThat(player.playWhenReady).isEqualTo(testPlayWhenReady); } @Test public void seekToDefaultPosition() throws Exception { controller.seekToDefaultPosition(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToDefaultPositionCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION, TIMEOUT_MS); } @Test public void seekToDefaultPosition_withMediaItemIndex() throws Exception { int mediaItemIndex = 3; + controller.seekToDefaultPosition(mediaItemIndex); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToDefaultPositionWithMediaItemIndexCalled).isTrue(); + + player.awaitMethodCalled( + MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS); assertThat(player.seekMediaItemIndex).isEqualTo(mediaItemIndex); } @@ -150,8 +149,8 @@ public class MediaSessionPlayerTest { public void seekTo() throws Exception { long seekPositionMs = 12125L; controller.seekTo(seekPositionMs); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO, TIMEOUT_MS); assertThat(player.seekPositionMs).isEqualTo(seekPositionMs); } @@ -159,9 +158,10 @@ public class MediaSessionPlayerTest { public void seekTo_withMediaItemIndex() throws Exception { int mediaItemIndex = 3; long seekPositionMs = 12125L; + controller.seekTo(mediaItemIndex, seekPositionMs); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToWithMediaItemIndexCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS); assertThat(player.seekMediaItemIndex).isEqualTo(mediaItemIndex); assertThat(player.seekPositionMs).isEqualTo(seekPositionMs); } @@ -169,8 +169,10 @@ public class MediaSessionPlayerTest { @Test public void setPlaybackSpeed() throws Exception { float testSpeed = 1.5f; + controller.setPlaybackSpeed(testSpeed); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_PLAYBACK_SPEED, TIMEOUT_MS); assertThat(player.playbackParameters.speed).isEqualTo(testSpeed); } @@ -178,9 +180,10 @@ public class MediaSessionPlayerTest { public void setPlaybackParameters() throws Exception { PlaybackParameters testPlaybackParameters = new PlaybackParameters(/* speed= */ 1.4f, /* pitch= */ 2.3f); + controller.setPlaybackParameters(testPlaybackParameters); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setPlaybackParametersCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_PLAYBACK_PARAMETERS, TIMEOUT_MS); assertThat(player.playbackParameters).isEqualTo(testPlaybackParameters); } @@ -194,8 +197,7 @@ public class MediaSessionPlayerTest { controller.setMediaItem(item); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setMediaItemCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEM, TIMEOUT_MS); assertThat(player.mediaItem).isEqualTo(item); assertThat(player.startPositionMs).isEqualTo(startPositionMs); assertThat(player.resetPosition).isEqualTo(resetPosition); @@ -211,8 +213,7 @@ public class MediaSessionPlayerTest { controller.setMediaItem(item); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setMediaItemCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEM, TIMEOUT_MS); assertThat(player.mediaItem).isEqualTo(item); assertThat(player.startPositionMs).isEqualTo(startPositionMs); assertThat(player.resetPosition).isEqualTo(resetPosition); @@ -228,8 +229,7 @@ public class MediaSessionPlayerTest { controller.setMediaItem(item); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setMediaItemCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEM, TIMEOUT_MS); assertThat(player.mediaItem).isEqualTo(item); assertThat(player.startPositionMs).isEqualTo(startPositionMs); assertThat(player.resetPosition).isEqualTo(resetPosition); @@ -241,8 +241,7 @@ public class MediaSessionPlayerTest { controller.setMediaItems(items); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setMediaItemsCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); assertThat(player.mediaItems).isEqualTo(items); assertThat(player.resetPosition).isFalse(); } @@ -253,8 +252,7 @@ public class MediaSessionPlayerTest { controller.setMediaItems(items, /* resetPosition= */ true); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setMediaItemsWithResetPositionCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(player.mediaItems).isEqualTo(items); assertThat(player.resetPosition).isTrue(); } @@ -267,8 +265,7 @@ public class MediaSessionPlayerTest { controller.setMediaItems(items, startMediaItemIndex, startPositionMs); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setMediaItemsWithStartIndexCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); assertThat(player.mediaItems).isEqualTo(items); assertThat(player.startMediaItemIndex).isEqualTo(startMediaItemIndex); assertThat(player.startPositionMs).isEqualTo(startPositionMs); @@ -279,9 +276,10 @@ public class MediaSessionPlayerTest { int listSize = 4; List list = MediaTestUtils.createMediaItems(listSize); list.set(2, list.get(1)); + controller.setMediaItems(list); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setMediaItemsCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); assertThat(player.mediaItems.size()).isEqualTo(listSize); for (int i = 0; i < listSize; i++) { assertThat(player.mediaItems.get(i).mediaId).isEqualTo(list.get(i).mediaId); @@ -293,9 +291,8 @@ public class MediaSessionPlayerTest { int listSize = 5000; // Make client app to generate a long list, and call setMediaItems() with it. controller.createAndSetFakeMediaItems(listSize); - assertThat(player.countDownLatch.await(LONG_TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setMediaItemsCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); assertThat(player.mediaItems).isNotNull(); assertThat(player.mediaItems.size()).isEqualTo(listSize); for (int i = 0; i < listSize; i++) { @@ -310,8 +307,7 @@ public class MediaSessionPlayerTest { controller.setPlaylistMetadata(playlistMetadata); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setPlaylistMetadataCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_PLAYLIST_METADATA, TIMEOUT_MS); assertThat(player.playlistMetadata).isEqualTo(playlistMetadata); } @@ -321,8 +317,7 @@ public class MediaSessionPlayerTest { controller.addMediaItem(mediaItem); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.addMediaItemCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM, TIMEOUT_MS); assertThat(player.mediaItem).isEqualTo(mediaItem); } @@ -333,8 +328,7 @@ public class MediaSessionPlayerTest { controller.addMediaItem(index, mediaItem); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.addMediaItemWithIndexCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM_WITH_INDEX, TIMEOUT_MS); assertThat(player.index).isEqualTo(index); assertThat(player.mediaItem).isEqualTo(mediaItem); } @@ -346,8 +340,7 @@ public class MediaSessionPlayerTest { controller.addMediaItems(mediaItems); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.addMediaItemsCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS); assertThat(player.mediaItems).isEqualTo(mediaItems); } @@ -359,8 +352,7 @@ public class MediaSessionPlayerTest { controller.addMediaItems(index, mediaItems); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.addMediaItemsWithIndexCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, TIMEOUT_MS); assertThat(player.index).isEqualTo(index); assertThat(player.mediaItems).isEqualTo(mediaItems); } @@ -371,8 +363,7 @@ public class MediaSessionPlayerTest { controller.removeMediaItem(index); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.removeMediaItemCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_REMOVE_MEDIA_ITEM, TIMEOUT_MS); assertThat(player.index).isEqualTo(index); } @@ -383,8 +374,7 @@ public class MediaSessionPlayerTest { controller.removeMediaItems(fromIndex, toIndex); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.removeMediaItemsCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_REMOVE_MEDIA_ITEMS, TIMEOUT_MS); assertThat(player.fromIndex).isEqualTo(fromIndex); assertThat(player.toIndex).isEqualTo(toIndex); } @@ -393,8 +383,7 @@ public class MediaSessionPlayerTest { public void clearMediaItems() throws Exception { controller.clearMediaItems(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.clearMediaItemsCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_CLEAR_MEDIA_ITEMS, TIMEOUT_MS); } @Test @@ -404,8 +393,7 @@ public class MediaSessionPlayerTest { controller.moveMediaItem(index, newIndex); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.moveMediaItemCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_MOVE_MEDIA_ITEM, TIMEOUT_MS); assertThat(player.index).isEqualTo(index); assertThat(player.newIndex).isEqualTo(newIndex); } @@ -418,8 +406,7 @@ public class MediaSessionPlayerTest { controller.moveMediaItems(fromIndex, toIndex, newIndex); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.moveMediaItemsCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_MOVE_MEDIA_ITEMS, TIMEOUT_MS); assertThat(player.fromIndex).isEqualTo(fromIndex); assertThat(player.toIndex).isEqualTo(toIndex); assertThat(player.newIndex).isEqualTo(newIndex); @@ -428,68 +415,69 @@ public class MediaSessionPlayerTest { @Test public void seekToPreviousMediaItem() throws Exception { controller.seekToPreviousMediaItem(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToPreviousMediaItemCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS_MEDIA_ITEM, TIMEOUT_MS); } @Test public void seekToNextMediaItem() throws Exception { controller.seekToNextMediaItem(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToNextMediaItemCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT_MEDIA_ITEM, TIMEOUT_MS); } @Test public void seekToPrevious() throws Exception { controller.seekToPrevious(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToPreviousCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS); } @Test public void seekToNext() throws Exception { controller.seekToNext(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToNextCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS); } @Test public void setShuffleModeEnabled() throws Exception { boolean testShuffleModeEnabled = true; - controller.setShuffleModeEnabled(testShuffleModeEnabled); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setShuffleModeCalled).isTrue(); + controller.setShuffleModeEnabled(testShuffleModeEnabled); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_SHUFFLE_MODE, TIMEOUT_MS); assertThat(player.shuffleModeEnabled).isEqualTo(testShuffleModeEnabled); } @Test public void setRepeatMode() throws Exception { int testRepeatMode = Player.REPEAT_MODE_ALL; - controller.setRepeatMode(testRepeatMode); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setRepeatModeCalled).isTrue(); + controller.setRepeatMode(testRepeatMode); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_REPEAT_MODE, TIMEOUT_MS); assertThat(player.repeatMode).isEqualTo(testRepeatMode); } @Test public void setVolume() throws Exception { float testVolume = .123f; + controller.setVolume(testVolume); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setVolumeCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_VOLUME, TIMEOUT_MS); assertThat(player.volume).isEqualTo(testVolume); } @Test public void setDeviceVolume() throws Exception { changePlaybackTypeToRemote(); - int testVolume = 12; + controller.setDeviceVolume(testVolume); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setDeviceVolumeCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_DEVICE_VOLUME, TIMEOUT_MS); assertThat(player.deviceVolume).isEqualTo(testVolume); } @@ -499,8 +487,7 @@ public class MediaSessionPlayerTest { controller.increaseDeviceVolume(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.increaseDeviceVolumeCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_INCREASE_DEVICE_VOLUME, TIMEOUT_MS); } @Test @@ -509,16 +496,16 @@ public class MediaSessionPlayerTest { controller.decreaseDeviceVolume(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.decreaseDeviceVolumeCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_DECREASE_DEVICE_VOLUME, TIMEOUT_MS); } @Test public void setDeviceMuted() throws Exception { player.deviceMuted = false; + controller.setDeviceMuted(true); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setDeviceMutedCalled).isTrue(); + + player.awaitMethodCalled(MockPlayer.METHOD_SET_DEVICE_MUTED, TIMEOUT_MS); assertThat(player.deviceMuted).isTrue(); } @@ -526,16 +513,14 @@ public class MediaSessionPlayerTest { public void seekBack() throws Exception { controller.seekBack(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekBackCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_BACK, TIMEOUT_MS); } @Test public void seekForward() throws Exception { controller.seekForward(); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekForwardCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_FORWARD, TIMEOUT_MS); } @Test @@ -545,8 +530,7 @@ public class MediaSessionPlayerTest { controller.setTrackSelectionParameters(trackSelectionParameters); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.setTrackSelectionParametersCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_TRACK_SELECTION_PARAMETERS, TIMEOUT_MS); assertThat(player.trackSelectionParameters).isEqualTo(trackSelectionParameters); } 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 1460c42d6b..e79f809572 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 @@ -47,6 +47,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; @@ -78,8 +79,7 @@ public class MediaSessionTest { public void setUp() throws Exception { context = ApplicationProvider.getApplicationContext(); handler = threadTestRule.getHandler(); - player = - new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); session = sessionTestRule.ensureReleaseAfterTest( @@ -107,6 +107,16 @@ public class MediaSessionTest { .get(TIMEOUT_MS, MILLISECONDS); } + @After + public void tearDown() throws Exception { + if ((controller != null)) { + threadTestRule.getHandler().postAndSync(() -> controller.release()); + } + if (session != null) { + session.release(); + } + } + @Test public void builder() { MediaSession.Builder builder; @@ -394,8 +404,7 @@ public class MediaSessionTest { long testSeekPositionMs = 1234; controllerCompat.getTransportControls().seekTo(testSeekPositionMs); - assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(player.seekToCalled).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO, TIMEOUT_MS); assertThat(player.seekPositionMs).isEqualTo(testSeekPositionMs); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MockPlayerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MockPlayerTest.java index 8fc8b456ed..f97a659fe3 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MockPlayerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MockPlayerTest.java @@ -44,81 +44,98 @@ public class MockPlayerTest { @Test public void play() { player.play(); - assertThat(player.playCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isTrue(); } @Test public void pause() { player.pause(); - assertThat(player.pauseCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PAUSE)).isTrue(); } @Test public void prepare() { player.prepare(); - assertThat(player.prepareCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); } @Test public void stop() { player.stop(); - assertThat(player.stopCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_STOP)).isTrue(); } @Test public void release() { player.release(); - assertThat(player.releaseCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_RELEASE)).isTrue(); } @Test public void setPlayWhenReady() { boolean testPlayWhenReady = false; + player.setPlayWhenReady(testPlayWhenReady); - assertThat(player.setPlayWhenReadyCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_PLAY_WHEN_READY)).isTrue(); } @Test public void seekTo() { long pos = 1004L; + player.seekTo(pos); - assertThat(player.seekToCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO)).isTrue(); assertThat(player.seekPositionMs).isEqualTo(pos); } @Test public void seekBack() { player.seekBack(); - assertThat(player.seekBackCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_BACK)).isTrue(); } @Test public void seekForward() { player.seekForward(); - assertThat(player.seekForwardCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_FORWARD)).isTrue(); } @Test public void setPlaybackParameters() { PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 1.5f); + player.setPlaybackParameters(playbackParameters); - assertThat(player.setPlaybackParametersCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_PLAYBACK_PARAMETERS)).isTrue(); assertThat(player.playbackParameters).isEqualTo(playbackParameters); } @Test public void setPlaybackSpeed() { float speed = 1.5f; + player.setPlaybackSpeed(speed); - assertThat(player.setPlaybackSpeedCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_PLAYBACK_SPEED)).isTrue(); assertThat(player.playbackParameters.speed).isEqualTo(speed); } @Test public void setMediaItem() { MediaItem mediaItem = MediaTestUtils.createMediaItem("setMediaItem"); + player.setMediaItem(mediaItem); - assertThat(player.setMediaItemCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEM)).isTrue(); assertThat(player.mediaItem).isSameInstanceAs(mediaItem); } @@ -126,8 +143,11 @@ public class MockPlayerTest { public void setMediaItem_withStartPosition() { MediaItem mediaItem = MediaTestUtils.createMediaItem("setMediaItem"); long startPositionMs = 321L; + player.setMediaItem(mediaItem, startPositionMs); - assertThat(player.setMediaItemWithStartPositionCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEM_WITH_START_POSITION)) + .isTrue(); assertThat(player.startPositionMs).isEqualTo(startPositionMs); assertThat(player.mediaItem).isSameInstanceAs(mediaItem); } @@ -136,8 +156,11 @@ public class MockPlayerTest { public void setMediaItem_withResetPosition() { MediaItem mediaItem = MediaTestUtils.createMediaItem("setMediaItem"); boolean resetPosition = true; + player.setMediaItem(mediaItem, resetPosition); - assertThat(player.setMediaItemWithResetPositionCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEM_WITH_RESET_POSITION)) + .isTrue(); assertThat(player.resetPosition).isEqualTo(resetPosition); assertThat(player.mediaItem).isEqualTo(mediaItem); } @@ -145,8 +168,10 @@ public class MockPlayerTest { @Test public void setMediaItems() { List list = MediaTestUtils.createMediaItems(/* size= */ 2); + player.setMediaItems(list); - assertThat(player.setMediaItemsCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS)).isTrue(); assertThat(player.mediaItems).isSameInstanceAs(list); } @@ -154,8 +179,11 @@ public class MockPlayerTest { public void setMediaItems_withResetPosition() { List list = MediaTestUtils.createMediaItems(/* size= */ 2); boolean resetPosition = true; + player.setMediaItems(list, resetPosition); - assertThat(player.setMediaItemsWithResetPositionCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION)) + .isTrue(); assertThat(player.resetPosition).isEqualTo(resetPosition); assertThat(player.mediaItems).isSameInstanceAs(list); } @@ -165,8 +193,11 @@ public class MockPlayerTest { List list = MediaTestUtils.createMediaItems(/* size= */ 2); int startWindowIndex = 3; long startPositionMs = 132L; + player.setMediaItems(list, startWindowIndex, startPositionMs); - assertThat(player.setMediaItemsWithStartIndexCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)) + .isTrue(); assertThat(player.startMediaItemIndex).isEqualTo(startWindowIndex); assertThat(player.startPositionMs).isEqualTo(startPositionMs); assertThat(player.mediaItems).isSameInstanceAs(list); @@ -176,8 +207,10 @@ public class MockPlayerTest { public void setMediaItems_withDuplicatedItems() { List list = MediaTestUtils.createMediaItems(/* size= */ 4); list.set(2, list.get(1)); + player.setMediaItems(list); - assertThat(player.setMediaItemsCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS)).isTrue(); assertThat(player.mediaItems).isSameInstanceAs(list); } @@ -187,7 +220,7 @@ public class MockPlayerTest { player.setPlaylistMetadata(playlistMetadata); - assertThat(player.setPlaylistMetadataCalled).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_PLAYLIST_METADATA)).isTrue(); assertThat(player.playlistMetadata).isSameInstanceAs(playlistMetadata); } @@ -197,7 +230,7 @@ public class MockPlayerTest { player.addMediaItem(mediaItem); - assertThat(player.addMediaItemCalled).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM)).isTrue(); assertThat(player.mediaItem).isSameInstanceAs(mediaItem); } @@ -208,7 +241,7 @@ public class MockPlayerTest { player.addMediaItem(index, mediaItem); - assertThat(player.addMediaItemWithIndexCalled).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM_WITH_INDEX)).isTrue(); assertThat(player.index).isEqualTo(index); assertThat(player.mediaItem).isSameInstanceAs(mediaItem); } @@ -221,7 +254,7 @@ public class MockPlayerTest { player.addMediaItems(index, mediaItems); - assertThat(player.addMediaItemsWithIndexCalled).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX)).isTrue(); assertThat(player.index).isEqualTo(index); assertThat(player.mediaItems).isSameInstanceAs(mediaItems); } @@ -234,7 +267,7 @@ public class MockPlayerTest { player.addMediaItems(index, mediaItems); - assertThat(player.addMediaItemsWithIndexCalled).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX)).isTrue(); assertThat(player.index).isEqualTo(index); assertThat(player.mediaItems).isSameInstanceAs(mediaItems); } @@ -245,7 +278,7 @@ public class MockPlayerTest { player.removeMediaItem(index); - assertThat(player.removeMediaItemCalled).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_REMOVE_MEDIA_ITEM)).isTrue(); assertThat(player.index).isEqualTo(index); } @@ -256,7 +289,7 @@ public class MockPlayerTest { player.removeMediaItems(fromIndex, toIndex); - assertThat(player.removeMediaItemsCalled).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_REMOVE_MEDIA_ITEMS)).isTrue(); assertThat(player.fromIndex).isEqualTo(fromIndex); assertThat(player.toIndex).isEqualTo(toIndex); } @@ -265,7 +298,7 @@ public class MockPlayerTest { public void clearMediaItems() { player.clearMediaItems(); - assertThat(player.clearMediaItemsCalled).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_CLEAR_MEDIA_ITEMS)).isTrue(); } @Test @@ -275,7 +308,7 @@ public class MockPlayerTest { player.moveMediaItem(index, newIndex); - assertThat(player.moveMediaItemCalled).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_MOVE_MEDIA_ITEM)).isTrue(); assertThat(player.index).isEqualTo(index); assertThat(player.newIndex).isEqualTo(newIndex); } @@ -288,7 +321,7 @@ public class MockPlayerTest { player.moveMediaItems(fromIndex, toIndex, newIndex); - assertThat(player.moveMediaItemsCalled).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_MOVE_MEDIA_ITEMS)).isTrue(); assertThat(player.fromIndex).isEqualTo(fromIndex); assertThat(player.toIndex).isEqualTo(toIndex); assertThat(player.newIndex).isEqualTo(newIndex); @@ -297,76 +330,92 @@ public class MockPlayerTest { @Test public void seekToPreviousMediaItem() { player.seekToPreviousMediaItem(); - assertThat(player.seekToPreviousMediaItemCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS_MEDIA_ITEM)).isTrue(); } @Test public void seekToNextMediaItem() { player.seekToNextMediaItem(); - assertThat(player.seekToNextMediaItemCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_NEXT_MEDIA_ITEM)).isTrue(); } @Test public void seekToPrevious() { player.seekToPrevious(); - assertThat(player.seekToPreviousCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS)).isTrue(); } @Test public void seekToNext() { player.seekToNext(); - assertThat(player.seekToNextCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_NEXT)).isTrue(); } @Test public void setShuffleModeEnabled() { boolean testShuffleModeEnabled = true; + player.setShuffleModeEnabled(testShuffleModeEnabled); - assertThat(player.setShuffleModeCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_SHUFFLE_MODE)).isTrue(); assertThat(player.shuffleModeEnabled).isEqualTo(testShuffleModeEnabled); } @Test public void setRepeatMode() { int testRepeatMode = Player.REPEAT_MODE_ALL; + player.setRepeatMode(testRepeatMode); - assertThat(player.setRepeatModeCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_REPEAT_MODE)).isTrue(); assertThat(player.repeatMode).isEqualTo(testRepeatMode); } @Test public void setVolume() { float testVolume = .123f; + player.setVolume(testVolume); - assertThat(player.setVolumeCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_VOLUME)).isTrue(); assertThat(player.volume).isEqualTo(testVolume); } @Test public void setDeviceVolume() { int testVolume = 12; + player.setDeviceVolume(testVolume); - assertThat(player.setDeviceVolumeCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_DEVICE_VOLUME)).isTrue(); assertThat(player.deviceVolume).isEqualTo(testVolume); } @Test public void increaseDeviceVolume() { player.increaseDeviceVolume(); - assertThat(player.increaseDeviceVolumeCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_INCREASE_DEVICE_VOLUME)).isTrue(); } @Test public void decreaseDeviceVolume() { player.decreaseDeviceVolume(); - assertThat(player.decreaseDeviceVolumeCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_DECREASE_DEVICE_VOLUME)).isTrue(); } @Test public void setDeviceMuted() { player.deviceMuted = false; + player.setDeviceMuted(true); - assertThat(player.setDeviceMutedCalled).isTrue(); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_DEVICE_MUTED)).isTrue(); assertThat(player.deviceMuted).isTrue(); } @@ -377,7 +426,8 @@ public class MockPlayerTest { player.setTrackSelectionParameters(trackSelectionParameters); - assertThat(player.setTrackSelectionParametersCalled).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_TRACK_SELECTION_PARAMETERS)) + .isTrue(); assertThat(player.trackSelectionParameters).isSameInstanceAs(trackSelectionParameters); } } diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java index 8a4ec96bee..49550bbb05 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java @@ -16,6 +16,8 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT; +import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT; import static androidx.media3.session.MediaTestUtils.assertLibraryParamsEquals; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION; @@ -30,6 +32,8 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID import static androidx.media3.test.session.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_EXTRAS; import static androidx.media3.test.session.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_ITEM_COUNT; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID; +import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR; +import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS; @@ -47,8 +51,10 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIB import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import android.app.PendingIntent; import android.app.Service; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.os.HandlerThread; import androidx.annotation.GuardedBy; @@ -134,8 +140,7 @@ public class MockMediaLibraryService extends MediaLibraryService { return (MediaLibrarySession) onGetSessionHandler.onGetSession(controllerInfo); } - MockPlayer player = - new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build(); + MockPlayer player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); MediaLibrarySessionCallback callback = registry.getSessionCallback(); session = @@ -233,6 +238,21 @@ public class MockMediaLibraryService extends MediaLibraryService { return Futures.immediateFuture(LibraryResult.ofItemList(list, params)); } else if (PARENT_ID_ERROR.equals(parentId)) { return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } else if (PARENT_ID_AUTH_EXPIRED_ERROR.equals(parentId)) { + Bundle bundle = new Bundle(); + Intent signInIntent = new Intent("action"); + int flags = Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0; + bundle.putParcelable( + EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT, + PendingIntent.getActivity( + getApplicationContext(), /* requestCode= */ 0, signInIntent, flags)); + bundle.putString( + EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT, + PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL); + return Futures.immediateFuture( + LibraryResult.ofError( + LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, + new LibraryParams.Builder().setExtras(bundle).build())); } // Includes the case of PARENT_ID_NO_CHILDREN. return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), params)); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java index 416365e414..69ee983771 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java @@ -15,11 +15,15 @@ */ package androidx.media3.session; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static java.lang.annotation.ElementType.TYPE_USE; + import android.os.Looper; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.collection.ArraySet; import androidx.media3.common.AudioAttributes; @@ -31,26 +35,166 @@ import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; import androidx.media3.common.text.Cue; +import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.List; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeoutException; /** A mock implementation of {@link Player} for testing. */ @UnstableApi public class MockPlayer implements Player { - public final CountDownLatch countDownLatch; + /** Player methods. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + METHOD_ADD_MEDIA_ITEM, + METHOD_ADD_MEDIA_ITEMS, + METHOD_ADD_MEDIA_ITEM_WITH_INDEX, + METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, + METHOD_CLEAR_MEDIA_ITEMS, + METHOD_DECREASE_DEVICE_VOLUME, + METHOD_INCREASE_DEVICE_VOLUME, + METHOD_MOVE_MEDIA_ITEM, + METHOD_MOVE_MEDIA_ITEMS, + METHOD_PAUSE, + METHOD_PLAY, + METHOD_PREPARE, + METHOD_RELEASE, + METHOD_REMOVE_MEDIA_ITEM, + METHOD_REMOVE_MEDIA_ITEMS, + METHOD_SEEK_BACK, + METHOD_SEEK_FORWARD, + METHOD_SEEK_TO, + METHOD_SEEK_TO_DEFAULT_POSITION, + METHOD_SEEK_TO_DEFAULT_POSITION_WITH_MEDIA_ITEM_INDEX, + METHOD_SEEK_TO_NEXT, + METHOD_SEEK_TO_NEXT_MEDIA_ITEM, + METHOD_SEEK_TO_PREVIOUS, + METHOD_SEEK_TO_PREVIOUS_MEDIA_ITEM, + METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX, + METHOD_SET_DEVICE_MUTED, + METHOD_SET_DEVICE_VOLUME, + METHOD_SET_MEDIA_ITEM, + METHOD_SET_MEDIA_ITEM_WITH_RESET_POSITION, + METHOD_SET_MEDIA_ITEM_WITH_START_POSITION, + METHOD_SET_MEDIA_ITEMS, + METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, + METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, + METHOD_SET_PLAY_WHEN_READY, + METHOD_SET_PLAYBACK_PARAMETERS, + METHOD_SET_PLAYBACK_SPEED, + METHOD_SET_PLAYLIST_METADATA, + METHOD_SET_REPEAT_MODE, + METHOD_SET_SHUFFLE_MODE, + METHOD_SET_TRACK_SELECTION_PARAMETERS, + METHOD_SET_VOLUME, + METHOD_STOP + }) + public @interface Method {} + + /** Maps to {@link Player#addMediaItem(MediaItem)}. */ + public static final int METHOD_ADD_MEDIA_ITEM = 0; + /** Maps to {@link Player#addMediaItems(List)}. */ + public static final int METHOD_ADD_MEDIA_ITEMS = 1; + /** Maps to {@link Player#addMediaItem(int, MediaItem)}. */ + public static final int METHOD_ADD_MEDIA_ITEM_WITH_INDEX = 2; + /** Maps to {@link Player#addMediaItems(int, List)}. */ + public static final int METHOD_ADD_MEDIA_ITEMS_WITH_INDEX = 3; + /** Maps to {@link Player#clearMediaItems()}. */ + public static final int METHOD_CLEAR_MEDIA_ITEMS = 4; + /** Maps to {@link Player#decreaseDeviceVolume()}. */ + public static final int METHOD_DECREASE_DEVICE_VOLUME = 5; + /** Maps to {@link Player#increaseDeviceVolume()}. */ + public static final int METHOD_INCREASE_DEVICE_VOLUME = 6; + /** Maps to {@link Player#moveMediaItem(int, int)}. */ + public static final int METHOD_MOVE_MEDIA_ITEM = 7; + /** Maps to {@link Player#moveMediaItems(int, int, int)}. */ + public static final int METHOD_MOVE_MEDIA_ITEMS = 8; + /** Maps to {@link Player#pause()}. */ + public static final int METHOD_PAUSE = 9; + /** Maps to {@link Player#play()}. */ + public static final int METHOD_PLAY = 10; + /** Maps to {@link Player#prepare()}. */ + public static final int METHOD_PREPARE = 11; + /** Maps to {@link Player#release()}. */ + public static final int METHOD_RELEASE = 12; + /** Maps to {@link Player#removeMediaItem(int)}. */ + public static final int METHOD_REMOVE_MEDIA_ITEM = 13; + /** Maps to {@link Player#removeMediaItems(int, int)}. */ + public static final int METHOD_REMOVE_MEDIA_ITEMS = 14; + /** Maps to {@link Player#seekBack()}. */ + public static final int METHOD_SEEK_BACK = 15; + /** Maps to {@link Player#seekForward()}. */ + public static final int METHOD_SEEK_FORWARD = 16; + /** Maps to {@link Player#seekTo(long)}. */ + public static final int METHOD_SEEK_TO = 17; + /** Maps to {@link Player#seekToDefaultPosition()}. */ + public static final int METHOD_SEEK_TO_DEFAULT_POSITION = 18; + /** Maps to {@link Player#seekToDefaultPosition(int)}. */ + public static final int METHOD_SEEK_TO_DEFAULT_POSITION_WITH_MEDIA_ITEM_INDEX = 19; + /** Maps to {@link Player#seekToNext()}. */ + public static final int METHOD_SEEK_TO_NEXT = 20; + /** Maps to {@link Player#seekToNextMediaItem()}. */ + public static final int METHOD_SEEK_TO_NEXT_MEDIA_ITEM = 21; + /** Maps to {@link Player#seekToPrevious()}. */ + public static final int METHOD_SEEK_TO_PREVIOUS = 22; + /** Maps to {@link Player#seekToPreviousMediaItem()}. */ + public static final int METHOD_SEEK_TO_PREVIOUS_MEDIA_ITEM = 23; + /** Maps to {@link Player#seekTo(int, long)}. */ + public static final int METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX = 24; + /** Maps to {@link Player#setDeviceMuted(boolean)}. */ + public static final int METHOD_SET_DEVICE_MUTED = 25; + /** Maps to {@link Player#setDeviceVolume(int)}. */ + public static final int METHOD_SET_DEVICE_VOLUME = 26; + /** Maps to {@link Player#setMediaItem(MediaItem)}. */ + public static final int METHOD_SET_MEDIA_ITEM = 27; + /** Maps to {@link Player#setMediaItem(MediaItem, boolean)}. */ + public static final int METHOD_SET_MEDIA_ITEM_WITH_RESET_POSITION = 28; + /** Maps to {@link Player#setMediaItem(MediaItem, long)}. */ + public static final int METHOD_SET_MEDIA_ITEM_WITH_START_POSITION = 29; + /** Maps to {@link Player#setMediaItems(List)}. */ + public static final int METHOD_SET_MEDIA_ITEMS = 30; + /** Maps to {@link Player#setMediaItems(List, boolean)}. */ + public static final int METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION = 31; + /** Maps to {@link Player#setMediaItems(List, int, long)}. */ + public static final int METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX = 32; + /** Maps to {@link Player#setPlayWhenReady(boolean)}. */ + public static final int METHOD_SET_PLAY_WHEN_READY = 33; + /** Maps to {@link Player#setPlaybackParameters(PlaybackParameters)}. */ + public static final int METHOD_SET_PLAYBACK_PARAMETERS = 34; + /** Maps to {@link Player#setPlaybackSpeed(float)}. */ + public static final int METHOD_SET_PLAYBACK_SPEED = 35; + /** Maps to {@link Player#setPlaylistMetadata(MediaMetadata)}. */ + public static final int METHOD_SET_PLAYLIST_METADATA = 36; + /** Maps to {@link Player#setRepeatMode(int)}. */ + public static final int METHOD_SET_REPEAT_MODE = 37; + /** Maps to {@link Player#setShuffleModeEnabled(boolean)}. */ + public static final int METHOD_SET_SHUFFLE_MODE = 38; + /** Maps to {@link Player#setTrackSelectionParameters(TrackSelectionParameters)}. */ + public static final int METHOD_SET_TRACK_SELECTION_PARAMETERS = 39; + /** Maps to {@link Player#setVolume(float)}. */ + public static final int METHOD_SET_VOLUME = 40; + /** Maps to {@link Player#stop()}. */ + public static final int METHOD_STOP = 41; + private final boolean changePlayerStateWithTransportControl; private final Looper applicationLooper; private final ArraySet listeners = new ArraySet<>(); + private final ImmutableMap<@Method Integer, ConditionVariable> conditionVariables = + createMethodConditionVariables(); @Nullable PlaybackException playerError; public AudioAttributes audioAttributes; @@ -106,51 +250,7 @@ public class MockPlayer implements Player { public long maxSeekToPreviousPositionMs; public TrackSelectionParameters trackSelectionParameters; - public boolean playCalled; - public boolean pauseCalled; - public boolean prepareCalled; - public boolean stopCalled; - public boolean releaseCalled; - public boolean seekToDefaultPositionCalled; - public boolean seekToDefaultPositionWithMediaItemIndexCalled; - public boolean seekToCalled; - public boolean seekToWithMediaItemIndexCalled; - public boolean setPlaybackSpeedCalled; - public boolean setPlaybackParametersCalled; - public boolean setMediaItemCalled; - public boolean setMediaItemWithStartPositionCalled; - public boolean setMediaItemWithResetPositionCalled; - public boolean setMediaItemsCalled; - public boolean setMediaItemsWithResetPositionCalled; - public boolean setMediaItemsWithStartIndexCalled; - public boolean setPlaylistMetadataCalled; - public boolean addMediaItemCalled; - public boolean addMediaItemWithIndexCalled; - public boolean addMediaItemsCalled; - public boolean addMediaItemsWithIndexCalled; - public boolean removeMediaItemCalled; - public boolean removeMediaItemsCalled; - public boolean clearMediaItemsCalled; - public boolean moveMediaItemCalled; - public boolean moveMediaItemsCalled; - public boolean seekToPreviousMediaItemCalled; - public boolean seekToNextMediaItemCalled; - public boolean seekToPreviousCalled; - public boolean seekToNextCalled; - public boolean setRepeatModeCalled; - public boolean setShuffleModeCalled; - public boolean setVolumeCalled; - public boolean setDeviceVolumeCalled; - public boolean increaseDeviceVolumeCalled; - public boolean decreaseDeviceVolumeCalled; - public boolean setDeviceMutedCalled; - public boolean setPlayWhenReadyCalled; - public boolean seekBackCalled; - public boolean seekForwardCalled; - public boolean setTrackSelectionParametersCalled; - private MockPlayer(Builder builder) { - countDownLatch = new CountDownLatch(builder.latchCount); changePlayerStateWithTransportControl = builder.changePlayerStateWithTransportControl; applicationLooper = builder.applicationLooper; @@ -203,13 +303,12 @@ public class MockPlayer implements Player { @Override public void release() { - releaseCalled = true; + checkNotNull(conditionVariables.get(METHOD_RELEASE)).open(); } @Override public void stop() { - stopCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_STOP)).open(); } @Deprecated @@ -236,8 +335,7 @@ public class MockPlayer implements Player { @Override public void play() { - playCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_PLAY)).open(); if (changePlayerStateWithTransportControl) { notifyPlayWhenReadyChanged( /* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); @@ -246,8 +344,7 @@ public class MockPlayer implements Player { @Override public void pause() { - pauseCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_PAUSE)).open(); if (changePlayerStateWithTransportControl) { notifyPlayWhenReadyChanged( /* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); @@ -256,8 +353,7 @@ public class MockPlayer implements Player { @Override public void prepare() { - prepareCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_PREPARE)).open(); if (changePlayerStateWithTransportControl) { notifyPlaybackStateChanged(Player.STATE_READY); } @@ -265,30 +361,27 @@ public class MockPlayer implements Player { @Override public void seekToDefaultPosition() { - seekToDefaultPositionCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SEEK_TO_DEFAULT_POSITION)).open(); } @Override public void seekToDefaultPosition(int mediaItemIndex) { - seekToDefaultPositionWithMediaItemIndexCalled = true; seekMediaItemIndex = mediaItemIndex; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SEEK_TO_DEFAULT_POSITION_WITH_MEDIA_ITEM_INDEX)) + .open(); } @Override public void seekTo(long positionMs) { - seekToCalled = true; seekPositionMs = positionMs; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SEEK_TO)).open(); } @Override public void seekTo(int mediaItemIndex, long positionMs) { - seekToWithMediaItemIndexCalled = true; seekMediaItemIndex = mediaItemIndex; seekPositionMs = positionMs; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX)).open(); } @Override @@ -298,8 +391,7 @@ public class MockPlayer implements Player { @Override public void seekBack() { - seekBackCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SEEK_BACK)).open(); } @Override @@ -309,8 +401,7 @@ public class MockPlayer implements Player { @Override public void seekForward() { - seekForwardCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SEEK_FORWARD)).open(); } @Override @@ -508,16 +599,14 @@ public class MockPlayer implements Player { @Override public void setPlaybackParameters(PlaybackParameters playbackParameters) { - setPlaybackParametersCalled = true; this.playbackParameters = playbackParameters; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_PLAYBACK_PARAMETERS)).open(); } @Override public void setPlaybackSpeed(float speed) { - setPlaybackSpeedCalled = true; playbackParameters = new PlaybackParameters(speed); - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_PLAYBACK_SPEED)).open(); } @Override @@ -527,9 +616,8 @@ public class MockPlayer implements Player { @Override public void setVolume(float volume) { - setVolumeCalled = true; this.volume = volume; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_VOLUME)).open(); } @Override @@ -554,35 +642,30 @@ public class MockPlayer implements Player { @Override public void setDeviceVolume(int volume) { - setDeviceVolumeCalled = true; deviceVolume = volume; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_DEVICE_VOLUME)).open(); } @Override public void increaseDeviceVolume() { - increaseDeviceVolumeCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_INCREASE_DEVICE_VOLUME)).open(); } @Override public void decreaseDeviceVolume() { - decreaseDeviceVolumeCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_DECREASE_DEVICE_VOLUME)).open(); } @Override public void setDeviceMuted(boolean muted) { - setDeviceMutedCalled = true; deviceMuted = muted; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_DEVICE_MUTED)).open(); } @Override public void setPlayWhenReady(boolean playWhenReady) { - this.setPlayWhenReadyCalled = true; this.playWhenReady = playWhenReady; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_PLAY_WHEN_READY)).open(); } @Override @@ -626,49 +709,43 @@ public class MockPlayer implements Player { @Override public void setMediaItem(MediaItem mediaItem) { - setMediaItemCalled = true; this.mediaItem = mediaItem; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_MEDIA_ITEM)).open(); } @Override public void setMediaItem(MediaItem mediaItem, long startPositionMs) { - setMediaItemWithStartPositionCalled = true; this.mediaItem = mediaItem; this.startPositionMs = startPositionMs; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_MEDIA_ITEM_WITH_START_POSITION)).open(); } @Override public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { - setMediaItemWithResetPositionCalled = true; this.mediaItem = mediaItem; this.resetPosition = resetPosition; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_MEDIA_ITEM_WITH_RESET_POSITION)).open(); } @Override public void setMediaItems(List mediaItems) { - setMediaItemsCalled = true; this.mediaItems = mediaItems; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_MEDIA_ITEMS)).open(); } @Override public void setMediaItems(List mediaItems, boolean resetPosition) { - setMediaItemsWithResetPositionCalled = true; this.mediaItems = mediaItems; this.resetPosition = resetPosition; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION)).open(); } @Override public void setMediaItems(List mediaItems, int startIndex, long startPositionMs) { - setMediaItemsWithStartIndexCalled = true; this.mediaItems = mediaItems; this.startMediaItemIndex = startIndex; this.startPositionMs = startPositionMs; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)).open(); } @Override @@ -678,9 +755,8 @@ public class MockPlayer implements Player { @Override public void setPlaylistMetadata(MediaMetadata playlistMetadata) { - setPlaylistMetadataCalled = true; this.playlistMetadata = playlistMetadata; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_PLAYLIST_METADATA)).open(); } @Deprecated @@ -775,70 +851,61 @@ public class MockPlayer implements Player { @Override public void addMediaItem(MediaItem mediaItem) { - addMediaItemCalled = true; this.mediaItem = mediaItem; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_ADD_MEDIA_ITEM)).open(); } @Override public void addMediaItem(int index, MediaItem mediaItem) { - addMediaItemWithIndexCalled = true; this.index = index; this.mediaItem = mediaItem; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_ADD_MEDIA_ITEM_WITH_INDEX)).open(); } @Override public void addMediaItems(List mediaItems) { - addMediaItemsCalled = true; this.mediaItems = mediaItems; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_ADD_MEDIA_ITEMS)).open(); } @Override public void addMediaItems(int index, List mediaItems) { - addMediaItemsWithIndexCalled = true; this.index = index; this.mediaItems = mediaItems; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_ADD_MEDIA_ITEMS_WITH_INDEX)).open(); } @Override public void removeMediaItem(int index) { - removeMediaItemCalled = true; this.index = index; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_REMOVE_MEDIA_ITEM)).open(); } @Override public void removeMediaItems(int fromIndex, int toIndex) { - removeMediaItemsCalled = true; this.fromIndex = fromIndex; this.toIndex = toIndex; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_REMOVE_MEDIA_ITEMS)).open(); } @Override public void clearMediaItems() { - clearMediaItemsCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_CLEAR_MEDIA_ITEMS)).open(); } @Override public void moveMediaItem(int currentIndex, int newIndex) { - moveMediaItemCalled = true; this.index = currentIndex; this.newIndex = newIndex; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_MOVE_MEDIA_ITEM)).open(); } @Override public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { - moveMediaItemsCalled = true; this.fromIndex = fromIndex; this.toIndex = toIndex; this.newIndex = newIndex; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_MOVE_MEDIA_ITEMS)).open(); } @Deprecated @@ -901,20 +968,17 @@ public class MockPlayer implements Player { @Override public void seekToPreviousMediaItem() { - seekToPreviousMediaItemCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SEEK_TO_PREVIOUS_MEDIA_ITEM)).open(); } @Override public void seekToNextMediaItem() { - seekToNextMediaItemCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SEEK_TO_NEXT_MEDIA_ITEM)).open(); } @Override public void seekToPrevious() { - seekToPreviousCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SEEK_TO_PREVIOUS)).open(); } @Override @@ -924,8 +988,7 @@ public class MockPlayer implements Player { @Override public void seekToNext() { - seekToNextCalled = true; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SEEK_TO_NEXT)).open(); } @Override @@ -935,9 +998,8 @@ public class MockPlayer implements Player { @Override public void setRepeatMode(int repeatMode) { - setRepeatModeCalled = true; this.repeatMode = repeatMode; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_REPEAT_MODE)).open(); } @Override @@ -947,9 +1009,8 @@ public class MockPlayer implements Player { @Override public void setShuffleModeEnabled(boolean shuffleModeEnabled) { - setShuffleModeCalled = true; this.shuffleModeEnabled = shuffleModeEnabled; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_SHUFFLE_MODE)).open(); } @Override @@ -1001,9 +1062,6 @@ public class MockPlayer implements Player { @Override public VideoSize getVideoSize() { - if (videoSize == null) { - videoSize = VideoSize.UNKNOWN; - } return videoSize; } @@ -1112,16 +1170,6 @@ public class MockPlayer implements Player { } } - @Override - public TrackGroupArray getCurrentTrackGroups() { - throw new UnsupportedOperationException(); - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - throw new UnsupportedOperationException(); - } - @Override public TracksInfo getCurrentTracksInfo() { throw new UnsupportedOperationException(); @@ -1134,9 +1182,8 @@ public class MockPlayer implements Player { @Override public void setTrackSelectionParameters(TrackSelectionParameters parameters) { - setTrackSelectionParametersCalled = true; trackSelectionParameters = parameters; - countDownLatch.countDown(); + checkNotNull(conditionVariables.get(METHOD_SET_TRACK_SELECTION_PARAMETERS)).open(); } @Override @@ -1144,10 +1191,73 @@ public class MockPlayer implements Player { return applicationLooper; } + /** Returns whether {@code method} has been called at least once. */ + public boolean hasMethodBeenCalled(@Method int method) { + return checkNotNull(conditionVariables.get(method)).isOpen(); + } + + /** + * Awaits up to {@code timeOutMs} until {@code method} is called, otherwise throws a {@link + * TimeoutException}. + */ + public void awaitMethodCalled(@Method int method, long timeOutMs) + throws TimeoutException, InterruptedException { + if (!checkNotNull(conditionVariables.get(method)).block(timeOutMs)) { + throw new TimeoutException( + Util.formatInvariant("Method %d not called after %f ms", method, timeOutMs)); + } + } + + private static ImmutableMap<@Method Integer, ConditionVariable> createMethodConditionVariables() { + return new ImmutableMap.Builder<@Method Integer, ConditionVariable>() + .put(METHOD_ADD_MEDIA_ITEM, new ConditionVariable()) + .put(METHOD_ADD_MEDIA_ITEMS, new ConditionVariable()) + .put(METHOD_ADD_MEDIA_ITEM_WITH_INDEX, new ConditionVariable()) + .put(METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, new ConditionVariable()) + .put(METHOD_CLEAR_MEDIA_ITEMS, new ConditionVariable()) + .put(METHOD_DECREASE_DEVICE_VOLUME, new ConditionVariable()) + .put(METHOD_INCREASE_DEVICE_VOLUME, new ConditionVariable()) + .put(METHOD_MOVE_MEDIA_ITEM, new ConditionVariable()) + .put(METHOD_MOVE_MEDIA_ITEMS, new ConditionVariable()) + .put(METHOD_PAUSE, new ConditionVariable()) + .put(METHOD_PLAY, new ConditionVariable()) + .put(METHOD_PREPARE, new ConditionVariable()) + .put(METHOD_RELEASE, new ConditionVariable()) + .put(METHOD_REMOVE_MEDIA_ITEM, new ConditionVariable()) + .put(METHOD_REMOVE_MEDIA_ITEMS, new ConditionVariable()) + .put(METHOD_SEEK_BACK, new ConditionVariable()) + .put(METHOD_SEEK_FORWARD, new ConditionVariable()) + .put(METHOD_SEEK_TO, new ConditionVariable()) + .put(METHOD_SEEK_TO_DEFAULT_POSITION, new ConditionVariable()) + .put(METHOD_SEEK_TO_DEFAULT_POSITION_WITH_MEDIA_ITEM_INDEX, new ConditionVariable()) + .put(METHOD_SEEK_TO_NEXT, new ConditionVariable()) + .put(METHOD_SEEK_TO_NEXT_MEDIA_ITEM, new ConditionVariable()) + .put(METHOD_SEEK_TO_PREVIOUS, new ConditionVariable()) + .put(METHOD_SEEK_TO_PREVIOUS_MEDIA_ITEM, new ConditionVariable()) + .put(METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX, new ConditionVariable()) + .put(METHOD_SET_DEVICE_MUTED, new ConditionVariable()) + .put(METHOD_SET_DEVICE_VOLUME, new ConditionVariable()) + .put(METHOD_SET_MEDIA_ITEM, new ConditionVariable()) + .put(METHOD_SET_MEDIA_ITEM_WITH_RESET_POSITION, new ConditionVariable()) + .put(METHOD_SET_MEDIA_ITEM_WITH_START_POSITION, new ConditionVariable()) + .put(METHOD_SET_MEDIA_ITEMS, new ConditionVariable()) + .put(METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, new ConditionVariable()) + .put(METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, new ConditionVariable()) + .put(METHOD_SET_PLAY_WHEN_READY, new ConditionVariable()) + .put(METHOD_SET_PLAYBACK_PARAMETERS, new ConditionVariable()) + .put(METHOD_SET_PLAYBACK_SPEED, new ConditionVariable()) + .put(METHOD_SET_PLAYLIST_METADATA, new ConditionVariable()) + .put(METHOD_SET_REPEAT_MODE, new ConditionVariable()) + .put(METHOD_SET_SHUFFLE_MODE, new ConditionVariable()) + .put(METHOD_SET_TRACK_SELECTION_PARAMETERS, new ConditionVariable()) + .put(METHOD_SET_VOLUME, new ConditionVariable()) + .put(METHOD_STOP, new ConditionVariable()) + .buildOrThrow(); + } + /** Builder for {@link MockPlayer}. */ public static final class Builder { - private int latchCount; private boolean changePlayerStateWithTransportControl; private Looper applicationLooper; private int itemCount; @@ -1156,11 +1266,6 @@ public class MockPlayer implements Player { applicationLooper = Util.getCurrentOrMainLooper(); } - public Builder setLatchCount(int latchCount) { - this.latchCount = latchCount; - return this; - } - public Builder setChangePlayerStateWithTransportControl( boolean changePlayerStateWithTransportControl) { this.changePlayerStateWithTransportControl = changePlayerStateWithTransportControl; diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/CacheAsserts.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/CacheAsserts.java index 864abd634f..b7bf6bcc47 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/CacheAsserts.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/CacheAsserts.java @@ -26,7 +26,7 @@ import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSourceInputStream; import androidx.media3.datasource.DataSourceUtil; import androidx.media3.datasource.DataSpec; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.test.utils.FakeDataSet.FakeData; @@ -129,7 +129,7 @@ public final class CacheAsserts { */ public static void assertDataCached(Cache cache, DataSpec dataSpec, byte[] expected) throws IOException { - DataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); + DataSource dataSource = new CacheDataSource(cache, PlaceholderDataSource.INSTANCE, 0); byte[] bytes; try { dataSource.open(dataSpec); diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaPeriod.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaPeriod.java index fbf23dee12..d104d00fb3 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaPeriod.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaPeriod.java @@ -23,7 +23,6 @@ import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.MimeTypes; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -38,6 +37,7 @@ import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.SequenceableLoader; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.source.chunk.ChunkSampleStream; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.Allocator; diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaSource.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaSource.java index ca6e264daa..eeca86404c 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaSource.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaSource.java @@ -18,7 +18,6 @@ package androidx.media3.test.utils; import androidx.annotation.Nullable; import androidx.media3.common.Timeline; import androidx.media3.common.Timeline.Period; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.TransferListener; @@ -27,6 +26,7 @@ import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSourceEventListener; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.upstream.Allocator; /** diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java index ae7b68389b..52fb79a3bd 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java @@ -28,7 +28,6 @@ import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSpec; @@ -40,6 +39,7 @@ import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.source.SampleStream; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem; diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaSource.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaSource.java index 1779b3ef71..c1c206ebf1 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaSource.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaSource.java @@ -30,7 +30,6 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.Timeline; import androidx.media3.common.Timeline.Period; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -45,6 +44,7 @@ import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSourceEventListener; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.test.utils.FakeMediaPeriod.TrackDataFactory; import com.google.common.collect.ImmutableMap; diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelector.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelector.java index d35db67b64..1fa7bf119c 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelector.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTrackSelector.java @@ -17,11 +17,11 @@ package androidx.media3.test.utils; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.RendererCapabilities.AdaptiveSupport; import androidx.media3.exoplayer.RendererCapabilities.Capabilities; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.MappingTrackSelector; @@ -67,7 +67,10 @@ public class FakeTrackSelector extends DefaultTrackSelector { for (int i = 0; i < rendererCount; i++) { TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); boolean hasTracks = trackGroupArray.length > 0; - definitions[i] = hasTracks ? new ExoTrackSelection.Definition(trackGroupArray.get(0)) : null; + definitions[i] = + hasTracks + ? new ExoTrackSelection.Definition(trackGroupArray.get(0), /* tracks...= */ 0) + : null; } return definitions; } 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 71e0a49fe4..c794f731d8 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 @@ -22,12 +22,12 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.StreamKey; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.offline.FilterableManifest; 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; diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java index baa9047330..ee75dc9d73 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java @@ -33,6 +33,8 @@ import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.AnalyticsListener; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.ShuffleOrder; +import androidx.media3.exoplayer.source.TrackGroupArray; +import androidx.media3.exoplayer.trackselection.TrackSelectionArray; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.exoplayer.video.VideoFrameMetadataListener; import androidx.media3.exoplayer.video.spherical.CameraMotionListener; @@ -294,6 +296,16 @@ public class StubExoPlayer extends StubPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public TrackGroupArray getCurrentTrackGroups() { + throw new UnsupportedOperationException(); + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + throw new UnsupportedOperationException(); + } + @Override public void setForegroundMode(boolean foregroundMode) { throw new UnsupportedOperationException(); diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java index b3373e1f68..49c8dd4093 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java @@ -30,8 +30,6 @@ import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroupArray; -import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; @@ -192,16 +190,6 @@ public class StubPlayer extends BasePlayer { throw new UnsupportedOperationException(); } - @Override - public TrackGroupArray getCurrentTrackGroups() { - throw new UnsupportedOperationException(); - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - throw new UnsupportedOperationException(); - } - @Override public TracksInfo getCurrentTracksInfo() { throw new UnsupportedOperationException(); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java new file mode 100644 index 0000000000..724eec72b8 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java @@ -0,0 +1,178 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.media3.transformer.BitmapTestUtil.FIRST_FRAME_PNG_ASSET_STRING; +import static androidx.media3.transformer.BitmapTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE; +import static androidx.media3.transformer.BitmapTestUtil.ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING; +import static androidx.media3.transformer.BitmapTestUtil.SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING; +import static androidx.media3.transformer.BitmapTestUtil.TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.SurfaceTexture; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLSurface; +import androidx.media3.common.util.GlUtil; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Pixel test for frame processing via {@link AdvancedFrameProcessor}. + * + *

    Expected images are taken from an emulator, so tests on different emulators or physical + * devices may fail. To test on other devices, please increase the {@link + * BitmapTestUtil#MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE} and/or inspect the saved output bitmaps + * as recommended in {@link FrameProcessorChainPixelTest}. + */ +@RunWith(AndroidJUnit4.class) +public final class AdvancedFrameProcessorPixelTest { + + static { + GlUtil.glAssertionsEnabled = true; + } + + private final EGLDisplay eglDisplay = GlUtil.createEglDisplay(); + private final EGLContext eglContext = GlUtil.createEglContext(eglDisplay); + private @MonotonicNonNull GlFrameProcessor advancedFrameProcessor; + private int inputTexId; + private int outputTexId; + private int width; + private int height; + + @Before + public void createTextures() throws IOException { + Bitmap inputBitmap = BitmapTestUtil.readBitmap(FIRST_FRAME_PNG_ASSET_STRING); + width = inputBitmap.getWidth(); + height = inputBitmap.getHeight(); + // This surface is needed for focussing a render target, but the tests don't write output to it. + // The frame processor's output is written to a framebuffer instead. + EGLSurface eglSurface = GlUtil.getEglSurface(eglDisplay, new SurfaceTexture(false)); + GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, width, height); + inputTexId = + BitmapTestUtil.createGlTextureFromBitmap( + BitmapTestUtil.readBitmap(FIRST_FRAME_PNG_ASSET_STRING)); + outputTexId = GlUtil.createTexture(width, height); + int frameBuffer = GlUtil.createFboForTexture(outputTexId); + GlUtil.focusFramebuffer(eglDisplay, eglContext, eglSurface, frameBuffer, width, height); + } + + @After + public void release() { + if (advancedFrameProcessor != null) { + advancedFrameProcessor.release(); + } + GlUtil.destroyEglContext(eglDisplay, eglContext); + } + + @Test + public void updateProgramAndDraw_noEdits_producesExpectedOutput() throws Exception { + String testId = "updateProgramAndDraw_noEdits"; + Matrix identityMatrix = new Matrix(); + advancedFrameProcessor = new AdvancedFrameProcessor(getApplicationContext(), identityMatrix); + advancedFrameProcessor.initialize(inputTexId, width, height); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(FIRST_FRAME_PNG_ASSET_STRING); + + advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap, /* throwOnFailure= */ false); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void updateProgramAndDraw_translateRight_producesExpectedOutput() throws Exception { + String testId = "updateProgramAndDraw_translateRight"; + Matrix translateRightMatrix = new Matrix(); + translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0); + advancedFrameProcessor = + new AdvancedFrameProcessor(getApplicationContext(), translateRightMatrix); + advancedFrameProcessor.initialize(inputTexId, width, height); + Bitmap expectedBitmap = + BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); + + advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap, /* throwOnFailure= */ false); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void updateProgramAndDraw_scaleNarrow_producesExpectedOutput() throws Exception { + String testId = "updateProgramAndDraw_scaleNarrow"; + Matrix scaleNarrowMatrix = new Matrix(); + scaleNarrowMatrix.postScale(.5f, 1.2f); + advancedFrameProcessor = new AdvancedFrameProcessor(getApplicationContext(), scaleNarrowMatrix); + advancedFrameProcessor.initialize(inputTexId, width, height); + Bitmap expectedBitmap = + BitmapTestUtil.readBitmap(SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING); + + advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap, /* throwOnFailure= */ false); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void updateProgramAndDraw_rotate90_producesExpectedOutput() throws Exception { + String testId = "updateProgramAndDraw_rotate90"; + Matrix rotate90Matrix = new Matrix(); + rotate90Matrix.postRotate(/* degrees= */ 90); + advancedFrameProcessor = new AdvancedFrameProcessor(getApplicationContext(), rotate90Matrix); + advancedFrameProcessor.initialize(inputTexId, width, height); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING); + + advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap, /* throwOnFailure= */ false); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java index 9a75a1360d..3fdc29bcd8 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java @@ -15,162 +15,85 @@ */ package androidx.media3.transformer; -import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; -import static java.util.concurrent.TimeUnit.SECONDS; import android.content.Context; -import android.net.Uri; import android.os.Build; -import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.MediaItem; +import androidx.media3.common.Format; import androidx.media3.common.util.Log; -import androidx.test.platform.app.InstrumentationRegistry; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; -import org.checkerframework.checker.nullness.compatqual.NullableType; +import java.util.List; import org.json.JSONException; import org.json.JSONObject; /** Utilities for instrumentation tests. */ public final class AndroidTestUtil { public static final String MP4_ASSET_URI_STRING = "asset:///media/mp4/sample.mp4"; - public static final String SEF_ASSET_URI_STRING = "asset:///media/mp4/sample_sef_slow_motion.mp4"; - public static final String REMOTE_MP4_10_SECONDS_URI_STRING = + public static final String MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING = + "asset:///media/mp4/sample_with_increasing_timestamps.mp4"; + public static final String MP4_ASSET_SEF_URI_STRING = + "asset:///media/mp4/sample_sef_slow_motion.mp4"; + public static final String MP4_REMOTE_10_SECONDS_URI_STRING = "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4"; + /** Test clip transcoded from {@link #MP4_REMOTE_10_SECONDS_URI_STRING} with H264 and MP3. */ + public static final String MP4_REMOTE_H264_MP3_URI_STRING = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/%20android-screens-10s-h264-mp3.mp4"; + + public static final String MP4_REMOTE_4K60_PORTRAIT_URI_STRING = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4"; /** - * Transforms the {@code uriString} with the {@link Transformer}, saving a summary of the - * transformation to the application cache. + * Log in logcat and in an analysis file that this test was skipped. * - * @param context The {@link Context}. - * @param testId An identifier for the test. - * @param transformer The {@link Transformer} that performs the transformation. - * @param uriString The uri (as a {@link String}) that will be transformed. - * @param timeoutSeconds The transformer timeout. An exception is thrown if this is exceeded. - * @return The {@link TestTransformationResult}. - * @throws Exception The cause of the transformation not completing. + *

    Analysis file is a JSON summarising the test, saved to the application cache. + * + *

    The analysis json will contain a {@code skipReason} key, with the reason for skipping the + * test case. */ - public static TestTransformationResult runTransformer( - Context context, String testId, Transformer transformer, String uriString, int timeoutSeconds) - throws Exception { - JSONObject resultJson = new JSONObject(); - try { - TestTransformationResult testTransformationResult = - runTransformerInternal(context, testId, transformer, uriString, timeoutSeconds); - resultJson.put( - "transformationResult", - getTransformationResultJson(testTransformationResult.transformationResult)); - return testTransformationResult; - } catch (Exception e) { - resultJson.put("exception", getExceptionJson(e)); - throw e; - } finally { - writeTestSummaryToFile(context, testId, resultJson); - } + public static void recordTestSkipped(Context context, String testId, String reason) + throws JSONException, IOException { + Log.i(testId, reason); + JSONObject testJson = new JSONObject(); + testJson.put("skipReason", reason); + + writeTestSummaryToFile(context, testId, testJson); } - private static TestTransformationResult runTransformerInternal( - Context context, String testId, Transformer transformer, String uriString, int timeoutSeconds) - throws Exception { - AtomicReference<@NullableType TransformationException> transformationExceptionReference = - new AtomicReference<>(); - AtomicReference<@NullableType Exception> unexpectedExceptionReference = new AtomicReference<>(); - AtomicReference<@NullableType TransformationResult> transformationResultReference = - new AtomicReference<>(); - CountDownLatch countDownLatch = new CountDownLatch(1); + /** + * A {@link Codec.EncoderFactory} that forces encoding, wrapping {@link DefaultEncoderFactory}. + */ + public static final Codec.EncoderFactory FORCE_ENCODE_ENCODER_FACTORY = + new Codec.EncoderFactory() { + @Override + public Codec createForAudioEncoding(Format format, List allowedMimeTypes) + throws TransformationException { + return Codec.EncoderFactory.DEFAULT.createForAudioEncoding(format, allowedMimeTypes); + } - Transformer testTransformer = - transformer - .buildUpon() - .addListener( - new Transformer.Listener() { - @Override - public void onTransformationCompleted( - MediaItem inputMediaItem, TransformationResult result) { - transformationResultReference.set(result); - countDownLatch.countDown(); - } + @Override + public Codec createForVideoEncoding(Format format, List allowedMimeTypes) + throws TransformationException { + return Codec.EncoderFactory.DEFAULT.createForVideoEncoding(format, allowedMimeTypes); + } - @Override - public void onTransformationError( - MediaItem inputMediaItem, TransformationException exception) { - transformationExceptionReference.set(exception); - countDownLatch.countDown(); - } - }) - .build(); + @Override + public boolean audioNeedsEncoding() { + return true; + } - Uri uri = Uri.parse(uriString); - File outputVideoFile = createExternalCacheFile(context, /* fileName= */ testId + "-output.mp4"); - InstrumentationRegistry.getInstrumentation() - .runOnMainSync( - () -> { - try { - testTransformer.startTransformation( - MediaItem.fromUri(uri), outputVideoFile.getAbsolutePath()); - // Catch all exceptions to report. Exceptions thrown here and not caught will NOT - // propagate. - } catch (Exception e) { - unexpectedExceptionReference.set(e); - countDownLatch.countDown(); - } - }); + @Override + public boolean videoNeedsEncoding() { + return true; + } + }; - if (!countDownLatch.await(timeoutSeconds, SECONDS)) { - throw new TimeoutException("Transformer timed out after " + timeoutSeconds + " seconds."); - } - - @Nullable Exception unexpectedException = unexpectedExceptionReference.get(); - if (unexpectedException != null) { - throw unexpectedException; - } - - @Nullable - TransformationException transformationException = transformationExceptionReference.get(); - if (transformationException != null) { - throw transformationException; - } - - // If both exceptions are null, the Transformation must have succeeded, and a - // transformationResult will be available. - TransformationResult transformationResult = - checkNotNull(transformationResultReference.get()) - .buildUpon() - .setFileSizeBytes(outputVideoFile.length()) - .build(); - - return new TestTransformationResult(transformationResult, outputVideoFile.getPath()); - } - - private static void writeTestSummaryToFile(Context context, String testId, JSONObject resultJson) - throws IOException, JSONException { - resultJson.put("testId", testId).put("device", getDeviceJson()); - - String analysisContents = resultJson.toString(/* indentSpaces= */ 2); - - // Log contents as well as writing to file, for easier visibility on individual device testing. - Log.i("TransformerAndroidTest_" + testId, analysisContents); - - File analysisFile = createExternalCacheFile(context, /* fileName= */ testId + "-result.txt"); - try (FileWriter fileWriter = new FileWriter(analysisFile)) { - fileWriter.write(analysisContents); - } - } - - private static File createExternalCacheFile(Context context, String fileName) throws IOException { - File file = new File(context.getExternalCacheDir(), fileName); - checkState(!file.exists() || file.delete(), "Could not delete file: " + file.getAbsolutePath()); - checkState(file.createNewFile(), "Could not create file: " + file.getAbsolutePath()); - return file; - } - - private static JSONObject getDeviceJson() throws JSONException { + /** + * Returns a {@link JSONObject} containing device specific details from {@link Build}, including + * manufacturer, model, SDK version and build fingerprint. + */ + public static JSONObject getDeviceDetailsAsJsonObject() throws JSONException { return new JSONObject() .put("manufacturer", Build.MANUFACTURER) .put("model", Build.MODEL) @@ -178,22 +101,12 @@ public final class AndroidTestUtil { .put("fingerprint", Build.FINGERPRINT); } - private static JSONObject getTransformationResultJson(TransformationResult transformationResult) - throws JSONException { - JSONObject transformationResultJson = new JSONObject(); - if (transformationResult.fileSizeBytes != C.LENGTH_UNSET) { - transformationResultJson.put("fileSizeBytes", transformationResult.fileSizeBytes); - } - if (transformationResult.averageAudioBitrate != C.RATE_UNSET_INT) { - transformationResultJson.put("averageAudioBitrate", transformationResult.averageAudioBitrate); - } - if (transformationResult.averageVideoBitrate != C.RATE_UNSET_INT) { - transformationResultJson.put("averageVideoBitrate", transformationResult.averageVideoBitrate); - } - return transformationResultJson; - } - - private static JSONObject getExceptionJson(Exception exception) throws JSONException { + /** + * Converts an exception to a {@link JSONObject}. + * + *

    If the exception is a {@link TransformationException}, {@code errorCode} is included. + */ + public static JSONObject exceptionAsJsonObject(Exception exception) throws JSONException { JSONObject exceptionJson = new JSONObject(); exceptionJson.put("message", exception.getMessage()); exceptionJson.put("type", exception.getClass()); @@ -204,5 +117,42 @@ public final class AndroidTestUtil { return exceptionJson; } + /** + * Writes the summary of a test run to the application cache file. + * + *

    The cache filename follows the pattern {@code -result.txt}. + * + * @param context The {@link Context}. + * @param testId A unique identifier for the transformer test run. + * @param testJson A {@link JSONObject} containing a summary of the test run. + */ + /* package */ static void writeTestSummaryToFile( + Context context, String testId, JSONObject testJson) throws IOException, JSONException { + testJson.put("testId", testId).put("device", getDeviceDetailsAsJsonObject()); + + String analysisContents = testJson.toString(/* indentSpaces= */ 2); + + // Log contents as well as writing to file, for easier visibility on individual device testing. + Log.i(testId, analysisContents); + + File analysisFile = createExternalCacheFile(context, /* fileName= */ testId + "-result.txt"); + try (FileWriter fileWriter = new FileWriter(analysisFile)) { + fileWriter.write(analysisContents); + } + } + + /** + * Creates a {@link File} of the {@code fileName} in the application cache directory. + * + *

    If a file of that name already exists, it is overwritten. + */ + /* package */ static File createExternalCacheFile(Context context, String fileName) + throws IOException { + File file = new File(context.getExternalCacheDir(), fileName); + checkState(!file.exists() || file.delete(), "Could not delete file: " + file.getAbsolutePath()); + checkState(file.createNewFile(), "Could not create file: " + file.getAbsolutePath()); + return file; + } + private AndroidTestUtil() {} } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java new file mode 100644 index 0000000000..a590b903f4 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java @@ -0,0 +1,258 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; +import static java.lang.Math.abs; +import static java.lang.Math.max; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.PixelFormat; +import android.media.Image; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import androidx.annotation.Nullable; +import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.Log; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * Utilities for instrumentation tests for the {@link FrameProcessorChain} and {@link + * GlFrameProcessor GlFrameProcessors}. + */ +public class BitmapTestUtil { + + private static final String TAG = "BitmapTestUtil"; + + /* Expected first frames after transformation. */ + public static final String FIRST_FRAME_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame.png"; + public static final String TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame_translate_right.png"; + public static final String SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame_scale_narrow.png"; + public static final String ROTATE_THEN_TRANSLATE_EXPECTED_OUTPUT_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame_rotate_then_translate.png"; + public static final String TRANSLATE_THEN_ROTATE_EXPECTED_OUTPUT_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame_translate_then_rotate.png"; + public static final String ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame_rotate90.png"; + public static final String REQUEST_OUTPUT_HEIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame_request_output_height.png"; + public static final String ROTATE45_SCALE_TO_FIT_EXPECTED_OUTPUT_PNG_ASSET_STRING = + "media/bitmap/sample_mp4_first_frame_rotate_45_scale_to_fit.png"; + /** + * Maximum allowed average pixel difference between the expected and actual edited images in pixel + * difference-based tests. The value is chosen so that differences in decoder behavior across + * emulator versions don't affect whether the test passes for most emulators, but substantial + * distortions introduced by changes in the behavior of the {@link GlFrameProcessor + * GlFrameProcessors} will cause the test to fail. + * + *

    To run pixel difference-based tests on physical devices, please use a value of 5f, rather + * than 0.1f. This higher value will ignore some very small errors, but will allow for some + * differences caused by graphics implementations to be ignored. When the difference is close to + * the threshold, manually inspect expected/actual bitmaps to confirm failure, as it's possible + * this is caused by a difference in the codec or graphics implementation as opposed to a {@link + * GlFrameProcessor} issue. + */ + public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE = 0.1f; + + /** + * Reads a bitmap from the specified asset location. + * + * @param assetString Relative path to the asset within the assets directory. + * @return A {@link Bitmap}. + * @throws IOException If the bitmap can't be read. + */ + public static Bitmap readBitmap(String assetString) throws IOException { + Bitmap bitmap; + try (InputStream inputStream = getApplicationContext().getAssets().open(assetString)) { + bitmap = BitmapFactory.decodeStream(inputStream); + } + return bitmap; + } + + /** + * Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per + * component image. + */ + public static Bitmap createArgb8888BitmapFromRgba8888Image(Image image) { + int width = image.getWidth(); + int height = image.getHeight(); + assertThat(image.getPlanes()).hasLength(1); + assertThat(image.getFormat()).isEqualTo(PixelFormat.RGBA_8888); + Image.Plane plane = image.getPlanes()[0]; + ByteBuffer buffer = plane.getBuffer(); + int[] colors = new int[width * height]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int offset = y * plane.getRowStride() + x * plane.getPixelStride(); + int r = buffer.get(offset) & 0xFF; + int g = buffer.get(offset + 1) & 0xFF; + int b = buffer.get(offset + 2) & 0xFF; + int a = buffer.get(offset + 3) & 0xFF; + colors[y * width + x] = Color.argb(a, r, g, b); + } + } + return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); + } + + /** + * Returns the average difference between the expected and actual bitmaps, calculated using the + * maximum difference across all color channels for each pixel, then divided by the total number + * of pixels in the image. The bitmap resolutions must match and they must use configuration + * {@link Bitmap.Config#ARGB_8888}. + * + * @param expected The expected {@link Bitmap}. + * @param actual The actual {@link Bitmap} produced by the test. + * @param testId The name of the test that produced the {@link Bitmap}, or {@code null} if the + * differences bitmap should not be saved to cache. + * @return The average of the maximum absolute pixel-wise differences between the expected and + * actual bitmaps. + */ + public static float getAveragePixelAbsoluteDifferenceArgb8888( + Bitmap expected, Bitmap actual, @Nullable String testId) { + int width = actual.getWidth(); + int height = actual.getHeight(); + assertThat(width).isEqualTo(expected.getWidth()); + assertThat(height).isEqualTo(expected.getHeight()); + assertThat(actual.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888); + long sumMaximumAbsoluteDifferences = 0; + // Debug-only image diff without alpha. To use, set a breakpoint right before the method return + // to view the difference between the expected and actual bitmaps. A passing test should show + // an image that is completely black (color == 0). + Bitmap differencesBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int actualColor = actual.getPixel(x, y); + int expectedColor = expected.getPixel(x, y); + + int alphaDifference = abs(Color.alpha(actualColor) - Color.alpha(expectedColor)); + int redDifference = abs(Color.red(actualColor) - Color.red(expectedColor)); + int blueDifference = abs(Color.blue(actualColor) - Color.blue(expectedColor)); + int greenDifference = abs(Color.green(actualColor) - Color.green(expectedColor)); + differencesBitmap.setPixel(x, y, Color.rgb(redDifference, blueDifference, greenDifference)); + + int maximumAbsoluteDifference = 0; + maximumAbsoluteDifference = max(maximumAbsoluteDifference, alphaDifference); + maximumAbsoluteDifference = max(maximumAbsoluteDifference, redDifference); + maximumAbsoluteDifference = max(maximumAbsoluteDifference, blueDifference); + maximumAbsoluteDifference = max(maximumAbsoluteDifference, greenDifference); + + sumMaximumAbsoluteDifferences += maximumAbsoluteDifference; + } + } + if (testId != null) { + try { + saveTestBitmapToCacheDirectory( + testId, "diff", differencesBitmap, /* throwOnFailure= */ false); + } catch (IOException impossible) { + throw new IllegalStateException(impossible); + } + } + return (float) sumMaximumAbsoluteDifferences / (width * height); + } + + /** + * Saves the {@link Bitmap} to the {@link Context#getCacheDir() cache directory} as a PNG. + * + *

    File name will be {@code _.png}. If {@code throwOnFailure} is {@code + * false}, any {@link IOException} will be caught and logged. + * + * @param testId Name of the test that produced the {@link Bitmap}. + * @param bitmapLabel Label to identify the bitmap. + * @param bitmap The {@link Bitmap} to save. + * @param throwOnFailure Whether to throw an exception if the bitmap can't be saved. + * @throws IOException If the bitmap can't be saved and {@code throwOnFailure} is {@code true}. + */ + public static void saveTestBitmapToCacheDirectory( + String testId, String bitmapLabel, Bitmap bitmap, boolean throwOnFailure) throws IOException { + File file = + new File( + getApplicationContext().getExternalCacheDir(), testId + "_" + bitmapLabel + ".png"); + try (FileOutputStream outputStream = new FileOutputStream(file)) { + bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream); + } catch (IOException e) { + if (throwOnFailure) { + throw e; + } else { + Log.e(TAG, "Could not write Bitmap to file path: " + file.getAbsolutePath(), e); + } + } + } + + /** + * Creates a bitmap with the values of the current OpenGL framebuffer. + * + *

    This method may block until any previously called OpenGL commands are complete. + * + * @param width The width of the pixel rectangle to read. + * @param height The height of the pixel rectangle to read. + * @return A {@link Bitmap} with the framebuffer's values. + */ + public static Bitmap createArgb8888BitmapFromCurrentGlFramebuffer(int width, int height) { + ByteBuffer rgba8888Buffer = ByteBuffer.allocateDirect(width * height * 4); + GLES20.glReadPixels( + 0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgba8888Buffer); + GlUtil.checkGlError(); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + // According to https://www.khronos.org/opengl/wiki/Pixel_Transfer#Endian_issues, + // the colors will have the order RGBA in client memory. This is what the bitmap expects: + // https://developer.android.com/reference/android/graphics/Bitmap.Config#ARGB_8888. + bitmap.copyPixelsFromBuffer(rgba8888Buffer); + // Flip the bitmap as its positive y-axis points down while OpenGL's positive y-axis points up. + return flipBitmapVertically(bitmap); + } + + /** + * Creates a {@link GLES20#GL_TEXTURE_2D 2-dimensional OpenGL texture} with the bitmap's contents. + * + * @param bitmap A {@link Bitmap}. + * @return The identifier of the newly created texture. + */ + public static int createGlTextureFromBitmap(Bitmap bitmap) { + int texId = GlUtil.createTexture(bitmap.getWidth(), bitmap.getHeight()); + // Put the flipped bitmap in the OpenGL texture as the bitmap's positive y-axis points down + // while OpenGL's positive y-axis points up. + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, flipBitmapVertically(bitmap), 0); + GlUtil.checkGlError(); + return texId; + } + + private static Bitmap flipBitmapVertically(Bitmap bitmap) { + Matrix flip = new Matrix(); + flip.postScale(1f, -1f); + return Bitmap.createBitmap( + bitmap, + /* x= */ 0, + /* y= */ 0, + bitmap.getWidth(), + bitmap.getHeight(), + flip, + /* filter= */ true); + } + + private BitmapTestUtil() {} +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameCountingMuxer.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameCountingMuxer.java deleted file mode 100644 index 27e05ace84..0000000000 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameCountingMuxer.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.transformer; - -import android.os.ParcelFileDescriptor; -import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.Format; -import androidx.media3.common.MimeTypes; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.nio.ByteBuffer; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - -/** - * An implementation of {@link Muxer} that forwards operations to another {@link Muxer}, counting - * the number of frames as they go past. - */ -/* package */ final class FrameCountingMuxer implements Muxer { - public static final class Factory implements Muxer.Factory { - - private final Muxer.Factory muxerFactory; - private @MonotonicNonNull FrameCountingMuxer frameCountingMuxer; - - public Factory(Muxer.Factory muxerFactory) { - this.muxerFactory = muxerFactory; - } - - @Override - public Muxer create(String path, String outputMimeType) throws IOException { - frameCountingMuxer = new FrameCountingMuxer(muxerFactory.create(path, outputMimeType)); - return frameCountingMuxer; - } - - @Override - public Muxer create(ParcelFileDescriptor parcelFileDescriptor, String outputMimeType) - throws IOException { - frameCountingMuxer = - new FrameCountingMuxer(muxerFactory.create(parcelFileDescriptor, outputMimeType)); - return frameCountingMuxer; - } - - @Override - public boolean supportsOutputMimeType(String mimeType) { - return muxerFactory.supportsOutputMimeType(mimeType); - } - - @Override - public boolean supportsSampleMimeType(@Nullable String sampleMimeType, String outputMimeType) { - return muxerFactory.supportsSampleMimeType(sampleMimeType, outputMimeType); - } - - @Override - public ImmutableList getSupportedSampleMimeTypes( - @C.TrackType int trackType, String containerMimeType) { - return muxerFactory.getSupportedSampleMimeTypes(trackType, containerMimeType); - } - - @Nullable - public FrameCountingMuxer getLastFrameCountingMuxerCreated() { - return frameCountingMuxer; - } - } - - private final Muxer muxer; - private int videoTrackIndex; - private int frameCount; - - private FrameCountingMuxer(Muxer muxer) throws IOException { - this.muxer = muxer; - } - - @Override - public int addTrack(Format format) throws MuxerException { - int trackIndex = muxer.addTrack(format); - if (MimeTypes.isVideo(format.sampleMimeType)) { - videoTrackIndex = trackIndex; - } - return trackIndex; - } - - @Override - public void writeSampleData( - int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) - throws MuxerException { - muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs); - if (trackIndex == videoTrackIndex) { - frameCount++; - } - } - - @Override - public void release(boolean forCancellation) throws MuxerException { - muxer.release(forCancellation); - } - - /* Returns the number of frames written for the video track. */ - public int getFrameCount() { - return frameCount; - } -} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorDataProcessingTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorDataProcessingTest.java deleted file mode 100644 index 09b7f5efdd..0000000000 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorDataProcessingTest.java +++ /dev/null @@ -1,358 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.transformer; - -import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; -import static com.google.common.truth.Truth.assertThat; -import static java.lang.Math.abs; -import static java.lang.Math.max; - -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Color; -import android.graphics.Matrix; -import android.graphics.PixelFormat; -import android.media.Image; -import android.media.ImageReader; -import android.media.MediaCodec; -import android.media.MediaExtractor; -import android.media.MediaFormat; -import androidx.annotation.Nullable; -import androidx.media3.common.MimeTypes; -import androidx.media3.common.util.Log; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.junit.After; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Pixel test for frame processing via {@link FrameEditor#processData()}. Expected images are taken - * from emulators, so tests on physical devices may fail. To test on physical devices, please modify - * the MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE. - */ -@RunWith(AndroidJUnit4.class) -public final class FrameEditorDataProcessingTest { - - private static final String TAG = "FrameEditorDataProcessingTest"; - - // Input MP4 file to transform. - private static final String INPUT_MP4_ASSET_STRING = "media/mp4/sample.mp4"; - - /* Expected first frames after transformation. - * To generate new "expected" assets: - * 1. Insert this code into a test, to download some editedBitmap. - * + try (FileOutputStream fileOutputStream = new FileOutputStream("/sdcard/tmp.png")) { - * + // quality is ignored - * + editedBitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream); - * + } - * 2. Run the test on a "Nexus 6P API 23" emulator. Emulators are preferred as the automated - * presubmit that will run this test will also be an emulator. API versions 29+ have storage - * restrictions that complicate file generation. - * 3. Open the "Device File Explorer", find "/sdcard/tmp.png", and "Save As..." the file. - */ - private static final String NO_EDITS_EXPECTED_OUTPUT_PNG_ASSET_STRING = - "media/bitmap/sample_mp4_first_frame.png"; - private static final String TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING = - "media/bitmap/sample_mp4_first_frame_translate_right.png"; - private static final String SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING = - "media/bitmap/sample_mp4_first_frame_scale_narrow.png"; - private static final String ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING = - "media/bitmap/sample_mp4_first_frame_rotate90.png"; - - /** - * Maximum allowed average pixel difference between the expected and actual edited images for the - * test to pass. The value is chosen so that differences in decoder behavior across emulator - * versions shouldn't affect whether the test passes, but substantial distortions introduced by - * changes in the behavior of the frame editor will cause the test to fail. - * - *

    To run this test on physical devices, please use a value of 5f, rather than 0.1f. This - * higher value will ignore some very small errors, but will allow for some differences caused by - * graphics implementations to be ignored. When the difference is close to the threshold, manually - * inspect expected/actual bitmaps to confirm failure, as it's possible this is caused by a - * difference in the codec or graphics implementation as opposed to a FrameEditor issue. - */ - private static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE = 0.1f; - /** Timeout for dequeueing buffers from the codec, in microseconds. */ - private static final int DEQUEUE_TIMEOUT_US = 5_000_000; - /** Time to wait for the frame editor's input to be populated by the decoder, in milliseconds. */ - private static final int SURFACE_WAIT_MS = 1000; - /** The ratio of width over height, for each pixel in a frame. */ - private static final float PIXEL_WIDTH_HEIGHT_RATIO = 1; - - private @MonotonicNonNull FrameEditor frameEditor; - private @MonotonicNonNull ImageReader frameEditorOutputImageReader; - private @MonotonicNonNull MediaFormat mediaFormat; - - @After - public void release() { - if (frameEditor != null) { - frameEditor.release(); - } - } - - @Test - public void processData_noEdits_producesExpectedOutput() throws Exception { - Matrix identityMatrix = new Matrix(); - setUpAndPrepareFirstFrame(identityMatrix); - - Bitmap expectedBitmap = getBitmap(NO_EDITS_EXPECTED_OUTPUT_PNG_ASSET_STRING); - checkNotNull(frameEditor).processData(); - Image editedImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap editedBitmap = getArgb8888BitmapForRgba8888Image(editedImage); - - // TODO(b/207848601): switch to using proper tooling for testing against golden data. - float averagePixelAbsoluteDifference = - getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); - saveTestBitmapToCacheDirectory("processData_noEdits", editedBitmap); - assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); - } - - @Test - public void processData_translateRight_producesExpectedOutput() throws Exception { - Matrix translateRightMatrix = new Matrix(); - translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0); - setUpAndPrepareFirstFrame(translateRightMatrix); - - Bitmap expectedBitmap = getBitmap(TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); - checkNotNull(frameEditor).processData(); - Image editedImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap editedBitmap = getArgb8888BitmapForRgba8888Image(editedImage); - - // TODO(b/207848601): switch to using proper tooling for testing against golden - // data.simple - float averagePixelAbsoluteDifference = - getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); - saveTestBitmapToCacheDirectory("processData_translateRight", editedBitmap); - assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); - } - - @Test - public void processData_scaleNarrow_producesExpectedOutput() throws Exception { - Matrix scaleNarrowMatrix = new Matrix(); - scaleNarrowMatrix.postScale(.5f, 1.2f); - setUpAndPrepareFirstFrame(scaleNarrowMatrix); - Bitmap expectedBitmap = getBitmap(SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING); - - checkNotNull(frameEditor).processData(); - Image editedImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap editedBitmap = getArgb8888BitmapForRgba8888Image(editedImage); - - // TODO(b/207848601): switch to using proper tooling for testing against golden data. - float averagePixelAbsoluteDifference = - getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); - saveTestBitmapToCacheDirectory("processData_scaleNarrow", editedBitmap); - assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); - } - - @Test - public void processData_rotate90_producesExpectedOutput() throws Exception { - // TODO(b/213190310): After creating a Presentation class, move VideoSamplePipeline - // resolution-based adjustments (ex. in cl/419619743) to that Presentation class, so we can - // test that rotation doesn't distort the image. - Matrix rotate90Matrix = new Matrix(); - rotate90Matrix.postRotate(/* degrees= */ 90); - setUpAndPrepareFirstFrame(rotate90Matrix); - - Bitmap expectedBitmap = getBitmap(ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING); - checkNotNull(frameEditor).processData(); - Image editedImage = checkNotNull(frameEditorOutputImageReader).acquireLatestImage(); - Bitmap editedBitmap = getArgb8888BitmapForRgba8888Image(editedImage); - - // TODO(b/207848601): switch to using proper tooling for testing against golden data. - float averagePixelAbsoluteDifference = - getAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, editedBitmap); - saveTestBitmapToCacheDirectory("processData_rotate90", editedBitmap); - assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); - } - - private void setUpAndPrepareFirstFrame(Matrix transformationMatrix) throws Exception { - // Set up the extractor to read the first video frame and get its format. - MediaExtractor mediaExtractor = new MediaExtractor(); - @Nullable MediaCodec mediaCodec = null; - try (AssetFileDescriptor afd = - getApplicationContext().getAssets().openFd(INPUT_MP4_ASSET_STRING)) { - mediaExtractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); - for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { - if (MimeTypes.isVideo(mediaExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME))) { - mediaFormat = mediaExtractor.getTrackFormat(i); - mediaExtractor.selectTrack(i); - break; - } - } - - int width = checkNotNull(mediaFormat).getInteger(MediaFormat.KEY_WIDTH); - int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT); - frameEditorOutputImageReader = - ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, /* maxImages= */ 1); - frameEditor = - FrameEditor.create( - getApplicationContext(), - width, - height, - PIXEL_WIDTH_HEIGHT_RATIO, - transformationMatrix, - frameEditorOutputImageReader.getSurface(), - /* enableExperimentalHdrEditing= */ false, - Transformer.DebugViewProvider.NONE); - frameEditor.registerInputFrame(); - - // Queue the first video frame from the extractor. - String mimeType = checkNotNull(mediaFormat.getString(MediaFormat.KEY_MIME)); - mediaCodec = MediaCodec.createDecoderByType(mimeType); - mediaCodec.configure( - mediaFormat, frameEditor.getInputSurface(), /* crypto= */ null, /* flags= */ 0); - mediaCodec.start(); - int inputBufferIndex = mediaCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); - assertThat(inputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - ByteBuffer inputBuffer = checkNotNull(mediaCodec.getInputBuffers()[inputBufferIndex]); - int sampleSize = mediaExtractor.readSampleData(inputBuffer, /* offset= */ 0); - mediaCodec.queueInputBuffer( - inputBufferIndex, - /* offset= */ 0, - sampleSize, - mediaExtractor.getSampleTime(), - mediaExtractor.getSampleFlags()); - - // Queue an end-of-stream buffer to force the codec to produce output. - inputBufferIndex = mediaCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); - assertThat(inputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - mediaCodec.queueInputBuffer( - inputBufferIndex, - /* offset= */ 0, - /* size= */ 0, - /* presentationTimeUs= */ 0, - MediaCodec.BUFFER_FLAG_END_OF_STREAM); - - // Dequeue and render the output video frame. - MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - int outputBufferIndex; - do { - outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIMEOUT_US); - assertThat(outputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); - } while (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED - || outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); - mediaCodec.releaseOutputBuffer(outputBufferIndex, /* render= */ true); - - // Sleep to give time for the surface texture to be populated. - Thread.sleep(SURFACE_WAIT_MS); - assertThat(frameEditor.canProcessData()).isTrue(); - } finally { - mediaExtractor.release(); - if (mediaCodec != null) { - mediaCodec.release(); - } - } - } - - private Bitmap getBitmap(String expectedAssetString) throws IOException { - Bitmap bitmap; - try (InputStream inputStream = getApplicationContext().getAssets().open(expectedAssetString)) { - bitmap = BitmapFactory.decodeStream(inputStream); - } - return bitmap; - } - - /** - * Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per - * component image. - */ - private static Bitmap getArgb8888BitmapForRgba8888Image(Image image) { - int width = image.getWidth(); - int height = image.getHeight(); - assertThat(image.getPlanes()).hasLength(1); - assertThat(image.getFormat()).isEqualTo(PixelFormat.RGBA_8888); - Image.Plane plane = image.getPlanes()[0]; - ByteBuffer buffer = plane.getBuffer(); - int[] colors = new int[width * height]; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int offset = y * plane.getRowStride() + x * plane.getPixelStride(); - int r = buffer.get(offset) & 0xFF; - int g = buffer.get(offset + 1) & 0xFF; - int b = buffer.get(offset + 2) & 0xFF; - int a = buffer.get(offset + 3) & 0xFF; - colors[y * width + x] = (a << 24) + (r << 16) + (g << 8) + b; - } - } - return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); - } - - /** - * Returns the sum of the absolute differences between the expected and actual bitmaps, calculated - * using the maximum difference across all color channels for each pixel, then divided by the - * total number of pixels in the image. The bitmap resolutions must match and they must use - * configuration {@link Bitmap.Config#ARGB_8888}. - */ - private static float getAveragePixelAbsoluteDifferenceArgb8888(Bitmap expected, Bitmap actual) { - int width = actual.getWidth(); - int height = actual.getHeight(); - assertThat(width).isEqualTo(expected.getWidth()); - assertThat(height).isEqualTo(expected.getHeight()); - assertThat(actual.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888); - long sumMaximumAbsoluteDifferences = 0; - // Debug-only image diff without alpha. To use, set a breakpoint right before the method return - // to view the difference between the expected and actual bitmaps. A passing test should show - // an image that is completely black (color == 0). - Bitmap debugDiff = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int actualColor = actual.getPixel(x, y); - int expectedColor = expected.getPixel(x, y); - - int alphaDifference = abs(Color.alpha(actualColor) - Color.alpha(expectedColor)); - int redDifference = abs(Color.red(actualColor) - Color.red(expectedColor)); - int blueDifference = abs(Color.blue(actualColor) - Color.blue(expectedColor)); - int greenDifference = abs(Color.green(actualColor) - Color.green(expectedColor)); - debugDiff.setPixel(x, y, Color.rgb(redDifference, blueDifference, greenDifference)); - - int maximumAbsoluteDifference = 0; - maximumAbsoluteDifference = max(maximumAbsoluteDifference, alphaDifference); - maximumAbsoluteDifference = max(maximumAbsoluteDifference, redDifference); - maximumAbsoluteDifference = max(maximumAbsoluteDifference, blueDifference); - maximumAbsoluteDifference = max(maximumAbsoluteDifference, greenDifference); - - sumMaximumAbsoluteDifferences += maximumAbsoluteDifference; - } - } - return (float) sumMaximumAbsoluteDifferences / (width * height); - } - - /** - * Saves the {@link Bitmap} to the {@link Context#getCacheDir() cache directory} as a PNG. - * - *

    File name will be {@code _output.png}. - * - * @param testId Name of the test that produced the {@link Bitmap}. - * @param bitmap The {@link Bitmap} to save. - */ - private static void saveTestBitmapToCacheDirectory(String testId, Bitmap bitmap) { - File file = new File(getApplicationContext().getExternalCacheDir(), testId + "_output.png"); - try (FileOutputStream outputStream = new FileOutputStream(file)) { - bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream); - } catch (IOException e) { - Log.e(TAG, "Could not write Bitmap to file path: " + file.getAbsolutePath(), e); - } - } -} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorTest.java deleted file mode 100644 index e92c045b66..0000000000 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameEditorTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.transformer; - -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import android.content.Context; -import android.graphics.Matrix; -import android.graphics.SurfaceTexture; -import android.view.Surface; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test for {@link FrameEditor#create(Context, int, int, float, Matrix, Surface, boolean, - * Transformer.DebugViewProvider) creating} a {@link FrameEditor}. - */ -@RunWith(AndroidJUnit4.class) -public final class FrameEditorTest { - // TODO(b/212539951): Make this a robolectric test by e.g. updating shadows or adding a - // wrapper around GlUtil to allow the usage of mocks or fakes which don't need (Shadow)GLES20. - - @Test - public void create_withSupportedPixelWidthHeightRatio_completesSuccessfully() - throws TransformationException { - FrameEditor.create( - getApplicationContext(), - /* outputWidth= */ 200, - /* outputHeight= */ 100, - /* pixelWidthHeightRatio= */ 1, - new Matrix(), - new Surface(new SurfaceTexture(false)), - /* enableExperimentalHdrEditing= */ false, - Transformer.DebugViewProvider.NONE); - } - - @Test - public void create_withUnsupportedPixelWidthHeightRatio_throwsException() { - TransformationException exception = - assertThrows( - TransformationException.class, - () -> - FrameEditor.create( - getApplicationContext(), - /* outputWidth= */ 200, - /* outputHeight= */ 100, - /* pixelWidthHeightRatio= */ 2, - new Matrix(), - new Surface(new SurfaceTexture(false)), - /* enableExperimentalHdrEditing= */ false, - Transformer.DebugViewProvider.NONE)); - - assertThat(exception).hasCauseThat().isInstanceOf(UnsupportedOperationException.class); - assertThat(exception).hasCauseThat().hasMessageThat().contains("pixelWidthHeightRatio"); - } -} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java new file mode 100644 index 0000000000..78df45a3ad --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java @@ -0,0 +1,326 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.transformer.BitmapTestUtil.FIRST_FRAME_PNG_ASSET_STRING; +import static androidx.media3.transformer.BitmapTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE; +import static androidx.media3.transformer.BitmapTestUtil.REQUEST_OUTPUT_HEIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING; +import static androidx.media3.transformer.BitmapTestUtil.ROTATE45_SCALE_TO_FIT_EXPECTED_OUTPUT_PNG_ASSET_STRING; +import static androidx.media3.transformer.BitmapTestUtil.ROTATE_THEN_TRANSLATE_EXPECTED_OUTPUT_PNG_ASSET_STRING; +import static androidx.media3.transformer.BitmapTestUtil.TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING; +import static androidx.media3.transformer.BitmapTestUtil.TRANSLATE_THEN_ROTATE_EXPECTED_OUTPUT_PNG_ASSET_STRING; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.asList; + +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.PixelFormat; +import android.media.Image; +import android.media.ImageReader; +import android.media.MediaCodec; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.util.Size; +import androidx.annotation.Nullable; +import androidx.media3.common.MimeTypes; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Pixel test for frame processing via {@link FrameProcessorChain}. + * + *

    Expected images are taken from an emulator, so tests on different emulators or physical + * devices may fail. To test on other devices, please increase the {@link + * BitmapTestUtil#MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE} and/or inspect the saved output + * bitmaps. + */ +@RunWith(AndroidJUnit4.class) +public final class FrameProcessorChainPixelTest { + + /** Input video of which we only use the first frame. */ + private static final String INPUT_MP4_ASSET_STRING = "media/mp4/sample.mp4"; + /** Timeout for dequeueing buffers from the codec, in microseconds. */ + private static final int DEQUEUE_TIMEOUT_US = 5_000_000; + /** + * Time to wait for the decoded frame to populate the {@link FrameProcessorChain}'s input surface + * and the {@link FrameProcessorChain} to finish processing the frame, in milliseconds. + */ + private static final int FRAME_PROCESSING_WAIT_MS = 5000; + /** The ratio of width over height, for each pixel in a frame. */ + private static final float PIXEL_WIDTH_HEIGHT_RATIO = 1; + + private @MonotonicNonNull FrameProcessorChain frameProcessorChain; + private @MonotonicNonNull ImageReader outputImageReader; + private @MonotonicNonNull MediaFormat mediaFormat; + + @After + public void release() { + if (frameProcessorChain != null) { + frameProcessorChain.release(); + } + } + + @Test + public void processData_noEdits_producesExpectedOutput() throws Exception { + String testId = "processData_noEdits"; + setUpAndPrepareFirstFrame(); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(FIRST_FRAME_PNG_ASSET_STRING); + + Bitmap actualBitmap = processFirstFrameAndEnd(); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap, /* throwOnFailure= */ false); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void processData_withAdvancedFrameProcessor_translateRight_producesExpectedOutput() + throws Exception { + String testId = "processData_withAdvancedFrameProcessor_translateRight"; + Matrix translateRightMatrix = new Matrix(); + translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0); + GlFrameProcessor glFrameProcessor = + new AdvancedFrameProcessor(getApplicationContext(), translateRightMatrix); + setUpAndPrepareFirstFrame(glFrameProcessor); + Bitmap expectedBitmap = + BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); + + Bitmap actualBitmap = processFirstFrameAndEnd(); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap, /* throwOnFailure= */ false); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void processData_withAdvancedAndScaleToFitFrameProcessors_producesExpectedOutput() + throws Exception { + String testId = "processData_withAdvancedAndScaleToFitFrameProcessors"; + Matrix translateRightMatrix = new Matrix(); + translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0); + GlFrameProcessor translateRightFrameProcessor = + new AdvancedFrameProcessor(getApplicationContext(), translateRightMatrix); + GlFrameProcessor rotate45FrameProcessor = + new ScaleToFitFrameProcessor.Builder(getApplicationContext()) + .setRotationDegrees(45) + .build(); + setUpAndPrepareFirstFrame(translateRightFrameProcessor, rotate45FrameProcessor); + Bitmap expectedBitmap = + BitmapTestUtil.readBitmap(TRANSLATE_THEN_ROTATE_EXPECTED_OUTPUT_PNG_ASSET_STRING); + + Bitmap actualBitmap = processFirstFrameAndEnd(); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap, /* throwOnFailure= */ false); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void processData_withScaleToFitAndAdvancedFrameProcessors_producesExpectedOutput() + throws Exception { + String testId = "processData_withScaleToFitAndAdvancedFrameProcessors"; + GlFrameProcessor rotate45FrameProcessor = + new ScaleToFitFrameProcessor.Builder(getApplicationContext()) + .setRotationDegrees(45) + .build(); + Matrix translateRightMatrix = new Matrix(); + translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0); + GlFrameProcessor translateRightFrameProcessor = + new AdvancedFrameProcessor(getApplicationContext(), translateRightMatrix); + setUpAndPrepareFirstFrame(rotate45FrameProcessor, translateRightFrameProcessor); + Bitmap expectedBitmap = + BitmapTestUtil.readBitmap(ROTATE_THEN_TRANSLATE_EXPECTED_OUTPUT_PNG_ASSET_STRING); + + Bitmap actualBitmap = processFirstFrameAndEnd(); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap, /* throwOnFailure= */ false); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void + processData_withPresentationFrameProcessor_requestOutputHeight_producesExpectedOutput() + throws Exception { + String testId = "processData_withPresentationFrameProcessor_requestOutputHeight"; + GlFrameProcessor glFrameProcessor = + new PresentationFrameProcessor.Builder(getApplicationContext()).setResolution(480).build(); + setUpAndPrepareFirstFrame(glFrameProcessor); + Bitmap expectedBitmap = + BitmapTestUtil.readBitmap(REQUEST_OUTPUT_HEIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); + + Bitmap actualBitmap = processFirstFrameAndEnd(); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap, /* throwOnFailure= */ false); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void processData_withScaleToFitFrameProcessor_rotate45_producesExpectedOutput() + throws Exception { + String testId = "processData_withScaleToFitFrameProcessor_rotate45"; + GlFrameProcessor glFrameProcessor = + new ScaleToFitFrameProcessor.Builder(getApplicationContext()) + .setRotationDegrees(45) + .build(); + setUpAndPrepareFirstFrame(glFrameProcessor); + Bitmap expectedBitmap = + BitmapTestUtil.readBitmap(ROTATE45_SCALE_TO_FIT_EXPECTED_OUTPUT_PNG_ASSET_STRING); + + Bitmap actualBitmap = processFirstFrameAndEnd(); + + // TODO(b/207848601): switch to using proper tooling for testing against golden data. + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + BitmapTestUtil.saveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap, /* throwOnFailure= */ false); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + /** + * Set up and prepare the first frame from an input video, as well as relevant test + * infrastructure. The frame will be sent towards the {@link FrameProcessorChain}, and may be + * accessed on the {@link FrameProcessorChain}'s output {@code outputImageReader}. + * + * @param frameProcessors The {@link GlFrameProcessor GlFrameProcessors} that will apply changes + * to the input frame. + */ + private void setUpAndPrepareFirstFrame(GlFrameProcessor... frameProcessors) throws Exception { + // Set up the extractor to read the first video frame and get its format. + MediaExtractor mediaExtractor = new MediaExtractor(); + @Nullable MediaCodec mediaCodec = null; + Context context = getApplicationContext(); + try (AssetFileDescriptor afd = context.getAssets().openFd(INPUT_MP4_ASSET_STRING)) { + mediaExtractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); + for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { + if (MimeTypes.isVideo(mediaExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME))) { + mediaFormat = mediaExtractor.getTrackFormat(i); + mediaExtractor.selectTrack(i); + break; + } + } + + int inputWidth = checkNotNull(mediaFormat).getInteger(MediaFormat.KEY_WIDTH); + int inputHeight = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT); + frameProcessorChain = + FrameProcessorChain.create( + context, + PIXEL_WIDTH_HEIGHT_RATIO, + inputWidth, + inputHeight, + asList(frameProcessors), + /* enableExperimentalHdrEditing= */ false); + Size outputSize = frameProcessorChain.getOutputSize(); + outputImageReader = + ImageReader.newInstance( + outputSize.getWidth(), + outputSize.getHeight(), + PixelFormat.RGBA_8888, + /* maxImages= */ 1); + frameProcessorChain.setOutputSurface( + outputImageReader.getSurface(), + outputSize.getWidth(), + outputSize.getHeight(), + /* debugSurfaceView= */ null); + frameProcessorChain.registerInputFrame(); + + // Queue the first video frame from the extractor. + String mimeType = checkNotNull(mediaFormat.getString(MediaFormat.KEY_MIME)); + mediaCodec = MediaCodec.createDecoderByType(mimeType); + mediaCodec.configure( + mediaFormat, frameProcessorChain.getInputSurface(), /* crypto= */ null, /* flags= */ 0); + mediaCodec.start(); + int inputBufferIndex = mediaCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); + assertThat(inputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + ByteBuffer inputBuffer = checkNotNull(mediaCodec.getInputBuffers()[inputBufferIndex]); + int sampleSize = mediaExtractor.readSampleData(inputBuffer, /* offset= */ 0); + mediaCodec.queueInputBuffer( + inputBufferIndex, + /* offset= */ 0, + sampleSize, + mediaExtractor.getSampleTime(), + mediaExtractor.getSampleFlags()); + + // Queue an end-of-stream buffer to force the codec to produce output. + inputBufferIndex = mediaCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); + assertThat(inputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + mediaCodec.queueInputBuffer( + inputBufferIndex, + /* offset= */ 0, + /* size= */ 0, + /* presentationTimeUs= */ 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM); + + // Dequeue and render the output video frame. + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + int outputBufferIndex; + do { + outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIMEOUT_US); + assertThat(outputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); + } while (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED + || outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); + mediaCodec.releaseOutputBuffer(outputBufferIndex, /* render= */ true); + } finally { + mediaExtractor.release(); + if (mediaCodec != null) { + mediaCodec.release(); + } + } + } + + private Bitmap processFirstFrameAndEnd() throws InterruptedException, TransformationException { + checkNotNull(frameProcessorChain).signalEndOfInputStream(); + Thread.sleep(FRAME_PROCESSING_WAIT_MS); + assertThat(frameProcessorChain.isEnded()).isTrue(); + frameProcessorChain.getAndRethrowBackgroundExceptions(); + + Image frameProcessorChainOutputImage = checkNotNull(outputImageReader).acquireLatestImage(); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromRgba8888Image(frameProcessorChainOutputImage); + frameProcessorChainOutputImage.close(); + return actualBitmap; + } +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainTest.java new file mode 100644 index 0000000000..ee81429b44 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.content.Context; +import android.util.Size; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for creating and configuring a {@link FrameProcessorChain}. + * + *

    See {@link FrameProcessorChainPixelTest} for data processing tests. + */ +@RunWith(AndroidJUnit4.class) +public final class FrameProcessorChainTest { + + @Test + public void create_withSupportedPixelWidthHeightRatio_completesSuccessfully() + throws TransformationException { + Context context = getApplicationContext(); + + FrameProcessorChain.create( + context, + /* pixelWidthHeightRatio= */ 1, + /* inputWidth= */ 200, + /* inputHeight= */ 100, + /* frameProcessors= */ ImmutableList.of(), + /* enableExperimentalHdrEditing= */ false); + } + + @Test + public void create_withUnsupportedPixelWidthHeightRatio_throwsException() { + Context context = getApplicationContext(); + + TransformationException exception = + assertThrows( + TransformationException.class, + () -> + FrameProcessorChain.create( + context, + /* pixelWidthHeightRatio= */ 2, + /* inputWidth= */ 200, + /* inputHeight= */ 100, + /* frameProcessors= */ ImmutableList.of(), + /* enableExperimentalHdrEditing= */ false)); + + assertThat(exception).hasCauseThat().isInstanceOf(UnsupportedOperationException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("pixelWidthHeightRatio"); + } + + @Test + public void getOutputSize_withoutFrameProcessors_returnsInputSize() + throws TransformationException { + Size inputSize = new Size(200, 100); + FrameProcessorChain frameProcessorChain = + createFrameProcessorChainWithFakeFrameProcessors( + inputSize, /* frameProcessorOutputSizes= */ ImmutableList.of()); + + Size outputSize = frameProcessorChain.getOutputSize(); + + assertThat(outputSize).isEqualTo(inputSize); + } + + @Test + public void getOutputSize_withOneFrameProcessor_returnsItsOutputSize() + throws TransformationException { + Size inputSize = new Size(200, 100); + Size frameProcessorOutputSize = new Size(300, 250); + FrameProcessorChain frameProcessorChain = + createFrameProcessorChainWithFakeFrameProcessors( + inputSize, /* frameProcessorOutputSizes= */ ImmutableList.of(frameProcessorOutputSize)); + + Size frameProcessorChainOutputSize = frameProcessorChain.getOutputSize(); + + assertThat(frameProcessorChainOutputSize).isEqualTo(frameProcessorOutputSize); + } + + @Test + public void getOutputSize_withThreeFrameProcessors_returnsLastOutputSize() + throws TransformationException { + Size inputSize = new Size(200, 100); + Size outputSize1 = new Size(300, 250); + Size outputSize2 = new Size(400, 244); + Size outputSize3 = new Size(150, 160); + FrameProcessorChain frameProcessorChain = + createFrameProcessorChainWithFakeFrameProcessors( + inputSize, + /* frameProcessorOutputSizes= */ ImmutableList.of( + outputSize1, outputSize2, outputSize3)); + + Size frameProcessorChainOutputSize = frameProcessorChain.getOutputSize(); + + assertThat(frameProcessorChainOutputSize).isEqualTo(outputSize3); + } + + private static FrameProcessorChain createFrameProcessorChainWithFakeFrameProcessors( + Size inputSize, List frameProcessorOutputSizes) throws TransformationException { + ImmutableList.Builder frameProcessors = new ImmutableList.Builder<>(); + for (Size element : frameProcessorOutputSizes) { + frameProcessors.add(new FakeFrameProcessor(element)); + } + return FrameProcessorChain.create( + getApplicationContext(), + /* pixelWidthHeightRatio= */ 1, + inputSize.getWidth(), + inputSize.getHeight(), + frameProcessors.build(), + /* enableExperimentalHdrEditing= */ false); + } + + private static class FakeFrameProcessor implements GlFrameProcessor { + + private final Size outputSize; + + private FakeFrameProcessor(Size outputSize) { + this.outputSize = outputSize; + } + + @Override + public void initialize(int inputTexId, int inputWidth, int inputHeight) {} + + @Override + public Size getOutputSize() { + return outputSize; + } + + @Override + public void updateProgramAndDraw(long presentationTimeNs) {} + + @Override + public void release() {} + } +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/SsimHelper.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/SsimHelper.java new file mode 100644 index 0000000000..dc489d00fe --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/SsimHelper.java @@ -0,0 +1,447 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.lang.Math.pow; + +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.graphics.ImageFormat; +import android.media.Image; +import android.media.ImageReader; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.os.Handler; +import androidx.annotation.Nullable; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.ConditionVariable; +import androidx.media3.common.util.Util; +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A helper for calculating SSIM score for transcoded videos. + * + *

    SSIM (Structural Similarity) Index is a statistical measurement of the similarity between two + * images. The mean SSIM score (taken between multiple frames) of two videos is a metric to + * determine the similarity of the videos. SSIM does not measure the absolute difference of the two + * images like MSE (mean squared error), but rather outputs the human perceptual difference. A + * higher SSIM score signifies higher similarity, while a SSIM score of 1 means the two images are + * exactly the same. + */ +public final class SsimHelper { + + /** The default comparison interval. */ + public static final int DEFAULT_COMPARISON_INTERVAL = 11; + + private static final int IMAGE_AVAILABLE_TIMEOUT_MS = 10_000; + private static final int DECODED_IMAGE_CHANNEL_COUNT = 3; + + /** + * Returns the mean SSIM score between the expected and the actual video. + * + *

    The method compares every {@link #DEFAULT_COMPARISON_INTERVAL n-th} frame from both videos. + * + * @param context The {@link Context}. + * @param expectedVideoPath The path to the expected video file, must be in {@link + * Context#getAssets() Assets}. + * @param actualVideoPath The path to the actual video file. + * @throws IOException When unable to open the provided video paths. + */ + public static double calculate(Context context, String expectedVideoPath, String actualVideoPath) + throws IOException, InterruptedException { + VideoDecodingWrapper expectedDecodingWrapper = + new VideoDecodingWrapper(context, expectedVideoPath, DEFAULT_COMPARISON_INTERVAL); + VideoDecodingWrapper actualDecodingWrapper = + new VideoDecodingWrapper(context, actualVideoPath, DEFAULT_COMPARISON_INTERVAL); + double accumulatedSsim = 0.0; + int comparedImagesCount = 0; + try { + while (true) { + @Nullable Image expectedImage = expectedDecodingWrapper.runUntilComparisonFrameOrEnded(); + @Nullable Image actualImage = actualDecodingWrapper.runUntilComparisonFrameOrEnded(); + if (expectedImage == null) { + assertThat(actualImage).isNull(); + break; + } + checkNotNull(actualImage); + + int width = expectedImage.getWidth(); + int height = expectedImage.getHeight(); + assertThat(actualImage.getWidth()).isEqualTo(width); + assertThat(actualImage.getHeight()).isEqualTo(height); + try { + accumulatedSsim += + SsimCalculator.calculate( + extractLumaChannelBuffer(expectedImage), + extractLumaChannelBuffer(actualImage), + /* offset= */ 0, + /* stride= */ width, + width, + height); + } finally { + expectedImage.close(); + actualImage.close(); + } + comparedImagesCount++; + } + } finally { + expectedDecodingWrapper.close(); + actualDecodingWrapper.close(); + } + assertWithMessage("Input had no frames.").that(comparedImagesCount).isGreaterThan(0); + return accumulatedSsim / comparedImagesCount; + } + + /** + * Returns the buffer of the luma (Y) channel of the image. + * + * @param image The {@link Image} in YUV format. + */ + private static int[] extractLumaChannelBuffer(Image image) { + // This method is invoked on the main thread. + // `image` should contain YUV channels. + Image.Plane[] imagePlanes = image.getPlanes(); + assertThat(imagePlanes).hasLength(DECODED_IMAGE_CHANNEL_COUNT); + Image.Plane lumaPlane = imagePlanes[0]; + int rowStride = lumaPlane.getRowStride(); + int pixelStride = lumaPlane.getPixelStride(); + int width = image.getWidth(); + int height = image.getHeight(); + ByteBuffer lumaByteBuffer = lumaPlane.getBuffer(); + int[] lumaChannelBuffer = new int[width * height]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + lumaChannelBuffer[y * width + x] = lumaByteBuffer.get(y * rowStride + x * pixelStride); + } + } + return lumaChannelBuffer; + } + + private SsimHelper() { + // Prevent instantiation. + } + + private static final class VideoDecodingWrapper implements Closeable { + // Use ExoPlayer's 10ms timeout setting. In practise, the test durations from using timeouts of + // 1/10/100ms don't differ significantly. + private static final long DEQUEUE_TIMEOUT_US = 10_000; + // SSIM should be calculated using the luma (Y) channel, thus using the YUV color space. + private static final int IMAGE_READER_COLOR_SPACE = ImageFormat.YUV_420_888; + private static final int MEDIA_CODEC_COLOR_SPACE = + MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible; + private static final String ASSET_FILE_SCHEME = "asset:///"; + private static final int MAX_IMAGES_ALLOWED = 1; + + private final MediaCodec mediaCodec; + private final MediaExtractor mediaExtractor; + private final MediaCodec.BufferInfo bufferInfo; + private final ImageReader imageReader; + private final ConditionVariable imageAvailableConditionVariable; + private final int comparisonInterval; + + private boolean isCurrentFrameComparisonFrame; + private boolean hasReadEndOfInputStream; + private boolean queuedEndOfStreamToDecoder; + private boolean dequeuedAllDecodedFrames; + private int dequeuedFramesCount; + + /** + * Creates a new instance. + * + * @param context The {@link Context}. + * @param filePath The path to the video file. + * @param comparisonInterval The number of frames between the frames selected for comparison by + * SSIM. + * @throws IOException When failed to open the video file. + */ + public VideoDecodingWrapper(Context context, String filePath, int comparisonInterval) + throws IOException { + this.comparisonInterval = comparisonInterval; + mediaExtractor = new MediaExtractor(); + bufferInfo = new MediaCodec.BufferInfo(); + + if (filePath.contains(ASSET_FILE_SCHEME)) { + AssetFileDescriptor assetFd = + context.getAssets().openFd(filePath.replace(ASSET_FILE_SCHEME, "")); + mediaExtractor.setDataSource( + assetFd.getFileDescriptor(), assetFd.getStartOffset(), assetFd.getLength()); + } else { + mediaExtractor.setDataSource(filePath); + } + + @Nullable MediaFormat mediaFormat = null; + for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { + if (MimeTypes.isVideo(mediaExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME))) { + mediaFormat = mediaExtractor.getTrackFormat(i); + mediaExtractor.selectTrack(i); + break; + } + } + + checkStateNotNull(mediaFormat); + checkState(mediaFormat.containsKey(MediaFormat.KEY_WIDTH)); + int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH); + checkState(mediaFormat.containsKey(MediaFormat.KEY_HEIGHT)); + int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT); + + // Create a handler for the main thread to receive image available notifications. The current + // (test) thread blocks until this callback is received. + Handler mainThreadHandler = Util.createHandlerForCurrentOrMainLooper(); + imageAvailableConditionVariable = new ConditionVariable(); + imageReader = + ImageReader.newInstance(width, height, IMAGE_READER_COLOR_SPACE, MAX_IMAGES_ALLOWED); + imageReader.setOnImageAvailableListener( + imageReader -> imageAvailableConditionVariable.open(), mainThreadHandler); + + String sampleMimeType = checkNotNull(mediaFormat.getString(MediaFormat.KEY_MIME)); + mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MEDIA_CODEC_COLOR_SPACE); + mediaCodec = MediaCodec.createDecoderByType(sampleMimeType); + mediaCodec.configure( + mediaFormat, imageReader.getSurface(), /* crypto= */ null, /* flags= */ 0); + mediaCodec.start(); + } + + /** + * Returns the next decoded comparison frame, or {@code null} if the stream has ended. The + * caller takes ownership of any returned image and is responsible for closing it before calling + * this method again. + */ + @Nullable + public Image runUntilComparisonFrameOrEnded() throws InterruptedException { + while (!hasEnded() && !isCurrentFrameComparisonFrame) { + while (dequeueOneFrameFromDecoder()) {} + while (queueOneFrameToDecoder()) {} + } + if (isCurrentFrameComparisonFrame) { + isCurrentFrameComparisonFrame = false; + assertThat(imageAvailableConditionVariable.block(IMAGE_AVAILABLE_TIMEOUT_MS)).isTrue(); + imageAvailableConditionVariable.close(); + return imageReader.acquireLatestImage(); + } + return null; + } + + /** Returns whether decoding has ended. */ + private boolean hasEnded() { + return dequeuedAllDecodedFrames; + } + + /** Returns whether a frame is queued to the {@link MediaCodec decoder}. */ + private boolean queueOneFrameToDecoder() { + if (queuedEndOfStreamToDecoder) { + return false; + } + + int inputBufferIndex = mediaCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); + if (inputBufferIndex < 0) { + return false; + } + + if (hasReadEndOfInputStream) { + mediaCodec.queueInputBuffer( + inputBufferIndex, + /* offset= */ 0, + /* size= */ 0, + /* presentationTimeUs= */ 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM); + queuedEndOfStreamToDecoder = true; + return false; + } + + ByteBuffer inputBuffer = checkNotNull(mediaCodec.getInputBuffer(inputBufferIndex)); + int sampleSize = mediaExtractor.readSampleData(inputBuffer, /* offset= */ 0); + mediaCodec.queueInputBuffer( + inputBufferIndex, + /* offset= */ 0, + sampleSize, + mediaExtractor.getSampleTime(), + mediaExtractor.getSampleFlags()); + // MediaExtractor.advance does not reliably return false for end-of-stream, so check sample + // metadata instead as a more reliable signal. See [internal: b/121204004]. + mediaExtractor.advance(); + hasReadEndOfInputStream = mediaExtractor.getSampleTime() == -1; + return true; + } + + /** Returns whether a frame is decoded, renders the frame if the frame is a comparison frame. */ + private boolean dequeueOneFrameFromDecoder() { + if (isCurrentFrameComparisonFrame) { + return false; + } + + int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIMEOUT_US); + if (outputBufferIndex <= 0) { + return false; + } + isCurrentFrameComparisonFrame = dequeuedFramesCount % comparisonInterval == 0; + dequeuedFramesCount++; + mediaCodec.releaseOutputBuffer( + outputBufferIndex, /* render= */ isCurrentFrameComparisonFrame); + + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + dequeuedAllDecodedFrames = true; + } + return true; + } + + @Override + public void close() { + mediaExtractor.release(); + mediaCodec.release(); + imageReader.close(); + } + } + + /** + * Image comparison using the Structural Similarity Index, developed by Wang, Bovik, Sheikh, and + * Simoncelli. + * + * @see The SSIM paper. + */ + private static final class SsimCalculator { + // These values were taken from the SSIM paper. Please see the linked paper for details. + private static final double IMAGE_DYNAMIC_RANGE = 255; + private static final double K1 = 0.01; + private static final double K2 = 0.03; + private static final double C1 = pow(IMAGE_DYNAMIC_RANGE * K1, 2); + private static final double C2 = pow(IMAGE_DYNAMIC_RANGE * K2, 2); + private static final int WINDOW_SIZE = 8; + + /** + * Calculates the Structural Similarity Index (SSIM) between two images. + * + * @param expected The luminance channel (Y) bitmap of the expected image. + * @param actual The luminance channel (Y) bitmap of the actual image. + * @param offset The offset. + * @param stride The stride of the bitmap. + * @param width The image width in pixels. + * @param height The image height in pixels. + * @return The SSIM score between the input images. + */ + public static double calculate( + int[] expected, int[] actual, int offset, int stride, int width, int height) { + double totalSsim = 0; + int windowsCount = 0; + + // X refers to the expected image, while Y refers to the actual image. + for (int currentWindowY = 0; currentWindowY < height; currentWindowY += WINDOW_SIZE) { + int windowHeight = computeWindowSize(currentWindowY, height); + for (int currentWindowX = 0; currentWindowX < width; currentWindowX += WINDOW_SIZE) { + windowsCount++; + int windowWidth = computeWindowSize(currentWindowX, width); + int start = getGlobalCoordinate(currentWindowX, currentWindowY, stride, offset); + double meanX = getMean(expected, start, stride, windowWidth, windowHeight); + double meanY = getMean(actual, start, stride, windowWidth, windowHeight); + + double[] variances = + getVariancesAndCovariance( + expected, actual, meanX, meanY, start, stride, windowWidth, windowHeight); + // varX is the variance of window X, covXY is the covariance between window X and Y. + double varX = variances[0]; + double varY = variances[1]; + double covXY = variances[2]; + + totalSsim += getWindowSsim(meanX, meanY, varX, varY, covXY); + } + } + + if (windowsCount == 0) { + return 1.0d; + } + + return totalSsim / windowsCount; + } + + /** + * Returns the window size at the provided start coordinate, uses {@link #WINDOW_SIZE} if there + * is enough space, otherwise the number of pixels between {@code start} and {@code dimension}. + */ + private static int computeWindowSize(int start, int dimension) { + if (start + WINDOW_SIZE <= dimension) { + return WINDOW_SIZE; + } + return dimension - start; + } + + /** Returns the SSIM of a window. */ + private static double getWindowSsim( + double meanX, double meanY, double varX, double varY, double covXY) { + + // Uses equation 13 on page 6 from the linked paper. + double numerator = (((2 * meanX * meanY) + C1) * ((2 * covXY) + C2)); + double denominator = ((meanX * meanX) + (meanY * meanY) + C1) * (varX + varY + C2); + return numerator / denominator; + } + + /** Returns the means of the pixels in the two provided windows, in order. */ + private static double getMean( + int[] pixels, int start, int stride, int windowWidth, int windowHeight) { + double total = 0; + for (int y = 0; y < windowHeight; y++) { + for (int x = 0; x < windowWidth; x++) { + total += pixels[getGlobalCoordinate(x, y, stride, start)]; + } + } + return total / windowWidth * windowHeight; + } + + /** Returns the two variances and the covariance of the two windows. */ + private static double[] getVariancesAndCovariance( + int[] pixelsX, + int[] pixelsY, + double meanX, + double meanY, + int start, + int stride, + int windowWidth, + int windowHeight) { + // The variances in X and Y. + double varX = 0; + double varY = 0; + // The covariance between X and Y. + double covXY = 0; + for (int y = 0; y < windowHeight; y++) { + for (int x = 0; x < windowWidth; x++) { + int index = getGlobalCoordinate(x, y, stride, start); + double offsetX = pixelsX[index] - meanX; + double offsetY = pixelsY[index] - meanY; + varX += pow(offsetX, 2); + varY += pow(offsetY, 2); + covXY += offsetX * offsetY; + } + } + + int normalizationFactor = windowWidth * windowHeight - 1; + return new double[] { + varX / normalizationFactor, varY / normalizationFactor, covXY / normalizationFactor + }; + } + + private static int getGlobalCoordinate(int x, int y, int stride, int offset) { + return x + (y * stride) + offset; + } + } +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java new file mode 100644 index 0000000000..c2bd3241e2 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java @@ -0,0 +1,173 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import org.json.JSONException; +import org.json.JSONObject; + +/** A test only class for holding the details of a test transformation. */ +public class TransformationTestResult { + /** Represents an unset or unknown SSIM score. */ + public static final double SSIM_UNSET = -1.0d; + + /** A builder for {@link TransformationTestResult}. */ + public static class Builder { + private final TransformationResult transformationResult; + + @Nullable private String filePath; + @Nullable private Exception analysisException; + private long elapsedTimeMs; + private double ssim; + + /** Creates a new {@link Builder}. */ + public Builder(TransformationResult transformationResult) { + this.transformationResult = transformationResult; + this.elapsedTimeMs = C.TIME_UNSET; + this.ssim = SSIM_UNSET; + } + + /** + * Sets the file path of the output file. + * + *

    {@code null} represents an unset or unknown value. + * + * @param filePath The path. + * @return This {@link Builder}. + */ + public Builder setFilePath(@Nullable String filePath) { + this.filePath = filePath; + return this; + } + + /** + * Sets the amount of time taken to perform the transformation in milliseconds. {@link + * C#TIME_UNSET} if unset. + * + *

    {@link C#TIME_UNSET} represents an unset or unknown value. + * + * @param elapsedTimeMs The time, in ms. + * @return This {@link Builder}. + */ + public Builder setElapsedTimeMs(long elapsedTimeMs) { + this.elapsedTimeMs = elapsedTimeMs; + return this; + } + + /** + * Sets the SSIM of the output file, compared to input file. + * + *

    {@link #SSIM_UNSET} represents an unset or unknown value. + * + * @param ssim The structural similarity index. + * @return This {@link Builder}. + */ + public Builder setSsim(double ssim) { + this.ssim = ssim; + return this; + } + + /** + * Sets an {@link Exception} that occurred during post-transformation analysis. + * + *

    {@code null} represents an unset or unknown value. + * + * @param analysisException The {@link Exception} thrown during analysis. + * @return This {@link Builder}. + */ + public Builder setAnalysisException(@Nullable Exception analysisException) { + this.analysisException = analysisException; + return this; + } + + /** Builds the {@link TransformationTestResult} instance. */ + public TransformationTestResult build() { + return new TransformationTestResult( + transformationResult, filePath, elapsedTimeMs, ssim, analysisException); + } + } + + public final TransformationResult transformationResult; + @Nullable public final String filePath; + /** + * The average rate (per second) at which frames are processed by the transformer, or {@link + * C#RATE_UNSET} if unset or unknown. + */ + public final float throughputFps; + /** + * The amount of time taken to perform the transformation in milliseconds. {@link C#TIME_UNSET} if + * unset. + */ + public final long elapsedTimeMs; + /** The SSIM score of the transformation, {@link #SSIM_UNSET} if unavailable. */ + public final double ssim; + /** + * The {@link Exception} that was thrown during post-transformation analysis, or {@code null} if + * nothing was thrown. + */ + @Nullable public final Exception analysisException; + + /** Returns a {@link JSONObject} representing all the values in {@code this}. */ + public JSONObject asJsonObject() throws JSONException { + JSONObject jsonObject = new JSONObject(); + if (transformationResult.durationMs != C.LENGTH_UNSET) { + jsonObject.put("durationMs", transformationResult.durationMs); + } + if (transformationResult.fileSizeBytes != C.LENGTH_UNSET) { + jsonObject.put("fileSizeBytes", transformationResult.fileSizeBytes); + } + if (transformationResult.averageAudioBitrate != C.RATE_UNSET_INT) { + jsonObject.put("averageAudioBitrate", transformationResult.averageAudioBitrate); + } + if (transformationResult.averageVideoBitrate != C.RATE_UNSET_INT) { + jsonObject.put("averageVideoBitrate", transformationResult.averageVideoBitrate); + } + if (transformationResult.videoFrameCount > 0) { + jsonObject.put("videoFrameCount", transformationResult.videoFrameCount); + } + if (throughputFps != C.RATE_UNSET) { + jsonObject.put("throughputFps", throughputFps); + } + if (elapsedTimeMs != C.TIME_UNSET) { + jsonObject.put("elapsedTimeMs", elapsedTimeMs); + } + if (ssim != TransformationTestResult.SSIM_UNSET) { + jsonObject.put("ssim", ssim); + } + if (analysisException != null) { + jsonObject.put("analysisException", AndroidTestUtil.exceptionAsJsonObject(analysisException)); + } + return jsonObject; + } + + private TransformationTestResult( + TransformationResult transformationResult, + @Nullable String filePath, + long elapsedTimeMs, + double ssim, + @Nullable Exception analysisException) { + this.transformationResult = transformationResult; + this.filePath = filePath; + this.elapsedTimeMs = elapsedTimeMs; + this.ssim = ssim; + this.analysisException = analysisException; + this.throughputFps = + elapsedTimeMs != C.TIME_UNSET && transformationResult.videoFrameCount > 0 + ? 1000f * transformationResult.videoFrameCount / elapsedTimeMs + : C.RATE_UNSET; + } +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java new file mode 100644 index 0000000000..8fd685129e --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java @@ -0,0 +1,306 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkNotNull; +import static java.util.concurrent.TimeUnit.SECONDS; + +import android.content.Context; +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.SystemClock; +import androidx.test.platform.app.InstrumentationRegistry; +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.json.JSONObject; + +/** An android instrumentation test runner for {@link Transformer}. */ +public class TransformerAndroidTestRunner { + private static final String TAG_PREFIX = "TransformerAndroidTest_"; + + /** The default transformation timeout value. */ + public static final int DEFAULT_TIMEOUT_SECONDS = 120; + + /** A {@link Builder} for {@link TransformerAndroidTestRunner} instances. */ + public static class Builder { + private final Context context; + private final Transformer transformer; + private boolean calculateSsim; + private int timeoutSeconds; + private boolean suppressAnalysisExceptions; + @Nullable private Map inputValues; + + /** + * Creates a {@link Builder}. + * + * @param context The {@link Context}. + * @param transformer The {@link Transformer} that performs the transformation. + */ + public Builder(Context context, Transformer transformer) { + this.context = context; + this.transformer = transformer; + this.timeoutSeconds = DEFAULT_TIMEOUT_SECONDS; + } + + /** + * Sets the timeout in seconds for a single transformation. An exception is thrown when this is + * exceeded. + * + *

    The default value is {@link #DEFAULT_TIMEOUT_SECONDS}. + * + * @param timeoutSeconds The timeout. + * @return This {@link Builder}. + */ + public Builder setTimeoutSeconds(int timeoutSeconds) { + this.timeoutSeconds = timeoutSeconds; + return this; + } + + /** + * Sets whether to calculate the SSIM of the transformation output. + * + *

    The calculation involves decoding and comparing both the input and the output video. + * Consequently this calculation is not cost-free. Requires the input and output video to be the + * same size. + * + *

    The default value is {@code false}. + * + * @param calculateSsim Whether to calculate SSIM. + * @return This {@link Builder}. + */ + public Builder setCalculateSsim(boolean calculateSsim) { + this.calculateSsim = calculateSsim; + return this; + } + + /** + * Sets whether the runner should suppress any {@link Exception} that occurs as a result of + * post-transformation analysis, such as SSIM calculation. + * + *

    Regardless of this value, analysis exceptions are attached to the analysis file. + * + *

    It's recommended to add a comment explaining why this suppression is needed, ideally with + * a bug number. + * + *

    The default value is {@code false}. + * + * @param suppressAnalysisExceptions Whether to suppress analysis exceptions. + * @return This {@link Builder}. + */ + public Builder setSuppressAnalysisExceptions(boolean suppressAnalysisExceptions) { + this.suppressAnalysisExceptions = suppressAnalysisExceptions; + return this; + } + + /** + * Sets a {@link Map} of transformer input values, which are propagated to the transformation + * summary JSON file. + * + *

    Values in the map should be convertible according to {@link JSONObject#wrap(Object)} to be + * recorded properly in the summary file. + * + * @param inputValues A {@link Map} of values to be written to the transformation summary. + * @return This {@link Builder}. + */ + public Builder setInputValues(@Nullable Map inputValues) { + this.inputValues = inputValues; + return this; + } + + /** Builds the {@link TransformerAndroidTestRunner}. */ + public TransformerAndroidTestRunner build() { + return new TransformerAndroidTestRunner( + context, + transformer, + timeoutSeconds, + calculateSsim, + suppressAnalysisExceptions, + inputValues); + } + } + + private final Context context; + private final Transformer transformer; + private final int timeoutSeconds; + private final boolean calculateSsim; + private final boolean suppressAnalysisExceptions; + @Nullable private final Map inputValues; + + private TransformerAndroidTestRunner( + Context context, + Transformer transformer, + int timeoutSeconds, + boolean calculateSsim, + boolean suppressAnalysisExceptions, + @Nullable Map inputValues) { + this.context = context; + this.transformer = transformer; + this.timeoutSeconds = timeoutSeconds; + this.calculateSsim = calculateSsim; + this.suppressAnalysisExceptions = suppressAnalysisExceptions; + this.inputValues = inputValues; + } + + /** + * Transforms the {@code uriString}, saving a summary of the transformation to the application + * cache. + * + * @param testId A unique identifier for the transformer test run. + * @param uriString The uri (as a {@link String}) of the file to transform. + * @return The {@link TransformationTestResult}. + * @throws Exception The cause of the transformation not completing. + */ + public TransformationTestResult run(String testId, String uriString) throws Exception { + JSONObject resultJson = new JSONObject(); + if (inputValues != null) { + resultJson.put("inputValues", JSONObject.wrap(inputValues)); + } + try { + TransformationTestResult transformationTestResult = runInternal(testId, uriString); + resultJson.put("transformationResult", transformationTestResult.asJsonObject()); + if (!suppressAnalysisExceptions && transformationTestResult.analysisException != null) { + throw transformationTestResult.analysisException; + } + return transformationTestResult; + } catch (Exception e) { + resultJson.put("exception", AndroidTestUtil.exceptionAsJsonObject(e)); + throw e; + } finally { + AndroidTestUtil.writeTestSummaryToFile(context, testId, resultJson); + } + } + + /** + * Transforms the {@code uriString}. + * + * @param testId An identifier for the test. + * @param uriString The uri (as a {@link String}) of the file to transform. + * @return The {@link TransformationTestResult}. + * @throws IOException If an error occurs opening the output file for writing + * @throws TimeoutException If the transformation takes longer than the {@link #timeoutSeconds}. + * @throws InterruptedException If the thread is interrupted whilst waiting for transformer to + * complete. + * @throws TransformationException If an exception occurs as a result of the transformation. + * @throws IllegalArgumentException If the path is invalid. + * @throws IllegalStateException If an unexpected exception occurs when starting a transformation. + */ + private TransformationTestResult runInternal(String testId, String uriString) + throws InterruptedException, IOException, TimeoutException, TransformationException { + AtomicReference<@NullableType TransformationException> transformationExceptionReference = + new AtomicReference<>(); + AtomicReference<@NullableType Exception> unexpectedExceptionReference = new AtomicReference<>(); + AtomicReference<@NullableType TransformationResult> transformationResultReference = + new AtomicReference<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + long startTimeMs = SystemClock.DEFAULT.elapsedRealtime(); + + Transformer testTransformer = + transformer + .buildUpon() + .addListener( + new Transformer.Listener() { + @Override + public void onTransformationCompleted( + MediaItem inputMediaItem, TransformationResult result) { + transformationResultReference.set(result); + countDownLatch.countDown(); + } + + @Override + public void onTransformationError( + MediaItem inputMediaItem, TransformationException exception) { + transformationExceptionReference.set(exception); + countDownLatch.countDown(); + } + }) + .build(); + + Uri uri = Uri.parse(uriString); + File outputVideoFile = + AndroidTestUtil.createExternalCacheFile(context, /* fileName= */ testId + "-output.mp4"); + InstrumentationRegistry.getInstrumentation() + .runOnMainSync( + () -> { + try { + testTransformer.startTransformation( + MediaItem.fromUri(uri), outputVideoFile.getAbsolutePath()); + // Catch all exceptions to report. Exceptions thrown here and not caught will NOT + // propagate. + } catch (Exception e) { + unexpectedExceptionReference.set(e); + countDownLatch.countDown(); + } + }); + + if (!countDownLatch.await(timeoutSeconds, SECONDS)) { + throw new TimeoutException("Transformer timed out after " + timeoutSeconds + " seconds."); + } + long elapsedTimeMs = SystemClock.DEFAULT.elapsedRealtime() - startTimeMs; + + @Nullable Exception unexpectedException = unexpectedExceptionReference.get(); + if (unexpectedException != null) { + throw new IllegalStateException( + "Unexpected exception starting the transformer.", unexpectedException); + } + + @Nullable + TransformationException transformationException = transformationExceptionReference.get(); + if (transformationException != null) { + throw transformationException; + } + + // If both exceptions are null, the Transformation must have succeeded, and a + // transformationResult will be available. + TransformationResult transformationResult = + checkNotNull(transformationResultReference.get()) + .buildUpon() + .setFileSizeBytes(outputVideoFile.length()) + .build(); + + TransformationTestResult.Builder resultBuilder = + new TransformationTestResult.Builder(transformationResult) + .setFilePath(outputVideoFile.getPath()) + .setElapsedTimeMs(elapsedTimeMs); + + try { + if (calculateSsim) { + double ssim = + SsimHelper.calculate( + context, /* expectedVideoPath= */ uriString, outputVideoFile.getPath()); + resultBuilder.setSsim(ssim); + } + } catch (InterruptedException interruptedException) { + // InterruptedException is a special unexpected case because it is not related to Ssim + // calculation, so it should be thrown, rather than processed as part of the + // TransformationTestResult. + throw interruptedException; + } catch (Exception analysisException) { + // Catch all (checked and unchecked) exceptions throw by the SsimHelper and process them as + // part of the TransformationTestResult. + resultBuilder.setAnalysisException(analysisException); + Log.e(TAG_PREFIX + testId, "SSIM calculation failed.", analysisException); + } + + return resultBuilder.build(); + } +} 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 104c5e7272..d1a9182e87 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -15,13 +15,10 @@ */ package androidx.media3.transformer; -import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.transformer.AndroidTestUtil.runTransformer; +import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING; import static com.google.common.truth.Truth.assertThat; import android.content.Context; -import android.graphics.Matrix; -import androidx.media3.common.MimeTypes; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; @@ -34,52 +31,13 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class TransformerEndToEndTest { - private static final String VP9_VIDEO_URI_STRING = "asset:///media/vp9/bear-vp9.webm"; - private static final String AVC_VIDEO_URI_STRING = "asset:///media/mp4/sample.mp4"; - - @Test - public void videoTranscoding_completesWithConsistentFrameCount() throws Exception { - Context context = ApplicationProvider.getApplicationContext(); - FrameCountingMuxer.Factory muxerFactory = - new FrameCountingMuxer.Factory(new FrameworkMuxer.Factory()); - Transformer transformer = - new Transformer.Builder(context) - .setTransformationRequest( - new TransformationRequest.Builder().setVideoMimeType(MimeTypes.VIDEO_H264).build()) - .setMuxerFactory(muxerFactory) - .setEncoderFactory( - new DefaultEncoderFactory(EncoderSelector.DEFAULT, /* enableFallback= */ false)) - .build(); - // Result of the following command: - // ffprobe -count_frames -select_streams v:0 -show_entries stream=nb_read_frames bear-vp9.webm - int expectedFrameCount = 82; - - runTransformer( - context, - /* testId= */ "videoTranscoding_completesWithConsistentFrameCount", - transformer, - VP9_VIDEO_URI_STRING, - /* timeoutSeconds= */ 120); - - FrameCountingMuxer frameCountingMuxer = - checkNotNull(muxerFactory.getLastFrameCountingMuxerCreated()); - assertThat(frameCountingMuxer.getFrameCount()).isEqualTo(expectedFrameCount); - } - @Test public void videoEditing_completesWithConsistentFrameCount() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - Matrix transformationMatrix = new Matrix(); - transformationMatrix.postTranslate(/* dx= */ .2f, /* dy= */ .1f); - FrameCountingMuxer.Factory muxerFactory = - new FrameCountingMuxer.Factory(new FrameworkMuxer.Factory()); Transformer transformer = new Transformer.Builder(context) .setTransformationRequest( - new TransformationRequest.Builder() - .setTransformationMatrix(transformationMatrix) - .build()) - .setMuxerFactory(muxerFactory) + new TransformationRequest.Builder().setResolution(480).build()) .setEncoderFactory( new DefaultEncoderFactory(EncoderSelector.DEFAULT, /* enableFallback= */ false)) .build(); @@ -87,15 +45,34 @@ public class TransformerEndToEndTest { // ffprobe -count_frames -select_streams v:0 -show_entries stream=nb_read_frames sample.mp4 int expectedFrameCount = 30; - runTransformer( - context, - /* testId= */ "videoEditing_completesWithConsistentFrameCount", - transformer, - AVC_VIDEO_URI_STRING, - /* timeoutSeconds= */ 120); + TransformationTestResult result = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run( + /* testId= */ "videoEditing_completesWithConsistentFrameCount", + MP4_ASSET_URI_STRING); - FrameCountingMuxer frameCountingMuxer = - checkNotNull(muxerFactory.getLastFrameCountingMuxerCreated()); - assertThat(frameCountingMuxer.getFrameCount()).isEqualTo(expectedFrameCount); + assertThat(result.transformationResult.videoFrameCount).isEqualTo(expectedFrameCount); + } + + @Test + public void videoOnly_completesWithConsistentDuration() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder(context) + .setRemoveAudio(true) + .setTransformationRequest( + new TransformationRequest.Builder().setResolution(480).build()) + .setEncoderFactory( + new DefaultEncoderFactory(EncoderSelector.DEFAULT, /* enableFallback= */ false)) + .build(); + long expectedDurationMs = 967; + + TransformationTestResult result = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(/* testId= */ "videoOnly_completesWithConsistentDuration", MP4_ASSET_URI_STRING); + + assertThat(result.transformationResult.durationMs).isEqualTo(expectedDurationMs); } } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RemoveAudioTransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RemoveAudioTransformationTest.java deleted file mode 100644 index 2e6dbab20a..0000000000 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RemoveAudioTransformationTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.transformer.mh; - -import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING; -import static androidx.media3.transformer.AndroidTestUtil.runTransformer; - -import android.content.Context; -import androidx.media3.common.util.Log; -import androidx.media3.common.util.Util; -import androidx.media3.transformer.Transformer; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** {@link Transformer} instrumentation test for removing audio. */ -@RunWith(AndroidJUnit4.class) -public class RemoveAudioTransformationTest { - - private static final String TAG = "RemoveAudioTransformationTest"; - - @Test - public void removeAudioTransform() throws Exception { - if (Util.SDK_INT < 25) { - // TODO(b/210593256): Remove test skipping after removing the MediaMuxer dependency. - Log.i(TAG, "Skipping on this API version due to lack of muxing support"); - return; - } - - Context context = ApplicationProvider.getApplicationContext(); - Transformer transformer = new Transformer.Builder(context).setRemoveAudio(true).build(); - runTransformer( - context, - /* testId= */ "removeAudioTransform", - transformer, - MP4_ASSET_URI_STRING, - /* timeoutSeconds= */ 120); - } -} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java index a893c4c420..a02be1f3e9 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java @@ -16,55 +16,51 @@ package androidx.media3.transformer.mh; import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.transformer.AndroidTestUtil.runTransformer; import static com.google.common.truth.Truth.assertWithMessage; import android.content.Context; -import android.graphics.Matrix; import androidx.media3.common.MimeTypes; import androidx.media3.transformer.AndroidTestUtil; -import androidx.media3.transformer.TestTransformationResult; import androidx.media3.transformer.TransformationRequest; +import androidx.media3.transformer.TransformationTestResult; import androidx.media3.transformer.Transformer; +import androidx.media3.transformer.TransformerAndroidTestRunner; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.HashSet; import java.util.Set; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; /** Tests repeated transcoding operations (as a stress test and to help reproduce flakiness). */ @RunWith(AndroidJUnit4.class) -@Ignore("Internal - b/206917996") public final class RepeatedTranscodeTransformationTest { private static final int TRANSCODE_COUNT = 10; @Test public void repeatedTranscode_givesConsistentLengthOutput() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - Matrix transformationMatrix = new Matrix(); - transformationMatrix.postTranslate((float) 0.1, (float) 0.1); - Transformer transformer = - new Transformer.Builder(context) - .setTransformationRequest( - new TransformationRequest.Builder() - .setVideoMimeType(MimeTypes.VIDEO_H265) - .setTransformationMatrix(transformationMatrix) - .setAudioMimeType(MimeTypes.AUDIO_AMR_NB) + + TransformerAndroidTestRunner transformerRunner = + new TransformerAndroidTestRunner.Builder( + context, + new Transformer.Builder(context) + .setTransformationRequest( + new TransformationRequest.Builder() + .setRotationDegrees(45) + // Video MIME type is H264. + .setAudioMimeType(MimeTypes.AUDIO_AAC) + .build()) .build()) .build(); Set differentOutputSizesBytes = new HashSet<>(); for (int i = 0; i < TRANSCODE_COUNT; i++) { // Use a long video in case an error occurs a while after the start of the video. - TestTransformationResult testResult = - runTransformer( - context, + TransformationTestResult testResult = + transformerRunner.run( /* testId= */ "repeatedTranscode_givesConsistentLengthOutput_" + i, - transformer, - AndroidTestUtil.REMOTE_MP4_10_SECONDS_URI_STRING, - /* timeoutSeconds= */ 120); + AndroidTestUtil.MP4_REMOTE_H264_MP3_URI_STRING); differentOutputSizesBytes.add(checkNotNull(testResult.transformationResult.fileSizeBytes)); } @@ -77,28 +73,26 @@ public final class RepeatedTranscodeTransformationTest { @Test public void repeatedTranscodeNoAudio_givesConsistentLengthOutput() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - Matrix transformationMatrix = new Matrix(); - transformationMatrix.postTranslate((float) 0.1, (float) 0.1); - Transformer transformer = - new Transformer.Builder(context) - .setRemoveAudio(true) - .setTransformationRequest( - new TransformationRequest.Builder() - .setVideoMimeType(MimeTypes.VIDEO_H265) - .setTransformationMatrix(transformationMatrix) + TransformerAndroidTestRunner transformerRunner = + new TransformerAndroidTestRunner.Builder( + context, + new Transformer.Builder(context) + .setRemoveAudio(true) + .setTransformationRequest( + new TransformationRequest.Builder() + // Video MIME type is H264. + .setRotationDegrees(45) + .build()) .build()) .build(); Set differentOutputSizesBytes = new HashSet<>(); for (int i = 0; i < TRANSCODE_COUNT; i++) { // Use a long video in case an error occurs a while after the start of the video. - TestTransformationResult testResult = - runTransformer( - context, + TransformationTestResult testResult = + transformerRunner.run( /* testId= */ "repeatedTranscodeNoAudio_givesConsistentLengthOutput_" + i, - transformer, - AndroidTestUtil.REMOTE_MP4_10_SECONDS_URI_STRING, - /* timeoutSeconds= */ 120); + AndroidTestUtil.MP4_REMOTE_H264_MP3_URI_STRING); differentOutputSizesBytes.add(checkNotNull(testResult.transformationResult.fileSizeBytes)); } @@ -111,25 +105,25 @@ public final class RepeatedTranscodeTransformationTest { @Test public void repeatedTranscodeNoVideo_givesConsistentLengthOutput() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - Transformer transformer = - new Transformer.Builder(context) - .setRemoveVideo(true) - .setTransformationRequest( - new TransformationRequest.Builder() - .setAudioMimeType(MimeTypes.AUDIO_AMR_NB) + TransformerAndroidTestRunner transformerRunner = + new TransformerAndroidTestRunner.Builder( + context, + new Transformer.Builder(context) + .setRemoveVideo(true) + .setTransformationRequest( + new TransformationRequest.Builder() + .setAudioMimeType(MimeTypes.AUDIO_AAC) + .build()) .build()) .build(); Set differentOutputSizesBytes = new HashSet<>(); for (int i = 0; i < TRANSCODE_COUNT; i++) { // Use a long video in case an error occurs a while after the start of the video. - TestTransformationResult testResult = - runTransformer( - context, + TransformationTestResult testResult = + transformerRunner.run( /* testId= */ "repeatedTranscodeNoVideo_givesConsistentLengthOutput_" + i, - transformer, - AndroidTestUtil.REMOTE_MP4_10_SECONDS_URI_STRING, - /* timeoutSeconds= */ 120); + AndroidTestUtil.MP4_REMOTE_H264_MP3_URI_STRING); differentOutputSizesBytes.add(checkNotNull(testResult.transformationResult.fileSizeBytes)); } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SefTransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SefTransformationTest.java deleted file mode 100644 index 165e48e30d..0000000000 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SefTransformationTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.transformer.mh; - -import static androidx.media3.transformer.AndroidTestUtil.SEF_ASSET_URI_STRING; -import static androidx.media3.transformer.AndroidTestUtil.runTransformer; - -import android.content.Context; -import androidx.media3.common.util.Log; -import androidx.media3.common.util.Util; -import androidx.media3.transformer.TransformationRequest; -import androidx.media3.transformer.Transformer; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** {@link Transformer} instrumentation test for SEF. */ -@RunWith(AndroidJUnit4.class) -public class SefTransformationTest { - - private static final String TAG = "SefTransformationTest"; - - @Test - public void sefTransform() throws Exception { - if (Util.SDK_INT < 25) { - // TODO(b/210593256): Remove test skipping after removing the MediaMuxer dependency. - Log.i(TAG, "Skipping on this API version due to lack of muxing support"); - return; - } - - Context context = ApplicationProvider.getApplicationContext(); - Transformer transformer = - new Transformer.Builder(context) - .setTransformationRequest( - new TransformationRequest.Builder().setFlattenForSlowMotion(true).build()) - .build(); - runTransformer( - context, - /* testId = */ "sefTransform", - transformer, - SEF_ASSET_URI_STRING, - /* timeoutSeconds= */ 120); - } -} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RemoveVideoTransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SetFrameEditTransformationTest.java similarity index 58% rename from libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RemoveVideoTransformationTest.java rename to libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SetFrameEditTransformationTest.java index b77cfa08e9..92bb7b3d16 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RemoveVideoTransformationTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SetFrameEditTransformationTest.java @@ -15,28 +15,32 @@ */ package androidx.media3.transformer.mh; -import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING; -import static androidx.media3.transformer.AndroidTestUtil.runTransformer; +import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING; import android.content.Context; +import androidx.media3.transformer.TransformationRequest; import androidx.media3.transformer.Transformer; +import androidx.media3.transformer.TransformerAndroidTestRunner; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; -/** {@link Transformer} instrumentation test for removing video. */ +/** {@link Transformer} instrumentation test for applying a frame edit. */ @RunWith(AndroidJUnit4.class) -public class RemoveVideoTransformationTest { +public class SetFrameEditTransformationTest { @Test - public void removeVideoTransform() throws Exception { + public void setFrameEditTransform() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - Transformer transformer = new Transformer.Builder(context).setRemoveVideo(true).build(); - runTransformer( - context, - /* testId= */ "removeVideoTransform", - transformer, - MP4_ASSET_URI_STRING, - /* timeoutSeconds= */ 120); + Transformer transformer = + new Transformer.Builder(context) + .setTransformationRequest( + new TransformationRequest.Builder().setRotationDegrees(45).build()) + .build(); + + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run( + /* testId= */ "SetFrameEditTransform", MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING); } } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SetTransformationMatrixTransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeQualityTest.java similarity index 55% rename from libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SetTransformationMatrixTransformationTest.java rename to libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeQualityTest.java index 89533baee3..2db30956a9 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SetTransformationMatrixTransformationTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeQualityTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Android Open Source Project + * Copyright 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,41 +13,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package androidx.media3.transformer.mh; -import static androidx.media3.transformer.AndroidTestUtil.REMOTE_MP4_10_SECONDS_URI_STRING; -import static androidx.media3.transformer.AndroidTestUtil.runTransformer; +import static com.google.common.truth.Truth.assertThat; import android.content.Context; -import android.graphics.Matrix; +import androidx.media3.common.MimeTypes; +import androidx.media3.transformer.AndroidTestUtil; import androidx.media3.transformer.TransformationRequest; +import androidx.media3.transformer.TransformationTestResult; import androidx.media3.transformer.Transformer; +import androidx.media3.transformer.TransformerAndroidTestRunner; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; -/** {@link Transformer} instrumentation test for setting a transformation matrix. */ +/** Checks transcoding quality. */ @RunWith(AndroidJUnit4.class) -public class SetTransformationMatrixTransformationTest { +public final class TranscodeQualityTest { @Test - public void setTransformationMatrixTransform() throws Exception { + public void singleTranscode_ssimIsGreaterThan95Percent() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - Matrix transformationMatrix = new Matrix(); - transformationMatrix.postTranslate(/* dx= */ .2f, /* dy= */ .1f); Transformer transformer = new Transformer.Builder(context) .setTransformationRequest( - new TransformationRequest.Builder() - .setTransformationMatrix(transformationMatrix) - .build()) + new TransformationRequest.Builder().setVideoMimeType(MimeTypes.VIDEO_H265).build()) + .setRemoveAudio(true) .build(); - runTransformer( - context, - /* testId= */ "setTransformationMatrixTransform", - transformer, - REMOTE_MP4_10_SECONDS_URI_STRING, - /* timeoutSeconds= */ 120); + TransformationTestResult result = + new TransformerAndroidTestRunner.Builder(context, transformer) + .setCalculateSsim(true) + .build() + .run(/* testId= */ "singleTranscode_ssim", AndroidTestUtil.MP4_ASSET_URI_STRING); + + assertThat(result.ssim).isGreaterThan(0.95); } } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java index adeb10aec7..1457584391 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java @@ -15,19 +15,27 @@ */ package androidx.media3.transformer.mh; +import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_SEF_URI_STRING; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING; -import static androidx.media3.transformer.AndroidTestUtil.runTransformer; +import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_4K60_PORTRAIT_URI_STRING; +import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped; import android.content.Context; -import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; +import androidx.media3.transformer.AndroidTestUtil; +import androidx.media3.transformer.DefaultEncoderFactory; +import androidx.media3.transformer.EncoderSelector; +import androidx.media3.transformer.TransformationRequest; import androidx.media3.transformer.Transformer; +import androidx.media3.transformer.TransformerAndroidTestRunner; +import androidx.media3.transformer.VideoEncoderSettings; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; -/** {@link Transformer} instrumentation test. */ +/** {@link Transformer} instrumentation tests. */ @RunWith(AndroidJUnit4.class) public class TransformationTest { @@ -35,19 +43,101 @@ public class TransformationTest { @Test public void transform() throws Exception { + String testId = TAG + "_transform"; + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = new Transformer.Builder(context).build(); + new TransformerAndroidTestRunner.Builder(context, transformer) + .setCalculateSsim(true) + .build() + .run(testId, MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING); + } + + @Test + public void transformWithDecodeEncode() throws Exception { + String testId = TAG + "_transformForceCodecUse"; + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder(context) + .setEncoderFactory(AndroidTestUtil.FORCE_ENCODE_ENCODER_FACTORY) + .build(); + new TransformerAndroidTestRunner.Builder(context, transformer) + .setCalculateSsim(true) + .build() + .run(testId, MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING); + } + + @Test + public void transformToSpecificBitrate() throws Exception { + String testId = TAG + "_transformWithSpecificBitrate"; + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder(context) + .setRemoveAudio(true) + .setEncoderFactory( + new DefaultEncoderFactory( + EncoderSelector.DEFAULT, + new VideoEncoderSettings.Builder().setBitrate(5_000_000).build(), + /* enableFallback= */ true)) + .build(); + new TransformerAndroidTestRunner.Builder(context, transformer) + .setCalculateSsim(true) + .build() + .run(testId, MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING); + } + + @Test + public void transform4K60() throws Exception { + String testId = TAG + "_transform4K60"; + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = new Transformer.Builder(context).build(); + new TransformerAndroidTestRunner.Builder(context, transformer) + .setCalculateSsim(true) + .build() + .run(testId, MP4_REMOTE_4K60_PORTRAIT_URI_STRING); + } + + @Test + public void transformNoAudio() throws Exception { + String testId = TAG + "_transformNoAudio"; + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = new Transformer.Builder(context).setRemoveAudio(true).build(); + new TransformerAndroidTestRunner.Builder(context, transformer) + .setCalculateSsim(true) + .build() + .run(testId, MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING); + } + + @Test + public void transformNoVideo() throws Exception { + String testId = TAG + "_transformNoVideo"; + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = new Transformer.Builder(context).setRemoveVideo(true).build(); + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, MP4_ASSET_URI_STRING); + } + + @Test + public void transformSef() throws Exception { + String testId = TAG + "_transformSef"; + Context context = ApplicationProvider.getApplicationContext(); + if (Util.SDK_INT < 25) { // TODO(b/210593256): Remove test skipping after removing the MediaMuxer dependency. - Log.i(TAG, "Skipping on this API version due to lack of muxing support"); + recordTestSkipped( + context, + testId, + /* reason= */ "Skipping on this API version due to lack of muxing support"); return; } - Context context = ApplicationProvider.getApplicationContext(); - Transformer transformer = new Transformer.Builder(context).build(); - runTransformer( - context, - /* testId= */ "transform", - transformer, - MP4_ASSET_URI_STRING, - /* timeoutSeconds= */ 120); + Transformer transformer = + new Transformer.Builder(context) + .setTransformationRequest( + new TransformationRequest.Builder().setFlattenForSlowMotion(true).build()) + .build(); + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, MP4_ASSET_SEF_URI_STRING); } } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/BitrateAnalysisTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/BitrateAnalysisTest.java new file mode 100644 index 0000000000..c6fd8e09f3 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/BitrateAnalysisTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer.mh.analysis; + +import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR; +import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR; + +import android.content.Context; +import androidx.media3.common.util.Assertions; +import androidx.media3.transformer.AndroidTestUtil; +import androidx.media3.transformer.DefaultEncoderFactory; +import androidx.media3.transformer.EncoderSelector; +import androidx.media3.transformer.Transformer; +import androidx.media3.transformer.TransformerAndroidTestRunner; +import androidx.media3.transformer.VideoEncoderSettings; +import androidx.test.core.app.ApplicationProvider; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** Instrumentation tests for analysing output bitrate and quality for a given input bitrate. */ +@RunWith(Parameterized.class) +public class BitrateAnalysisTest { + private static final ImmutableList INPUT_FILES = + ImmutableList.of( + AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING, + AndroidTestUtil.MP4_REMOTE_4K60_PORTRAIT_URI_STRING); + private static final ImmutableList INPUT_BITRATE_MODES = + ImmutableList.of(BITRATE_MODE_VBR, BITRATE_MODE_CBR); + + private static final int START_BITRATE = 2_000_000; + private static final int END_BITRATE = 10_000_000; + private static final int BITRATE_INTERVAL = 1_000_000; + + @Parameter(0) + public int bitrate; + + @Parameter(1) + public int bitrateMode; + + @Parameter(2) + public @MonotonicNonNull String fileUri; + + @Parameters(name = "analyzeBitrate_{0}_{1}_{2}") + public static List parameters() { + List parameterList = new ArrayList<>(); + for (int bitrate = START_BITRATE; bitrate <= END_BITRATE; bitrate += BITRATE_INTERVAL) { + for (int mode : INPUT_BITRATE_MODES) { + for (String file : INPUT_FILES) { + parameterList.add(new Object[] {bitrate, mode, file}); + } + } + } + + return parameterList; + } + + @Test + public void analyzeBitrate() throws Exception { + Assertions.checkNotNull(fileUri); + String fileName = Assertions.checkNotNull(Iterables.getLast(Splitter.on("/").split(fileUri))); + String testId = String.format("analyzeBitrate_ssim_%s_%d_%s", bitrate, bitrateMode, fileName); + + Map inputValues = new HashMap<>(); + inputValues.put("targetBitrate", bitrate); + inputValues.put("inputFilename", fileName); + if (bitrateMode == BITRATE_MODE_CBR) { + inputValues.put("bitrateMode", "CBR"); + } else if (bitrateMode == BITRATE_MODE_VBR) { + inputValues.put("bitrateMode", "VBR"); + } + + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder(context) + .setRemoveAudio(true) + .setEncoderFactory( + new DefaultEncoderFactory( + EncoderSelector.DEFAULT, + new VideoEncoderSettings.Builder() + .setBitrate(bitrate) + .setBitrateMode(bitrateMode) + .build(), + /* enableFallback= */ false)) + .build(); + + inputValues.put("Transformer", transformer); + + new TransformerAndroidTestRunner.Builder(context, transformer) + .setInputValues(inputValues) + .setCalculateSsim(true) + .build() + .run(testId, fileUri); + } +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java new file mode 100644 index 0000000000..9a289ab944 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.transformer.mh.analysis; + +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.content.Context; +import android.media.MediaFormat; +import android.net.Uri; +import androidx.media3.common.util.Util; +import androidx.media3.transformer.AndroidTestUtil; +import androidx.media3.transformer.DefaultEncoderFactory; +import androidx.media3.transformer.EncoderSelector; +import androidx.media3.transformer.Transformer; +import androidx.media3.transformer.TransformerAndroidTestRunner; +import androidx.media3.transformer.VideoEncoderSettings; +import androidx.test.core.app.ApplicationProvider; +import com.google.common.collect.ImmutableList; +import java.util.HashMap; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** Instrumentation tests for analyzing encoder performance settings. */ +@RunWith(Parameterized.class) +public class EncoderPerformanceAnalysisTest { + + /** A non-realtime {@link MediaFormat#KEY_PRIORITY encoder priority}. */ + private static final int MEDIA_CODEC_PRIORITY_NON_REALTIME = 0; + /** A realtime {@link MediaFormat#KEY_PRIORITY encoder priority}. */ + private static final int MEDIA_CODEC_PRIORITY_REALTIME = 1; + + private static final ImmutableList INPUT_FILES = + ImmutableList.of( + AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING, + AndroidTestUtil.MP4_REMOTE_4K60_PORTRAIT_URI_STRING); + + private static final ImmutableList OPERATING_RATE_SETTINGS = + ImmutableList.of(VideoEncoderSettings.NO_VALUE, 30, Integer.MAX_VALUE); + + private static final ImmutableList PRIORITY_SETTINGS = + ImmutableList.of( + // Use NO_VALUE to skip setting priority. + VideoEncoderSettings.NO_VALUE, + MEDIA_CODEC_PRIORITY_NON_REALTIME, + MEDIA_CODEC_PRIORITY_REALTIME); + + @Parameter(0) + public @MonotonicNonNull String fileUri; + + @Parameter(1) + public int operatingRate; + + @Parameter(2) + public int priority; + + @Parameters(name = "analyzePerformance_{0}_OpRate={1}_Priority={2}") + public static ImmutableList parameters() { + ImmutableList.Builder parametersBuilder = new ImmutableList.Builder<>(); + for (int i = 0; i < INPUT_FILES.size(); i++) { + for (int j = 0; j < OPERATING_RATE_SETTINGS.size(); j++) { + for (int k = 0; k < PRIORITY_SETTINGS.size(); k++) { + parametersBuilder.add( + new Object[] { + INPUT_FILES.get(i), OPERATING_RATE_SETTINGS.get(j), PRIORITY_SETTINGS.get(k) + }); + } + } + } + return parametersBuilder.build(); + } + + @Test + public void analyzeEncoderPerformance() throws Exception { + checkNotNull(fileUri); + String filename = checkNotNull(Uri.parse(fileUri).getLastPathSegment()); + String testId = + Util.formatInvariant( + "analyzePerformance_%s_OpRate_%d_Priority_%d", filename, operatingRate, priority); + + Map inputValues = new HashMap<>(); + inputValues.put("inputFilename", filename); + inputValues.put("operatingRate", operatingRate); + inputValues.put("priority", priority); + + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder(context) + .setRemoveAudio(true) + .setEncoderFactory( + new DefaultEncoderFactory( + EncoderSelector.DEFAULT, + new VideoEncoderSettings.Builder() + .setEncoderPerformanceParameters(operatingRate, priority) + .build(), + /* enableFallback= */ false)) + .build(); + + new TransformerAndroidTestRunner.Builder(context, transformer) + .setInputValues(inputValues) + .build() + .run(testId, fileUri); + } +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TestTransformationResult.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/package-info.java similarity index 57% rename from libraries/transformer/src/androidTest/java/androidx/media3/transformer/TestTransformationResult.java rename to libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/package-info.java index da45d55714..f624be94c1 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TestTransformationResult.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/package-info.java @@ -13,15 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.media3.transformer; +@NonNullApi +package androidx.media3.transformer.mh.analysis; -/** A test only class for holding additional details alongside a {@link TransformationResult}. */ -public class TestTransformationResult { - public final TransformationResult transformationResult; - public final String filePath; - - public TestTransformationResult(TransformationResult transformationResult, String filePath) { - this.transformationResult = transformationResult; - this.filePath = filePath; - } -} +import androidx.media3.common.util.NonNullApi; diff --git a/libraries/transformer/src/main/assets/shaders/fragment_shader_copy_es2.glsl b/libraries/transformer/src/main/assets/shaders/fragment_shader_copy_es2.glsl new file mode 100644 index 0000000000..83dba1af26 --- /dev/null +++ b/libraries/transformer/src/main/assets/shaders/fragment_shader_copy_es2.glsl @@ -0,0 +1,24 @@ +#version 100 +// Copyright 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ES 2 fragment shader that samples from a (non-external) texture with uTexSampler, +// copying from this texture to the current output. + +precision mediump float; +uniform sampler2D uTexSampler; +varying vec2 vTexSamplingCoord; +void main() { + gl_FragColor = texture2D(uTexSampler, vTexSamplingCoord); +} diff --git a/libraries/transformer/src/main/assets/shaders/fragment_shader_copy_external.glsl b/libraries/transformer/src/main/assets/shaders/fragment_shader_copy_external_es2.glsl similarity index 81% rename from libraries/transformer/src/main/assets/shaders/fragment_shader_copy_external.glsl rename to libraries/transformer/src/main/assets/shaders/fragment_shader_copy_external_es2.glsl index 17667e636d..ed021e44e3 100644 --- a/libraries/transformer/src/main/assets/shaders/fragment_shader_copy_external.glsl +++ b/libraries/transformer/src/main/assets/shaders/fragment_shader_copy_external_es2.glsl @@ -1,3 +1,4 @@ +#version 100 // Copyright 2021 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,13 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Fragment shader that samples from an external texture with uTexSampler, +// ES 2 fragment shader that samples from an external texture with uTexSampler, // copying from this texture to the current output. #extension GL_OES_EGL_image_external : require precision mediump float; uniform samplerExternalOES uTexSampler; -varying vec2 vTexCoords; +varying vec2 vTexSamplingCoord; void main() { - gl_FragColor = texture2D(uTexSampler, vTexCoords); + gl_FragColor = texture2D(uTexSampler, vTexSamplingCoord); } diff --git a/libraries/transformer/src/main/assets/shaders/fragment_shader_copy_external_yuv_es3.glsl b/libraries/transformer/src/main/assets/shaders/fragment_shader_copy_external_yuv_es3.glsl index 05b1168d3e..12f59e3c05 100644 --- a/libraries/transformer/src/main/assets/shaders/fragment_shader_copy_external_yuv_es3.glsl +++ b/libraries/transformer/src/main/assets/shaders/fragment_shader_copy_external_yuv_es3.glsl @@ -24,10 +24,10 @@ precision mediump float; uniform __samplerExternal2DY2YEXT uTexSampler; uniform mat3 uColorTransform; -in vec2 vTexCoords; +in vec2 vTexSamplingCoord; out vec4 outColor; void main() { - vec3 srcYuv = texture(uTexSampler, vTexCoords).xyz; + vec3 srcYuv = texture(uTexSampler, vTexSamplingCoord).xyz; vec3 yuvOffset; yuvOffset.x = srcYuv.r - 0.0625; yuvOffset.y = srcYuv.g - 0.5; diff --git a/libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es2.glsl b/libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es2.glsl new file mode 100644 index 0000000000..f521d2fedb --- /dev/null +++ b/libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es2.glsl @@ -0,0 +1,27 @@ +#version 100 +// Copyright 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ES 2 vertex shader that applies an external surface texture's 4 * 4 texture +// transformation matrix to convert the texture coordinates to the sampling +// locations. + +attribute vec4 aFramePosition; +attribute vec4 aTexSamplingCoord; +uniform mat4 uTexTransform; +varying vec2 vTexSamplingCoord; +void main() { + gl_Position = aFramePosition; + vTexSamplingCoord = (uTexTransform * aTexSamplingCoord).xy; +} diff --git a/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es3.glsl b/libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es3.glsl similarity index 68% rename from libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es3.glsl rename to libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es3.glsl index 56da553bad..00dd9bc711 100644 --- a/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es3.glsl +++ b/libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es3.glsl @@ -13,15 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -// ES 3 vertex shader that applies the 4 * 4 transformation matrix -// uTransformationMatrix. +// ES 3 vertex shader that applies an external surface texture's 4 * 4 texture +// transformation matrix to convert the texture coordinates to the sampling +// locations. in vec4 aFramePosition; -in vec4 aTexCoords; +in vec4 aTexSamplingCoord; uniform mat4 uTexTransform; -uniform mat4 uTransformationMatrix; -out vec2 vTexCoords; +out vec2 vTexSamplingCoord; void main() { - gl_Position = uTransformationMatrix * aFramePosition; - vTexCoords = (uTexTransform * aTexCoords).xy; + gl_Position = aFramePosition; + vTexSamplingCoord = (uTexTransform * aTexSamplingCoord).xy; } diff --git a/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation.glsl b/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es2.glsl similarity index 80% rename from libraries/transformer/src/main/assets/shaders/vertex_shader_transformation.glsl rename to libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es2.glsl index 1268e9265c..5ac512a0cb 100644 --- a/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation.glsl +++ b/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es2.glsl @@ -1,3 +1,4 @@ +#version 100 // Copyright 2021 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,15 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Vertex shader that applies the 4 * 4 transformation matrix +// ES 2 vertex shader that applies the 4 * 4 transformation matrix // uTransformationMatrix. attribute vec4 aFramePosition; -attribute vec4 aTexCoords; -uniform mat4 uTexTransform; +attribute vec4 aTexSamplingCoord; uniform mat4 uTransformationMatrix; -varying vec2 vTexCoords; +varying vec2 vTexSamplingCoord; void main() { gl_Position = uTransformationMatrix * aFramePosition; - vTexCoords = (uTexTransform * aTexCoords).xy; + vTexSamplingCoord = aTexSamplingCoord.xy; } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java new file mode 100644 index 0000000000..583be441e3 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java @@ -0,0 +1,160 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import android.content.Context; +import android.graphics.Matrix; +import android.opengl.GLES20; +import android.util.Size; +import androidx.media3.common.util.GlProgram; +import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.UnstableApi; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Applies a transformation matrix in the vertex shader. + * + *

    Operations are done on normalized device coordinates (-1 to 1 on x and y axes). No automatic + * adjustments (like done in {@link ScaleToFitFrameProcessor}) are applied on the transformation. + * Width and height are not modified. The background color will default to black. + */ +@UnstableApi +public final class AdvancedFrameProcessor implements GlFrameProcessor { + + static { + GlUtil.glAssertionsEnabled = true; + } + + private static final String VERTEX_SHADER_TRANSFORMATION_PATH = + "shaders/vertex_shader_transformation_es2.glsl"; + private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_copy_es2.glsl"; + + /** + * Returns a 4x4, column-major {@link android.opengl.Matrix} float array, from an input {@link + * Matrix}. + * + *

    This is useful for converting to the 4x4 column-major format commonly used in OpenGL. + */ + private static float[] getGlMatrixArray(Matrix matrix) { + float[] matrix3x3Array = new float[9]; + matrix.getValues(matrix3x3Array); + float[] matrix4x4Array = getMatrix4x4Array(matrix3x3Array); + + // Transpose from row-major to column-major representations. + float[] transposedMatrix4x4Array = new float[16]; + android.opengl.Matrix.transposeM( + transposedMatrix4x4Array, /* mTransOffset= */ 0, matrix4x4Array, /* mOffset= */ 0); + + return transposedMatrix4x4Array; + } + + /** + * Returns a 4x4 matrix array containing the 3x3 matrix array's contents. + * + *

    The 3x3 matrix array is expected to be in 2 dimensions, and the 4x4 matrix array is expected + * to be in 3 dimensions. The output will have the third row/column's values be an identity + * matrix's values, so that vertex transformations using this matrix will not affect the z axis. + *
    + * Input format: [a, b, c, d, e, f, g, h, i]
    + * Output format: [a, b, 0, c, d, e, 0, f, 0, 0, 1, 0, g, h, 0, i] + */ + private static float[] getMatrix4x4Array(float[] matrix3x3Array) { + float[] matrix4x4Array = new float[16]; + matrix4x4Array[10] = 1; + for (int inputRow = 0; inputRow < 3; inputRow++) { + for (int inputColumn = 0; inputColumn < 3; inputColumn++) { + int outputRow = (inputRow == 2) ? 3 : inputRow; + int outputColumn = (inputColumn == 2) ? 3 : inputColumn; + matrix4x4Array[outputRow * 4 + outputColumn] = matrix3x3Array[inputRow * 3 + inputColumn]; + } + } + return matrix4x4Array; + } + + private final Context context; + private final float[] transformationMatrix; + + private @MonotonicNonNull Size size; + private @MonotonicNonNull GlProgram glProgram; + + /** + * Creates a new instance. + * + * @param context The {@link Context}. + * @param transformationMatrix The transformation {@link Matrix} to apply to each frame. + * Operations are done on normalized device coordinates (-1 to 1 on x and y), and no automatic + * adjustments are applied on the transformation matrix. + */ + public AdvancedFrameProcessor(Context context, Matrix transformationMatrix) { + this(context, getGlMatrixArray(transformationMatrix)); + } + + /** + * Creates a new instance. + * + * @param context The {@link Context}. + * @param transformationMatrix The 4x4 transformation {@link android.opengl.Matrix} to apply to + * each frame. Operations are done on normalized device coordinates (-1 to 1 on x and y), and + * no automatic adjustments are applied on the transformation matrix. + */ + public AdvancedFrameProcessor(Context context, float[] transformationMatrix) { + checkArgument( + transformationMatrix.length == 16, "A 4x4 transformation matrix must have 16 elements."); + this.context = context; + this.transformationMatrix = transformationMatrix.clone(); + } + + @Override + public void initialize(int inputTexId, int inputWidth, int inputHeight) throws IOException { + size = new Size(inputWidth, inputHeight); + // TODO(b/205002913): check the loaded program is consistent with the attributes and uniforms + // expected in the code. + glProgram = new GlProgram(context, VERTEX_SHADER_TRANSFORMATION_PATH, FRAGMENT_SHADER_PATH); + glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0); + // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. + glProgram.setBufferAttribute( + "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + glProgram.setBufferAttribute( + "aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + glProgram.setFloatsUniform("uTransformationMatrix", transformationMatrix); + } + + @Override + public Size getOutputSize() { + return checkStateNotNull(size); + } + + @Override + public void updateProgramAndDraw(long presentationTimeUs) { + checkStateNotNull(glProgram); + glProgram.use(); + glProgram.bindAttributesAndUniforms(); + // The four-vertex triangle strip forms a quad. + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); + GlUtil.checkGlError(); + } + + @Override + public void release() { + if (glProgram != null) { + glProgram.delete(); + } + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java index 9f821c9f10..9e9f3b3baa 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java @@ -36,7 +36,7 @@ import java.util.List; @UnstableApi public interface Codec { - /** A factory for {@link Codec decoder} instances. */ + /** A factory for {@linkplain Codec decoder} instances. */ interface DecoderFactory { /** A default {@code DecoderFactory} implementation. */ @@ -58,14 +58,16 @@ public interface Codec { * @param format The {@link Format} (of the input data) used to determine the underlying decoder * and its configuration values. * @param outputSurface The {@link Surface} to which the decoder output is rendered. + * @param enableRequestSdrToneMapping Whether to request tone-mapping to SDR. * @return A {@link Codec} for video decoding. * @throws TransformationException If no suitable {@link Codec} can be created. */ - Codec createForVideoDecoding(Format format, Surface outputSurface) + Codec createForVideoDecoding( + Format format, Surface outputSurface, boolean enableRequestSdrToneMapping) throws TransformationException; } - /** A factory for {@link Codec encoder} instances. */ + /** A factory for {@linkplain Codec encoder} instances. */ interface EncoderFactory { /** A default {@code EncoderFactory} implementation. */ @@ -75,13 +77,13 @@ public interface Codec { * Returns a {@link Codec} for audio encoding. * *

    This method must validate that the {@link Codec} is configured to produce one of the - * {@code allowedMimeTypes}. The {@link Format#sampleMimeType sample MIME type} given in {@code - * format} is not necessarily allowed. + * {@code allowedMimeTypes}. The {@linkplain Format#sampleMimeType sample MIME type} given in + * {@code format} is not necessarily allowed. * * @param format The {@link Format} (of the output data) used to determine the underlying * encoder and its configuration values. - * @param allowedMimeTypes The non-empty list of allowed output sample {@link MimeTypes MIME - * types}. + * @param allowedMimeTypes The non-empty list of allowed output sample {@linkplain MimeTypes + * MIME types}. * @return A {@link Codec} for audio encoding. * @throws TransformationException If no suitable {@link Codec} can be created. */ @@ -92,20 +94,32 @@ public interface Codec { * Returns a {@link Codec} for video encoding. * *

    This method must validate that the {@link Codec} is configured to produce one of the - * {@code allowedMimeTypes}. The {@link Format#sampleMimeType sample MIME type} given in {@code - * format} is not necessarily allowed. + * {@code allowedMimeTypes}. The {@linkplain Format#sampleMimeType sample MIME type} given in + * {@code format} is not necessarily allowed. * * @param format The {@link Format} (of the output data) used to determine the underlying * encoder and its configuration values. {@link Format#sampleMimeType}, {@link Format#width} - * and {@link Format#height} must be set to those of the desired output video format. {@link - * Format#rotationDegrees} should be 0. The video should always be in landscape orientation. - * @param allowedMimeTypes The non-empty list of allowed output sample {@link MimeTypes MIME - * types}. + * and {@link Format#height} are set to those of the desired output video format. {@link + * Format#rotationDegrees} is 0 and {@link Format#width} {@code >=} {@link Format#height}, + * therefore the video is always in landscape orientation. {@link Format#frameRate} is set + * to the output video's frame rate, if available. + * @param allowedMimeTypes The non-empty list of allowed output sample {@linkplain MimeTypes + * MIME types}. * @return A {@link Codec} for video encoding. * @throws TransformationException If no suitable {@link Codec} can be created. */ Codec createForVideoEncoding(Format format, List allowedMimeTypes) throws TransformationException; + + /** Returns whether the audio needs to be encoded because of encoder specific configuration. */ + default boolean audioNeedsEncoding() { + return false; + } + + /** Returns whether the video needs to be encoded because of encoder specific configuration. */ + default boolean videoNeedsEncoding() { + return false; + } } /** @@ -128,8 +142,8 @@ public interface Codec { /** * Dequeues a writable input buffer, if available. * - *

    This method must not be called from video encoders because they must use {@link Surface - * surfaces} as inputs. + *

    This method must not be called from video encoders because they must use a {@link Surface} + * to receive input. * * @param inputBuffer The buffer where the dequeued buffer data is stored, at {@link * DecoderInputBuffer#data inputBuffer.data}. @@ -139,13 +153,13 @@ public interface Codec { boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer) throws TransformationException; /** - * Queues an input buffer to the {@code Codec}. No buffers may be queued after {@link + * Queues an input buffer to the {@code Codec}. No buffers may be queued after {@linkplain * DecoderInputBuffer#isEndOfStream() end of stream} buffer has been queued. * - *

    This method must not be called from video encoders because they must use {@link Surface - * surfaces} as inputs. + *

    This method must not be called from video encoders because they must use a {@link Surface} + * to receive input. * - * @param inputBuffer The {@link DecoderInputBuffer input buffer}. + * @param inputBuffer The {@linkplain DecoderInputBuffer input buffer}. * @throws TransformationException If the underlying decoder or encoder encounters a problem. */ void queueInputBuffer(DecoderInputBuffer inputBuffer) throws TransformationException; @@ -155,7 +169,8 @@ public interface Codec { * *

    This method must only be called on video encoders because they must use a {@link Surface} as * input. For audio/video decoders or audio encoders, the {@link C#BUFFER_FLAG_END_OF_STREAM} flag - * should be set on the last input buffer {@link #queueInputBuffer(DecoderInputBuffer) queued}. + * should be set on the last input buffer {@linkplain #queueInputBuffer(DecoderInputBuffer) + * queued}. * * @throws TransformationException If the underlying video encoder encounters a problem. */ diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java index 72c3155424..3280df2da4 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java @@ -68,16 +68,14 @@ public final class DefaultCodec implements Codec { * {@code null}. * @param configurationMediaFormat The {@link MediaFormat} to configure the underlying {@link * MediaCodec}. - * @param mediaCodecName The name of a specific {@link MediaCodec} to instantiate. If {@code - * null}, {@code DefaultCodec} uses {@link Format#sampleMimeType - * configurationFormat.sampleMimeType} to create the underlying {@link MediaCodec codec}. + * @param mediaCodecName The name of a specific {@link MediaCodec} to instantiate. * @param isDecoder Whether the {@code DefaultCodec} is intended as a decoder. * @param outputSurface The output {@link Surface} if the {@link MediaCodec} outputs to a surface. */ public DefaultCodec( Format configurationFormat, MediaFormat configurationMediaFormat, - @Nullable String mediaCodecName, + String mediaCodecName, boolean isDecoder, @Nullable Surface outputSurface) throws TransformationException { @@ -87,17 +85,11 @@ public final class DefaultCodec implements Codec { inputBufferIndex = C.INDEX_UNSET; outputBufferIndex = C.INDEX_UNSET; - String sampleMimeType = checkNotNull(configurationFormat.sampleMimeType); - boolean isVideo = MimeTypes.isVideo(sampleMimeType); + boolean isVideo = MimeTypes.isVideo(checkNotNull(configurationFormat.sampleMimeType)); @Nullable MediaCodec mediaCodec = null; @Nullable Surface inputSurface = null; try { - mediaCodec = - mediaCodecName != null - ? MediaCodec.createByCodecName(mediaCodecName) - : isDecoder - ? MediaCodec.createDecoderByType(sampleMimeType) - : MediaCodec.createEncoderByType(sampleMimeType); + mediaCodec = MediaCodec.createByCodecName(mediaCodecName); configureCodec(mediaCodec, configurationMediaFormat, isDecoder, outputSurface); if (isVideo && !isDecoder) { inputSurface = mediaCodec.createInputSurface(); @@ -108,7 +100,6 @@ public final class DefaultCodec implements Codec { inputSurface.release(); } if (mediaCodec != null) { - mediaCodecName = mediaCodec.getName(); mediaCodec.release(); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java index 5e951a6847..7b525416a3 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java @@ -21,11 +21,15 @@ import static androidx.media3.common.util.Util.SDK_INT; import android.media.MediaFormat; import android.view.Surface; +import androidx.annotation.Nullable; import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; import androidx.media3.common.util.MediaFormatUtil; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** A default implementation of {@link Codec.DecoderFactory}. */ /* package */ final class DefaultDecoderFactory implements Codec.DecoderFactory { + @Override public Codec createForAudioDecoding(Format format) throws TransformationException { MediaFormat mediaFormat = @@ -35,16 +39,18 @@ import androidx.media3.common.util.MediaFormatUtil; mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + @Nullable + String mediaCodecName = EncoderUtil.findCodecForFormat(mediaFormat, /* isDecoder= */ true); + if (mediaCodecName == null) { + throw createTransformationException(format); + } return new DefaultCodec( - format, - mediaFormat, - /* mediaCodecName= */ null, - /* isDecoder= */ true, - /* outputSurface= */ null); + format, mediaFormat, mediaCodecName, /* isDecoder= */ true, /* outputSurface= */ null); } @Override - public Codec createForVideoDecoding(Format format, Surface outputSurface) + public Codec createForVideoDecoding( + Format format, Surface outputSurface, boolean enableRequestSdrToneMapping) throws TransformationException { MediaFormat mediaFormat = MediaFormat.createVideoFormat( @@ -58,8 +64,28 @@ import androidx.media3.common.util.MediaFormatUtil; // cycle. This key ensures no frame dropping when the decoder's output surface is full. mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); } + if (SDK_INT >= 31 && enableRequestSdrToneMapping) { + mediaFormat.setInteger( + MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO); + } + @Nullable + String mediaCodecName = EncoderUtil.findCodecForFormat(mediaFormat, /* isDecoder= */ true); + if (mediaCodecName == null) { + throw createTransformationException(format); + } return new DefaultCodec( - format, mediaFormat, /* mediaCodecName= */ null, /* isDecoder= */ true, outputSurface); + format, mediaFormat, mediaCodecName, /* isDecoder= */ true, outputSurface); + } + + @RequiresNonNull("#1.sampleMimeType") + private static TransformationException createTransformationException(Format format) { + return TransformationException.createForCodec( + new IllegalArgumentException("The requested decoding format is not supported."), + MimeTypes.isVideo(format.sampleMimeType), + /* isDecoder= */ true, + format, + /* mediaCodecName= */ null, + TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED); } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java index 2c6fe623a2..ef3c612a30 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -26,45 +26,70 @@ import static java.lang.Math.abs; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.util.Pair; +import android.util.Size; import androidx.annotation.Nullable; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; -import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** A default implementation of {@link Codec.EncoderFactory}. */ +// TODO(b/224949986) Split audio and video encoder factory. @UnstableApi public final class DefaultEncoderFactory implements Codec.EncoderFactory { - private static final int DEFAULT_COLOR_FORMAT = - MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; - private static final int DEFAULT_FRAME_RATE = 60; - private static final int DEFAULT_I_FRAME_INTERVAL_SECS = 1; + private static final int DEFAULT_FRAME_RATE = 30; + private static final String TAG = "DefaultEncoderFactory"; - @Nullable private final EncoderSelector videoEncoderSelector; + private final EncoderSelector videoEncoderSelector; + private final VideoEncoderSettings requestedVideoEncoderSettings; private final boolean enableFallback; /** - * Creates a new instance using the {@link EncoderSelector#DEFAULT default encoder selector}, and - * format fallback enabled. - * - *

    With format fallback enabled, and when the requested {@link Format} is not supported, {@code - * DefaultEncoderFactory} finds a format that is supported by the device and configures the {@link - * Codec} with it. The fallback process may change the requested {@link Format#sampleMimeType MIME - * type}, resolution, {@link Format#bitrate bitrate}, {@link Format#codecs profile/level}, etc. + * Creates a new instance using the {@link EncoderSelector#DEFAULT default encoder selector}, a + * default {@link VideoEncoderSettings}, and with format fallback enabled. */ public DefaultEncoderFactory() { this(EncoderSelector.DEFAULT, /* enableFallback= */ true); } - /** Creates a new instance. */ + /** Creates a new instance using a default {@link VideoEncoderSettings}. */ + public DefaultEncoderFactory(EncoderSelector videoEncoderSelector, boolean enableFallback) { + this(videoEncoderSelector, VideoEncoderSettings.DEFAULT, enableFallback); + } + + /** + * Creates a new instance. + * + *

    With format fallback enabled, when the requested {@link Format} is not supported, {@code + * DefaultEncoderFactory} finds a format that is supported by the device and configures the {@link + * Codec} with it. The fallback process may change the requested {@link Format#sampleMimeType MIME + * type}, resolution, {@link Format#bitrate bitrate}, {@link Format#codecs profile/level} etc. + * + *

    Values in {@code requestedVideoEncoderSettings} could be adjusted to improve encoding + * quality and/or reduce failures. Specifically, {@link VideoEncoderSettings#profile} and {@link + * VideoEncoderSettings#level} are ignored for {@link MimeTypes#VIDEO_H264}. Consider implementing + * {@link Codec.EncoderFactory} if such adjustments are unwanted. + * + *

    {@code requestedVideoEncoderSettings} should be handled with care because there is no + * fallback support for it. For example, using incompatible {@link VideoEncoderSettings#profile} + * and {@link VideoEncoderSettings#level} can cause codec configuration failure. Setting an + * unsupported {@link VideoEncoderSettings#bitrateMode} may cause encoder instantiation failure. + * + * @param videoEncoderSelector The {@link EncoderSelector}. + * @param requestedVideoEncoderSettings The {@link VideoEncoderSettings}. + * @param enableFallback Whether to enable fallback. + */ public DefaultEncoderFactory( - @Nullable EncoderSelector videoEncoderSelector, boolean enableFallback) { + EncoderSelector videoEncoderSelector, + VideoEncoderSettings requestedVideoEncoderSettings, + boolean enableFallback) { this.videoEncoderSelector = videoEncoderSelector; + this.requestedVideoEncoderSettings = requestedVideoEncoderSettings; this.enableFallback = enableFallback; } @@ -73,19 +98,14 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { throws TransformationException { // TODO(b/210591626) Add encoder selection for audio. checkArgument(!allowedMimeTypes.isEmpty()); + checkNotNull(format.sampleMimeType); if (!allowedMimeTypes.contains(format.sampleMimeType)) { if (enableFallback) { // TODO(b/210591626): Pick fallback MIME type using same strategy as for encoder // capabilities limitations. format = format.buildUpon().setSampleMimeType(allowedMimeTypes.get(0)).build(); } else { - throw TransformationException.createForCodec( - new IllegalArgumentException("The requested output format is not supported."), - /* isVideo= */ false, - /* isDecoder= */ false, - format, - /* mediaCodecName= */ null, - TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED); + throw createTransformationException(format); } } MediaFormat mediaFormat = @@ -93,17 +113,21 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate); + @Nullable + String mediaCodecName = EncoderUtil.findCodecForFormat(mediaFormat, /* isDecoder= */ false); + if (mediaCodecName == null) { + throw createTransformationException(format); + } return new DefaultCodec( - format, - mediaFormat, - /* mediaCodecName= */ null, - /* isDecoder= */ false, - /* outputSurface= */ null); + format, mediaFormat, mediaCodecName, /* isDecoder= */ false, /* outputSurface= */ null); } @Override public Codec createForVideoEncoding(Format format, List allowedMimeTypes) throws TransformationException { + if (format.frameRate == Format.NO_VALUE) { + format = format.buildUpon().setFrameRate(DEFAULT_FRAME_RATE).build(); + } checkArgument(format.width != Format.NO_VALUE); checkArgument(format.height != Format.NO_VALUE); // According to interface Javadoc, format.rotationDegrees should be 0. The video should always @@ -115,84 +139,62 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { checkStateNotNull(videoEncoderSelector); @Nullable - Pair encoderAndClosestFormatSupport = + VideoEncoderQueryResult encoderAndClosestFormatSupport = findEncoderWithClosestFormatSupport( - format, videoEncoderSelector, allowedMimeTypes, enableFallback); + format, + requestedVideoEncoderSettings, + videoEncoderSelector, + allowedMimeTypes, + enableFallback); + if (encoderAndClosestFormatSupport == null) { - throw TransformationException.createForCodec( - new IllegalArgumentException("The requested output format is not supported."), - /* isVideo= */ true, - /* isDecoder= */ false, - format, - /* mediaCodecName= */ null, - TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED); + throw createTransformationException(format); } - MediaCodecInfo encoderInfo = encoderAndClosestFormatSupport.first; - format = encoderAndClosestFormatSupport.second; + MediaCodecInfo encoderInfo = encoderAndClosestFormatSupport.encoder; + format = encoderAndClosestFormatSupport.supportedFormat; + VideoEncoderSettings supportedVideoEncoderSettings = + encoderAndClosestFormatSupport.supportedEncoderSettings; + String mimeType = checkNotNull(format.sampleMimeType); MediaFormat mediaFormat = MediaFormat.createVideoFormat(mimeType, format.width, format.height); mediaFormat.setFloat(MediaFormat.KEY_FRAME_RATE, format.frameRate); - mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.averageBitrate); + mediaFormat.setInteger( + MediaFormat.KEY_BIT_RATE, + supportedVideoEncoderSettings.bitrate != VideoEncoderSettings.NO_VALUE + ? supportedVideoEncoderSettings.bitrate + : getSuggestedBitrate(format.width, format.height, format.frameRate)); - @Nullable - Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); - if (codecProfileAndLevel != null) { - // The codecProfileAndLevel is supported by the encoder. - mediaFormat.setInteger(MediaFormat.KEY_PROFILE, codecProfileAndLevel.first); - if (SDK_INT >= 23) { - mediaFormat.setInteger(MediaFormat.KEY_LEVEL, codecProfileAndLevel.second); - } + mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, supportedVideoEncoderSettings.bitrateMode); + + if (supportedVideoEncoderSettings.profile != VideoEncoderSettings.NO_VALUE + && supportedVideoEncoderSettings.level != VideoEncoderSettings.NO_VALUE + && SDK_INT >= 23) { + // Set profile and level at the same time to maximize compatibility, or the encoder will pick + // the values. + mediaFormat.setInteger(MediaFormat.KEY_PROFILE, supportedVideoEncoderSettings.profile); + mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedVideoEncoderSettings.level); } - // TODO(b/210593256): Remove overriding profile/level (before API 29) after switching to in-app - // muxing. if (mimeType.equals(MimeTypes.VIDEO_H264)) { - // Applying suggested profile/level settings from - // https://developer.android.com/guide/topics/media/sharing-video#b-frames_and_encoding_profiles - if (Util.SDK_INT >= 29) { - int supportedEncodingLevel = - EncoderUtil.findHighestSupportedEncodingLevel( - encoderInfo, mimeType, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh); - if (supportedEncodingLevel != EncoderUtil.LEVEL_UNSET) { - // Use the highest supported profile and use B-frames. - mediaFormat.setInteger( - MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh); - mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedEncodingLevel); - mediaFormat.setInteger(MediaFormat.KEY_MAX_B_FRAMES, 1); - } - } else if (Util.SDK_INT >= 26) { - int supportedEncodingLevel = - EncoderUtil.findHighestSupportedEncodingLevel( - encoderInfo, mimeType, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh); - if (supportedEncodingLevel != EncoderUtil.LEVEL_UNSET) { - // Use the highest-supported profile, but disable the generation of B-frames using - // MediaFormat.KEY_LATENCY. This accommodates some limitations in the MediaMuxer in these - // system versions. - mediaFormat.setInteger( - MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh); - mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedEncodingLevel); - // TODO(b/210593256): Set KEY_LATENCY to 2 to enable B-frame production after switching to - // in-app muxing. - mediaFormat.setInteger(MediaFormat.KEY_LATENCY, 1); - } - } else if (Util.SDK_INT >= 24) { - int supportedLevel = - EncoderUtil.findHighestSupportedEncodingLevel( - encoderInfo, mimeType, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline); - checkState(supportedLevel != EncoderUtil.LEVEL_UNSET); - // Use the baseline profile for safest results, as encoding in baseline is required per - // https://source.android.com/compatibility/5.0/android-5.0-cdd#5_2_video_encoding - mediaFormat.setInteger( - MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline); - mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedLevel); - } - // For API levels below 24, setting profile and level can lead to failures in MediaCodec - // configuration. The encoder selects the profile/level when we don't set them. + adjustMediaFormatForH264EncoderSettings(mediaFormat, encoderInfo); } - mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, DEFAULT_COLOR_FORMAT); - mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL_SECS); + mediaFormat.setInteger( + MediaFormat.KEY_COLOR_FORMAT, supportedVideoEncoderSettings.colorProfile); + mediaFormat.setFloat( + MediaFormat.KEY_I_FRAME_INTERVAL, supportedVideoEncoderSettings.iFrameIntervalSeconds); + + if (Util.SDK_INT >= 23) { + // Setting operating rate and priority is supported from API 23. + if (supportedVideoEncoderSettings.operatingRate != VideoEncoderSettings.NO_VALUE) { + mediaFormat.setInteger( + MediaFormat.KEY_OPERATING_RATE, supportedVideoEncoderSettings.operatingRate); + } + if (supportedVideoEncoderSettings.priority != VideoEncoderSettings.NO_VALUE) { + mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, supportedVideoEncoderSettings.priority); + } + } return new DefaultCodec( format, @@ -202,15 +204,22 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { /* outputSurface= */ null); } + @Override + public boolean videoNeedsEncoding() { + return !requestedVideoEncoderSettings.equals(VideoEncoderSettings.DEFAULT); + } + /** - * Finds a {@link MediaCodecInfo encoder} that supports the requested format most closely. Returns - * the {@link MediaCodecInfo encoder} and the supported {@link Format} in a {@link Pair}, or - * {@code null} if none is found. + * Finds an {@linkplain MediaCodecInfo encoder} that supports the requested format most closely. + * + *

    Returns the {@linkplain MediaCodecInfo encoder} and the supported {@link Format} in a {@link + * Pair}, or {@code null} if none is found. */ @RequiresNonNull("#1.sampleMimeType") @Nullable - private static Pair findEncoderWithClosestFormatSupport( + private static VideoEncoderQueryResult findEncoderWithClosestFormatSupport( Format requestedFormat, + VideoEncoderSettings videoEncoderSettings, EncoderSelector encoderSelector, List allowedMimeTypes, boolean enableFallback) { @@ -226,96 +235,203 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { return null; } if (!enableFallback) { - return Pair.create(encodersForMimeType.get(0), requestedFormat); + return new VideoEncoderQueryResult( + encodersForMimeType.get(0), requestedFormat, videoEncoderSettings); } + ImmutableList filteredEncoders = - filterEncoders( - encodersForMimeType, - /* cost= */ (encoderInfo) -> { - @Nullable - Pair closestSupportedResolution = - EncoderUtil.getClosestSupportedResolution( - encoderInfo, mimeType, requestedFormat.width, requestedFormat.height); - if (closestSupportedResolution == null) { - // Drops encoder. - return Integer.MAX_VALUE; - } - return abs( - requestedFormat.width * requestedFormat.height - - closestSupportedResolution.first * closestSupportedResolution.second); - }); + filterEncodersByResolution( + encodersForMimeType, mimeType, requestedFormat.width, requestedFormat.height); if (filteredEncoders.isEmpty()) { return null; } // The supported resolution is the same for all remaining encoders. - Pair finalResolution = + Size finalResolution = checkNotNull( - EncoderUtil.getClosestSupportedResolution( + EncoderUtil.getSupportedResolution( filteredEncoders.get(0), mimeType, requestedFormat.width, requestedFormat.height)); int requestedBitrate = - requestedFormat.averageBitrate == Format.NO_VALUE - ? getSuggestedBitrate( - /* width= */ finalResolution.first, - /* height= */ finalResolution.second, - requestedFormat.frameRate == Format.NO_VALUE - ? DEFAULT_FRAME_RATE - : requestedFormat.frameRate) - : requestedFormat.averageBitrate; + videoEncoderSettings.bitrate != VideoEncoderSettings.NO_VALUE + ? videoEncoderSettings.bitrate + : getSuggestedBitrate( + finalResolution.getWidth(), finalResolution.getHeight(), requestedFormat.frameRate); + filteredEncoders = filterEncodersByBitrate(filteredEncoders, mimeType, requestedBitrate); + if (filteredEncoders.isEmpty()) { + return null; + } + filteredEncoders = - filterEncoders( - filteredEncoders, - /* cost= */ (encoderInfo) -> { - int achievableBitrate = - EncoderUtil.getClosestSupportedBitrate(encoderInfo, mimeType, requestedBitrate); - return abs(achievableBitrate - requestedBitrate); - }); + filterEncodersByBitrateMode(filteredEncoders, mimeType, videoEncoderSettings.bitrateMode); if (filteredEncoders.isEmpty()) { return null; } MediaCodecInfo pickedEncoder = filteredEncoders.get(0); - @Nullable - Pair profileLevel = MediaCodecUtil.getCodecProfileAndLevel(requestedFormat); - @Nullable String codecs = null; - if (profileLevel != null - && requestedFormat.sampleMimeType.equals(mimeType) - && profileLevel.second - <= EncoderUtil.findHighestSupportedEncodingLevel( - pickedEncoder, mimeType, /* profile= */ profileLevel.first)) { - codecs = requestedFormat.codecs; + int closestSupportedBitrate = + EncoderUtil.getClosestSupportedBitrate(pickedEncoder, mimeType, requestedBitrate); + VideoEncoderSettings.Builder supportedEncodingSettingBuilder = + videoEncoderSettings.buildUpon().setBitrate(closestSupportedBitrate); + + if (videoEncoderSettings.profile == VideoEncoderSettings.NO_VALUE + || videoEncoderSettings.level == VideoEncoderSettings.NO_VALUE + || videoEncoderSettings.level + > EncoderUtil.findHighestSupportedEncodingLevel( + pickedEncoder, mimeType, videoEncoderSettings.profile)) { + supportedEncodingSettingBuilder.setEncodingProfileLevel( + VideoEncoderSettings.NO_VALUE, VideoEncoderSettings.NO_VALUE); } - Format encoderSupportedFormat = + Format supportedEncoderFormat = requestedFormat .buildUpon() .setSampleMimeType(mimeType) - .setCodecs(codecs) - .setWidth(finalResolution.first) - .setHeight(finalResolution.second) - .setFrameRate( - requestedFormat.frameRate != Format.NO_VALUE - ? requestedFormat.frameRate - : DEFAULT_FRAME_RATE) - .setAverageBitrate( - EncoderUtil.getClosestSupportedBitrate(pickedEncoder, mimeType, requestedBitrate)) + .setWidth(finalResolution.getWidth()) + .setHeight(finalResolution.getHeight()) + .setAverageBitrate(closestSupportedBitrate) .build(); - return Pair.create(pickedEncoder, encoderSupportedFormat); + return new VideoEncoderQueryResult( + pickedEncoder, supportedEncoderFormat, supportedEncodingSettingBuilder.build()); + } + + /** Returns a list of encoders that support the requested resolution most closely. */ + private static ImmutableList filterEncodersByResolution( + List encoders, String mimeType, int requestedWidth, int requestedHeight) { + return filterEncoders( + encoders, + /* cost= */ (encoderInfo) -> { + @Nullable + Size closestSupportedResolution = + EncoderUtil.getSupportedResolution( + encoderInfo, mimeType, requestedWidth, requestedHeight); + if (closestSupportedResolution == null) { + // Drops encoder. + return Integer.MAX_VALUE; + } + return abs( + requestedWidth * requestedHeight + - closestSupportedResolution.getWidth() * closestSupportedResolution.getHeight()); + }, + /* filterName= */ "resolution"); + } + + /** Returns a list of encoders that support the requested bitrate most closely. */ + private static ImmutableList filterEncodersByBitrate( + List encoders, String mimeType, int requestedBitrate) { + return filterEncoders( + encoders, + /* cost= */ (encoderInfo) -> { + int achievableBitrate = + EncoderUtil.getClosestSupportedBitrate(encoderInfo, mimeType, requestedBitrate); + return abs(achievableBitrate - requestedBitrate); + }, + /* filterName= */ "bitrate"); + } + + /** Returns a list of encoders that support the requested bitrate mode. */ + private static ImmutableList filterEncodersByBitrateMode( + List encoders, String mimeType, int requestedBitrateMode) { + return filterEncoders( + encoders, + /* cost= */ (encoderInfo) -> + EncoderUtil.isBitrateModeSupported(encoderInfo, mimeType, requestedBitrateMode) + ? 0 + : Integer.MAX_VALUE, // Drops encoder. + /* filterName= */ "bitrate mode"); + } + + private static final class VideoEncoderQueryResult { + public final MediaCodecInfo encoder; + public final Format supportedFormat; + public final VideoEncoderSettings supportedEncoderSettings; + + public VideoEncoderQueryResult( + MediaCodecInfo encoder, + Format supportedFormat, + VideoEncoderSettings supportedEncoderSettings) { + this.encoder = encoder; + this.supportedFormat = supportedFormat; + this.supportedEncoderSettings = supportedEncoderSettings; + } + } + + /** + * Applying suggested profile/level settings from + * https://developer.android.com/guide/topics/media/sharing-video#b-frames_and_encoding_profiles + * + *

    The adjustment is applied in-place to {@code mediaFormat}. + */ + private static void adjustMediaFormatForH264EncoderSettings( + MediaFormat mediaFormat, MediaCodecInfo encoderInfo) { + // TODO(b/210593256): Remove overriding profile/level (before API 29) after switching to in-app + // muxing. + String mimeType = MimeTypes.VIDEO_H264; + if (Util.SDK_INT >= 29) { + int expectedEncodingProfile = MediaCodecInfo.CodecProfileLevel.AVCProfileHigh; + int supportedEncodingLevel = + EncoderUtil.findHighestSupportedEncodingLevel( + encoderInfo, mimeType, expectedEncodingProfile); + if (supportedEncodingLevel != EncoderUtil.LEVEL_UNSET) { + // Use the highest supported profile and use B-frames. + mediaFormat.setInteger(MediaFormat.KEY_PROFILE, expectedEncodingProfile); + mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedEncodingLevel); + mediaFormat.setInteger(MediaFormat.KEY_MAX_B_FRAMES, 1); + } + } else if (Util.SDK_INT >= 26) { + int expectedEncodingProfile = MediaCodecInfo.CodecProfileLevel.AVCProfileHigh; + int supportedEncodingLevel = + EncoderUtil.findHighestSupportedEncodingLevel( + encoderInfo, mimeType, expectedEncodingProfile); + if (supportedEncodingLevel != EncoderUtil.LEVEL_UNSET) { + // Use the highest-supported profile, but disable the generation of B-frames using + // MediaFormat.KEY_LATENCY. This accommodates some limitations in the MediaMuxer in these + // system versions. + mediaFormat.setInteger(MediaFormat.KEY_PROFILE, expectedEncodingProfile); + mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedEncodingLevel); + // TODO(b/210593256): Set KEY_LATENCY to 2 to enable B-frame production after switching to + // in-app muxing. + mediaFormat.setInteger(MediaFormat.KEY_LATENCY, 1); + } + } else if (Util.SDK_INT >= 24) { + int expectedEncodingProfile = MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline; + int supportedLevel = + EncoderUtil.findHighestSupportedEncodingLevel( + encoderInfo, mimeType, expectedEncodingProfile); + checkState(supportedLevel != EncoderUtil.LEVEL_UNSET); + // Use the baseline profile for safest results, as encoding in baseline is required per + // https://source.android.com/compatibility/5.0/android-5.0-cdd#5_2_video_encoding + mediaFormat.setInteger(MediaFormat.KEY_PROFILE, expectedEncodingProfile); + mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedLevel); + } else { + // For API levels below 24, setting profile and level can lead to failures in MediaCodec + // configuration. The encoder selects the profile/level when we don't set them. + mediaFormat.setString(MediaFormat.KEY_PROFILE, null); + mediaFormat.setString(MediaFormat.KEY_LEVEL, null); + } } private interface EncoderFallbackCost { /** * Returns a cost that represents the gap between the requested encoding parameter(s) and the - * {@link MediaCodecInfo encoder}'s support for them. + * {@linkplain MediaCodecInfo encoder}'s support for them. * - *

    The method must return {@link Integer#MAX_VALUE} when the {@link MediaCodecInfo encoder} - * does not support the encoding parameters. + *

    The method must return {@link Integer#MAX_VALUE} when the {@linkplain MediaCodecInfo + * encoder} does not support the encoding parameters. */ int getParameterSupportGap(MediaCodecInfo encoderInfo); } + /** + * Filters a list of {@linkplain MediaCodecInfo encoders} by a {@linkplain EncoderFallbackCost + * cost function}. + * + * @param encoders A list of {@linkplain MediaCodecInfo encoders}. + * @param cost A {@linkplain EncoderFallbackCost cost function}. + * @return A list of {@linkplain MediaCodecInfo encoders} with the lowest costs, empty if the + * costs of all encoders are {@link Integer#MAX_VALUE}. + */ private static ImmutableList filterEncoders( - List encoders, EncoderFallbackCost cost) { + List encoders, EncoderFallbackCost cost, String filterName) { List filteredEncoders = new ArrayList<>(encoders.size()); int minGap = Integer.MAX_VALUE; @@ -334,9 +450,24 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { filteredEncoders.add(encoderInfo); } } + + List removedEncoders = new ArrayList<>(encoders); + removedEncoders.removeAll(filteredEncoders); + StringBuilder stringBuilder = + new StringBuilder("Encoders removed for ").append(filterName).append(":\n"); + for (int i = 0; i < removedEncoders.size(); i++) { + MediaCodecInfo encoderInfo = removedEncoders.get(i); + stringBuilder.append(Util.formatInvariant(" %s\n", encoderInfo.getName())); + } + Log.d(TAG, stringBuilder.toString()); + return ImmutableList.copyOf(filteredEncoders); } + /** + * Finds a {@linkplain MimeTypes MIME type} that is supported by the encoder and in the {@code + * allowedMimeTypes}. + */ @Nullable private static String findFallbackMimeType( EncoderSelector encoderSelector, String requestedMimeType, List allowedMimeTypes) { @@ -369,4 +500,15 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { // 1080p30 -> 6.2Mbps, 720p30 -> 2.7Mbps. return (int) (width * height * frameRate * 0.1); } + + @RequiresNonNull("#1.sampleMimeType") + private static TransformationException createTransformationException(Format format) { + return TransformationException.createForCodec( + new IllegalArgumentException("The requested encoding format is not supported."), + MimeTypes.isVideo(format.sampleMimeType), + /* isDecoder= */ false, + format, + /* mediaCodecName= */ null, + TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED); + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java index 5d15ac2ed2..c68719a1a4 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java @@ -36,8 +36,8 @@ public interface EncoderSelector { * Returns a list of encoders that can encode media in the specified {@code mimeType}, in priority * order. * - * @param mimeType The {@link MimeTypes MIME type} for which an encoder is required. - * @return An unmodifiable list of {@link MediaCodecInfo encoders} that supports the {@code + * @param mimeType The {@linkplain MimeTypes MIME type} for which an encoder is required. + * @return An unmodifiable list of {@linkplain MediaCodecInfo encoders} that support the {@code * mimeType}. The list may be empty. */ List selectEncoderInfos(String mimeType); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java index 970644433a..e4ef7cb71b 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java @@ -22,12 +22,14 @@ import static java.lang.Math.round; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; -import android.util.Pair; +import android.media.MediaFormat; +import android.util.Size; import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.MediaFormatUtil; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.base.Ascii; @@ -45,8 +47,8 @@ public final class EncoderUtil { private static final List encoders = new ArrayList<>(); /** - * Returns a list of {@link MediaCodecInfo encoders} that support the given {@code mimeType}, or - * an empty list if there is none. + * Returns a list of {@linkplain MediaCodecInfo encoders} that support the given {@code mimeType}, + * or an empty list if there is none. */ public static ImmutableList getSupportedEncoders(String mimeType) { maybePopulateEncoderInfos(); @@ -65,59 +67,76 @@ public final class EncoderUtil { } /** - * Finds the {@link MediaCodecInfo encoder}'s closest supported resolution from the given - * resolution. + * Finds an {@linkplain MediaCodecInfo encoder}'s supported resolution from a given resolution. * - *

    The input resolution is returned, if it is supported by the {@link MediaCodecInfo encoder}. + *

    The input resolution is returned, if it (after aligning to the encoder's requirement) is + * supported by the {@linkplain MediaCodecInfo encoder}. * - *

    The resolution will be clamped to the {@link MediaCodecInfo encoder}'s range of supported - * resolutions, and adjusted to the {@link MediaCodecInfo encoder}'s size alignment. The - * adjustment process takes into account the original aspect ratio. But the fixed resolution may - * not preserve the original aspect ratio, depending on the encoder's required size alignment. + *

    The resolution will be adjusted to be within the {@linkplain MediaCodecInfo encoder}'s range + * of supported resolutions, and will be aligned to the {@linkplain MediaCodecInfo encoder}'s + * alignment requirement. The adjustment process takes into account the original aspect ratio. But + * the fixed resolution may not preserve the original aspect ratio, depending on the encoder's + * required size alignment. * * @param encoderInfo The {@link MediaCodecInfo} of the encoder. * @param mimeType The output MIME type. * @param width The original width. * @param height The original height. - * @return A {@link Pair} of width and height, or {@code null} if unable to find a fix. + * @return A {@linkplain Size supported resolution}, or {@code null} if unable to find a fallback. */ @Nullable - public static Pair getClosestSupportedResolution( + public static Size getSupportedResolution( MediaCodecInfo encoderInfo, String mimeType, int width, int height) { MediaCodecInfo.VideoCapabilities videoEncoderCapabilities = encoderInfo.getCapabilitiesForType(mimeType).getVideoCapabilities(); + int widthAlignment = videoEncoderCapabilities.getWidthAlignment(); + int heightAlignment = videoEncoderCapabilities.getHeightAlignment(); + // Fix size alignment. + width = alignResolution(width, widthAlignment); + height = alignResolution(height, heightAlignment); if (videoEncoderCapabilities.isSizeSupported(width, height)) { - return Pair.create(width, height); + return new Size(width, height); + } + + // Try three-fourths (e.g. 1440 -> 1080). + int newWidth = alignResolution(width * 3 / 4, widthAlignment); + int newHeight = alignResolution(height * 3 / 4, heightAlignment); + if (videoEncoderCapabilities.isSizeSupported(newWidth, newHeight)) { + return new Size(newWidth, newHeight); + } + + // Try two-thirds (e.g. 4k -> 1440). + newWidth = alignResolution(width * 2 / 3, widthAlignment); + newHeight = alignResolution(height * 2 / 3, heightAlignment); + if (videoEncoderCapabilities.isSizeSupported(newWidth, newHeight)) { + return new Size(newWidth, newHeight); + } + + // Try half (e.g. 4k -> 1080). + newWidth = alignResolution(width / 2, widthAlignment); + newHeight = alignResolution(height / 2, heightAlignment); + if (videoEncoderCapabilities.isSizeSupported(newWidth, newHeight)) { + return new Size(newWidth, newHeight); } // Fix frame being too wide or too tall. - int adjustedHeight = videoEncoderCapabilities.getSupportedHeights().clamp(height); + width = videoEncoderCapabilities.getSupportedWidths().clamp(width); + int adjustedHeight = videoEncoderCapabilities.getSupportedHeightsFor(width).clamp(height); if (adjustedHeight != height) { - width = (int) round((double) width * adjustedHeight / height); - height = adjustedHeight; + width = + alignResolution((int) round((double) width * adjustedHeight / height), widthAlignment); + height = alignResolution(adjustedHeight, heightAlignment); } - int adjustedWidth = videoEncoderCapabilities.getSupportedWidths().clamp(width); - if (adjustedWidth != width) { - height = (int) round((double) height * adjustedWidth / width); - width = adjustedWidth; - } - - // Fix pixel alignment. - width = alignResolution(width, videoEncoderCapabilities.getWidthAlignment()); - height = alignResolution(height, videoEncoderCapabilities.getHeightAlignment()); - - return videoEncoderCapabilities.isSizeSupported(width, height) - ? Pair.create(width, height) - : null; + return videoEncoderCapabilities.isSizeSupported(width, height) ? new Size(width, height) : null; } /** * Finds the highest supported encoding level given a profile. * * @param encoderInfo The {@link MediaCodecInfo encoderInfo}. - * @param mimeType The {@link MimeTypes MIME type}. + * @param mimeType The {@linkplain MimeTypes MIME type}. * @param profile The encoding profile. * @return The highest supported encoding level, as documented in {@link * MediaCodecInfo.CodecProfileLevel}, or {@link #LEVEL_UNSET} if the profile is not supported. @@ -138,7 +157,34 @@ public final class EncoderUtil { } /** - * Finds the {@link MediaCodecInfo encoder}'s closest supported bitrate from the given bitrate. + * Finds a {@link MediaCodec} that supports the {@link MediaFormat}, or {@code null} if none is + * found. + */ + @Nullable + public static String findCodecForFormat(MediaFormat format, boolean isDecoder) { + MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + // Format must not include KEY_FRAME_RATE on API21. + // https://developer.android.com/reference/android/media/MediaCodecList#findDecoderForFormat(android.media.MediaFormat) + @Nullable String frameRate = null; + if (Util.SDK_INT == 21 && format.containsKey(MediaFormat.KEY_FRAME_RATE)) { + frameRate = format.getString(MediaFormat.KEY_FRAME_RATE); + format.setString(MediaFormat.KEY_FRAME_RATE, null); + } + + String mediaCodecName = + isDecoder + ? mediaCodecList.findDecoderForFormat(format) + : mediaCodecList.findEncoderForFormat(format); + + if (Util.SDK_INT == 21) { + MediaFormatUtil.maybeSetString(format, MediaFormat.KEY_FRAME_RATE, frameRate); + } + return mediaCodecName; + } + + /** + * Finds the {@linkplain MediaCodecInfo encoder}'s closest supported bitrate from the given + * bitrate. */ public static int getClosestSupportedBitrate( MediaCodecInfo encoderInfo, String mimeType, int bitrate) { @@ -149,7 +195,16 @@ public final class EncoderUtil { .clamp(bitrate); } - /** Checks if a {@link MediaCodecInfo codec} is hardware-accelerated. */ + /** Returns whether the bitrate mode is supported by the encoder. */ + public static boolean isBitrateModeSupported( + MediaCodecInfo encoderInfo, String mimeType, int bitrateMode) { + return encoderInfo + .getCapabilitiesForType(mimeType) + .getEncoderCapabilities() + .isBitrateModeSupported(bitrateMode); + } + + /** Checks if a {@linkplain MediaCodecInfo codec} is hardware-accelerated. */ public static boolean isHardwareAccelerated(MediaCodecInfo encoderInfo, String mimeType) { // TODO(b/214964116): Merge into MediaCodecUtil. if (Util.SDK_INT >= 29) { @@ -192,7 +247,15 @@ public final class EncoderUtil { * aligned to 48. */ private static int alignResolution(int size, int alignment) { - return alignment * Math.round((float) size / alignment); + // Aligning to resolutions that are multiples of 10, like from 1081 to 1080, assuming alignment + // is 2 in most encoders. + boolean shouldRoundDown = false; + if (size % 10 == 1) { + shouldRoundDown = true; + } + return shouldRoundDown + ? (int) (alignment * Math.floor((float) size / alignment)) + : alignment * Math.round((float) size / alignment); } private static synchronized void maybePopulateEncoderInfos() { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java new file mode 100644 index 0000000000..eaeeedad05 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java @@ -0,0 +1,120 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import android.content.Context; +import android.opengl.GLES20; +import android.util.Size; +import androidx.media3.common.util.GlProgram; +import androidx.media3.common.util.GlUtil; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** Copies frames from an external texture and applies color transformations for HDR if needed. */ +/* package */ class ExternalCopyFrameProcessor implements GlFrameProcessor { + + static { + GlUtil.glAssertionsEnabled = true; + } + + private static final String VERTEX_SHADER_TEX_TRANSFORM_PATH = + "shaders/vertex_shader_tex_transform_es2.glsl"; + private static final String VERTEX_SHADER_TEX_TRANSFORM_ES3_PATH = + "shaders/vertex_shader_tex_transform_es3.glsl"; + private static final String FRAGMENT_SHADER_COPY_EXTERNAL_PATH = + "shaders/fragment_shader_copy_external_es2.glsl"; + private static final String FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH = + "shaders/fragment_shader_copy_external_yuv_es3.glsl"; + // Color transform coefficients from + // https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/libstagefright/colorconversion/ColorConverter.cpp;l=668-670;drc=487adf977a50cac3929eba15fad0d0f461c7ff0f. + private static final float[] MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM = { + 1.168f, 1.168f, 1.168f, + 0.0f, -0.188f, 2.148f, + 1.683f, -0.652f, 0.0f, + }; + + private final Context context; + private final boolean enableExperimentalHdrEditing; + + private @MonotonicNonNull Size size; + private @MonotonicNonNull GlProgram glProgram; + + public ExternalCopyFrameProcessor(Context context, boolean enableExperimentalHdrEditing) { + this.context = context; + this.enableExperimentalHdrEditing = enableExperimentalHdrEditing; + } + + @Override + public void initialize(int inputTexId, int inputWidth, int inputHeight) throws IOException { + size = new Size(inputWidth, inputHeight); + // TODO(b/205002913): check the loaded program is consistent with the attributes and uniforms + // expected in the code. + String vertexShaderFilePath = + enableExperimentalHdrEditing + ? VERTEX_SHADER_TEX_TRANSFORM_ES3_PATH + : VERTEX_SHADER_TEX_TRANSFORM_PATH; + String fragmentShaderFilePath = + enableExperimentalHdrEditing + ? FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH + : FRAGMENT_SHADER_COPY_EXTERNAL_PATH; + glProgram = new GlProgram(context, vertexShaderFilePath, fragmentShaderFilePath); + glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0); + // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. + glProgram.setBufferAttribute( + "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + glProgram.setBufferAttribute( + "aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + if (enableExperimentalHdrEditing) { + // In HDR editing mode the decoder output is sampled in YUV. + glProgram.setFloatsUniform("uColorTransform", MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM); + } + } + + @Override + public Size getOutputSize() { + return checkStateNotNull(size); + } + + /** + * Sets the texture transform matrix for converting an external surface texture's coordinates to + * sampling locations. + * + * @param textureTransformMatrix The external surface texture's {@linkplain + * android.graphics.SurfaceTexture#getTransformMatrix(float[]) transform matrix}. + */ + public void setTextureTransformMatrix(float[] textureTransformMatrix) { + checkStateNotNull(glProgram); + glProgram.setFloatsUniform("uTexTransform", textureTransformMatrix); + } + + @Override + public void updateProgramAndDraw(long presentationTimeUs) { + checkStateNotNull(glProgram); + glProgram.use(); + glProgram.bindAttributesAndUniforms(); + // The four-vertex triangle strip forms a quad. + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); + } + + @Override + public void release() { + if (glProgram != null) { + glProgram.delete(); + } + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java index 05463fa906..4bcf1c30b6 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java @@ -40,7 +40,8 @@ import androidx.media3.common.util.Util; * Creates a new instance. * * @param mediaItem The {@link MediaItem} to transform. - * @param transformerListeners The {@link Transformer.Listener listeners} to forward events to. + * @param transformerListeners The {@linkplain Transformer.Listener listeners} to forward events + * to. * @param originalTransformationRequest The original {@link TransformationRequest}. */ public FallbackListener( @@ -56,7 +57,7 @@ import androidx.media3.common.util.Util; /** * Registers an output track. * - *

    All tracks must be registered before a transformation request is {@link + *

    All tracks must be registered before a transformation request is {@linkplain * #onTransformationRequestFinalized(TransformationRequest) finalized}. */ public void registerTrack() { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameEditor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameEditor.java deleted file mode 100644 index f3dc89c94d..0000000000 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameEditor.java +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.transformer; - -import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.common.util.Assertions.checkState; - -import android.content.Context; -import android.graphics.Matrix; -import android.graphics.SurfaceTexture; -import android.opengl.EGL14; -import android.opengl.EGLContext; -import android.opengl.EGLDisplay; -import android.opengl.EGLExt; -import android.opengl.EGLSurface; -import android.opengl.GLES20; -import android.view.Surface; -import android.view.SurfaceView; -import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.util.GlUtil; -import java.io.IOException; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * {@code FrameEditor} applies changes to individual video frames. - * - *

    Input becomes available on its {@link #getInputSurface() input surface} asynchronously so - * {@link #canProcessData()} needs to be checked before calling {@link #processData()}. Output is - * written to its {@link #create(Context, int, int, float, Matrix, Surface, boolean, - * Transformer.DebugViewProvider) output surface}. - */ -/* package */ final class FrameEditor { - - static { - GlUtil.glAssertionsEnabled = true; - } - - /** - * Returns a new {@code FrameEditor} for applying changes to individual frames. - * - * @param context A {@link Context}. - * @param outputWidth The output width in pixels. - * @param outputHeight The output height in pixels. - * @param pixelWidthHeightRatio The ratio of width over height, for each pixel. - * @param transformationMatrix The transformation matrix to apply to each frame. - * @param outputSurface The {@link Surface}. - * @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal. - * @param debugViewProvider Provider for optional debug views to show intermediate output. - * @return A configured {@code FrameEditor}. - * @throws TransformationException If the {@code pixelWidthHeightRatio} isn't 1, reading shader - * files fails, or an OpenGL error occurs while creating and configuring the OpenGL - * components. - */ - public static FrameEditor create( - Context context, - int outputWidth, - int outputHeight, - float pixelWidthHeightRatio, - Matrix transformationMatrix, - Surface outputSurface, - boolean enableExperimentalHdrEditing, - Transformer.DebugViewProvider debugViewProvider) - throws TransformationException { - if (pixelWidthHeightRatio != 1.0f) { - // TODO(b/211782176): Consider implementing support for non-square pixels. - throw TransformationException.createForFrameEditor( - new UnsupportedOperationException( - "Transformer's frame editor currently does not support frame edits on non-square" - + " pixels. The pixelWidthHeightRatio is: " - + pixelWidthHeightRatio), - TransformationException.ERROR_CODE_GL_INIT_FAILED); - } - - GlFrameProcessor frameProcessor = - new GlFrameProcessor(context, transformationMatrix, enableExperimentalHdrEditing); - @Nullable - SurfaceView debugSurfaceView = - debugViewProvider.getDebugPreviewSurfaceView(outputWidth, outputHeight); - - EGLDisplay eglDisplay; - EGLContext eglContext; - EGLSurface eglSurface; - int textureId; - @Nullable EGLSurface debugPreviewEglSurface = null; - try { - eglDisplay = GlUtil.createEglDisplay(); - - if (enableExperimentalHdrEditing) { - eglContext = GlUtil.createEglContextEs3Rgba1010102(eglDisplay); - // TODO(b/209404935): Don't assume BT.2020 PQ input/output. - eglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, outputSurface); - if (debugSurfaceView != null) { - debugPreviewEglSurface = - GlUtil.getEglSurfaceBt2020Pq(eglDisplay, checkNotNull(debugSurfaceView.getHolder())); - } - } else { - eglContext = GlUtil.createEglContext(eglDisplay); - eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface); - if (debugSurfaceView != null) { - debugPreviewEglSurface = - GlUtil.getEglSurface(eglDisplay, checkNotNull(debugSurfaceView.getHolder())); - } - } - - GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); - textureId = GlUtil.createExternalTexture(); - frameProcessor.initialize(); - } catch (IOException | GlUtil.GlException e) { - throw TransformationException.createForFrameEditor( - e, TransformationException.ERROR_CODE_GL_INIT_FAILED); - } - - int debugPreviewWidth; - int debugPreviewHeight; - if (debugSurfaceView != null) { - debugPreviewWidth = debugSurfaceView.getWidth(); - debugPreviewHeight = debugSurfaceView.getHeight(); - } else { - debugPreviewWidth = C.LENGTH_UNSET; - debugPreviewHeight = C.LENGTH_UNSET; - } - - return new FrameEditor( - eglDisplay, - eglContext, - eglSurface, - textureId, - frameProcessor, - outputWidth, - outputHeight, - debugPreviewEglSurface, - debugPreviewWidth, - debugPreviewHeight); - } - - private final GlFrameProcessor frameProcessor; - private final float[] textureTransformMatrix; - private final EGLDisplay eglDisplay; - private final EGLContext eglContext; - private final EGLSurface eglSurface; - private final int textureId; - private final AtomicInteger pendingInputFrameCount; - private final AtomicInteger availableInputFrameCount; - private final SurfaceTexture inputSurfaceTexture; - private final Surface inputSurface; - private final int outputWidth; - private final int outputHeight; - @Nullable private final EGLSurface debugPreviewEglSurface; - private final int debugPreviewWidth; - private final int debugPreviewHeight; - - private boolean inputStreamEnded; - - private FrameEditor( - EGLDisplay eglDisplay, - EGLContext eglContext, - EGLSurface eglSurface, - int textureId, - GlFrameProcessor frameProcessor, - int outputWidth, - int outputHeight, - @Nullable EGLSurface debugPreviewEglSurface, - int debugPreviewWidth, - int debugPreviewHeight) { - this.eglDisplay = eglDisplay; - this.eglContext = eglContext; - this.eglSurface = eglSurface; - this.textureId = textureId; - this.frameProcessor = frameProcessor; - this.outputWidth = outputWidth; - this.outputHeight = outputHeight; - this.debugPreviewEglSurface = debugPreviewEglSurface; - this.debugPreviewWidth = debugPreviewWidth; - this.debugPreviewHeight = debugPreviewHeight; - pendingInputFrameCount = new AtomicInteger(); - availableInputFrameCount = new AtomicInteger(); - textureTransformMatrix = new float[16]; - inputSurfaceTexture = new SurfaceTexture(textureId); - inputSurfaceTexture.setOnFrameAvailableListener( - surfaceTexture -> { - checkState(pendingInputFrameCount.getAndDecrement() > 0); - availableInputFrameCount.incrementAndGet(); - }); - inputSurface = new Surface(inputSurfaceTexture); - } - - /** Returns the input {@link Surface}. */ - public Surface getInputSurface() { - return inputSurface; - } - - /** - * Informs the frame editor that a frame will be queued to its input surface. - * - *

    Should be called before rendering a frame to the frame editor's input surface. - * - * @throws IllegalStateException If called after {@link #signalEndOfInputStream()}. - */ - public void registerInputFrame() { - checkState(!inputStreamEnded); - pendingInputFrameCount.incrementAndGet(); - } - - /** - * Returns whether there is available input data that can be processed by calling {@link - * #processData()}. - */ - public boolean canProcessData() { - return availableInputFrameCount.get() > 0; - } - - /** - * Processes an input frame. - * - * @throws TransformationException If an OpenGL error occurs while processing the data. - * @throws IllegalStateException If there is no input data to process. Use {@link - * #canProcessData()} to check whether input data is available. - */ - public void processData() throws TransformationException { - checkState(canProcessData()); - try { - inputSurfaceTexture.updateTexImage(); - inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); - GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); - frameProcessor.setTextureTransformMatrix(textureTransformMatrix); - frameProcessor.updateProgramAndDraw(textureId); - long presentationTimeNs = inputSurfaceTexture.getTimestamp(); - EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeNs); - EGL14.eglSwapBuffers(eglDisplay, eglSurface); - - if (debugPreviewEglSurface != null) { - GlUtil.focusEglSurface( - eglDisplay, eglContext, debugPreviewEglSurface, debugPreviewWidth, debugPreviewHeight); - GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0); - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); - // The four-vertex triangle strip forms a quad. - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); - EGL14.eglSwapBuffers(eglDisplay, debugPreviewEglSurface); - } - } catch (GlUtil.GlException e) { - throw TransformationException.createForFrameEditor( - e, TransformationException.ERROR_CODE_GL_PROCESSING_FAILED); - } - availableInputFrameCount.decrementAndGet(); - } - - /** Releases all resources. */ - public void release() { - frameProcessor.release(); - GlUtil.deleteTexture(textureId); - GlUtil.destroyEglContext(eglDisplay, eglContext); - inputSurfaceTexture.release(); - inputSurface.release(); - } - - /** Returns whether all data has been processed. */ - public boolean isEnded() { - return inputStreamEnded - && pendingInputFrameCount.get() == 0 - && availableInputFrameCount.get() == 0; - } - - /** Informs the {@code FrameEditor} that no further input data should be accepted. */ - public void signalEndOfInputStream() { - inputStreamEnded = true; - } -} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java new file mode 100644 index 0000000000..b5dfe5af2a --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -0,0 +1,472 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static com.google.common.collect.Iterables.getLast; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLExt; +import android.opengl.EGLSurface; +import android.opengl.GLES20; +import android.util.Size; +import android.view.Surface; +import android.view.SurfaceView; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.Util; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * {@code FrameProcessorChain} applies changes to individual video frames. + * + *

    Input becomes available on its {@linkplain #getInputSurface() input surface} asynchronously + * and is processed on a background thread as it becomes available. All input frames should be + * {@linkplain #registerInputFrame() registered} before they are rendered to the input surface. + * {@link #getPendingFrameCount()} can be used to check whether there are frames that have not been + * fully processed yet. Output is written to its {@linkplain #setOutputSurface(Surface, int, int, + * SurfaceView) output surface}. + */ +/* package */ final class FrameProcessorChain { + + static { + GlUtil.glAssertionsEnabled = true; + } + + /** + * Creates a new instance. + * + * @param context A {@link Context}. + * @param pixelWidthHeightRatio The ratio of width over height, for each pixel. + * @param inputWidth The input frame width, in pixels. + * @param inputHeight The input frame height, in pixels. + * @param frameProcessors The {@link GlFrameProcessor GlFrameProcessors} to apply to each frame. + * @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal. + * @return A new instance. + * @throws TransformationException If the {@code pixelWidthHeightRatio} isn't 1, reading shader + * files fails, or an OpenGL error occurs while creating and configuring the OpenGL + * components. + */ + public static FrameProcessorChain create( + Context context, + float pixelWidthHeightRatio, + int inputWidth, + int inputHeight, + List frameProcessors, + boolean enableExperimentalHdrEditing) + throws TransformationException { + if (pixelWidthHeightRatio != 1.0f) { + // TODO(b/211782176): Consider implementing support for non-square pixels. + throw TransformationException.createForFrameProcessorChain( + new UnsupportedOperationException( + "Transformer's FrameProcessorChain currently does not support frame edits on" + + " non-square pixels. The pixelWidthHeightRatio is: " + + pixelWidthHeightRatio), + TransformationException.ERROR_CODE_GL_INIT_FAILED); + } + + ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME); + ExternalCopyFrameProcessor externalCopyFrameProcessor = + new ExternalCopyFrameProcessor(context, enableExperimentalHdrEditing); + + try { + return singleThreadExecutorService + .submit( + () -> + createOpenGlObjectsAndFrameProcessorChain( + inputWidth, + inputHeight, + frameProcessors, + enableExperimentalHdrEditing, + singleThreadExecutorService, + externalCopyFrameProcessor)) + .get(); + } catch (ExecutionException e) { + throw TransformationException.createForFrameProcessorChain( + e, TransformationException.ERROR_CODE_GL_INIT_FAILED); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw TransformationException.createForFrameProcessorChain( + e, TransformationException.ERROR_CODE_GL_INIT_FAILED); + } + } + + /** + * Creates the OpenGL textures, framebuffers, initializes the {@link GlFrameProcessor + * GlFrameProcessors} and returns a new {@code FrameProcessorChain}. + * + *

    This method must by executed using the {@code singleThreadExecutorService}. + */ + private static FrameProcessorChain createOpenGlObjectsAndFrameProcessorChain( + int inputWidth, + int inputHeight, + List frameProcessors, + boolean enableExperimentalHdrEditing, + ExecutorService singleThreadExecutorService, + ExternalCopyFrameProcessor externalCopyFrameProcessor) + throws IOException { + checkState(Thread.currentThread().getName().equals(THREAD_NAME)); + + EGLDisplay eglDisplay = GlUtil.createEglDisplay(); + EGLContext eglContext = + enableExperimentalHdrEditing + ? GlUtil.createEglContextEs3Rgba1010102(eglDisplay) + : GlUtil.createEglContext(eglDisplay); + + if (GlUtil.isSurfacelessContextExtensionSupported()) { + GlUtil.focusEglSurface( + eglDisplay, eglContext, EGL14.EGL_NO_SURFACE, /* width= */ 1, /* height= */ 1); + } else if (enableExperimentalHdrEditing) { + // TODO(b/209404935): Don't assume BT.2020 PQ input/output. + GlUtil.focusPlaceholderEglSurfaceBt2020Pq(eglContext, eglDisplay); + } else { + GlUtil.focusPlaceholderEglSurface(eglContext, eglDisplay); + } + + int inputExternalTexId = GlUtil.createExternalTexture(); + externalCopyFrameProcessor.initialize(inputExternalTexId, inputWidth, inputHeight); + + int[] framebuffers = new int[frameProcessors.size()]; + Size inputSize = externalCopyFrameProcessor.getOutputSize(); + for (int i = 0; i < frameProcessors.size(); i++) { + int inputTexId = GlUtil.createTexture(inputSize.getWidth(), inputSize.getHeight()); + framebuffers[i] = GlUtil.createFboForTexture(inputTexId); + frameProcessors.get(i).initialize(inputTexId, inputSize.getWidth(), inputSize.getHeight()); + inputSize = frameProcessors.get(i).getOutputSize(); + } + return new FrameProcessorChain( + eglDisplay, + eglContext, + singleThreadExecutorService, + inputExternalTexId, + framebuffers, + new ImmutableList.Builder() + .add(externalCopyFrameProcessor) + .addAll(frameProcessors) + .build(), + enableExperimentalHdrEditing); + } + + private static final String THREAD_NAME = "Transformer:FrameProcessorChain"; + + private final boolean enableExperimentalHdrEditing; + private final EGLDisplay eglDisplay; + private final EGLContext eglContext; + /** Some OpenGL commands may block, so all OpenGL commands are run on a background thread. */ + private final ExecutorService singleThreadExecutorService; + /** Futures corresponding to the executor service's pending tasks. */ + private final ConcurrentLinkedQueue> futures; + /** Number of frames {@linkplain #registerInputFrame() registered} but not fully processed. */ + private final AtomicInteger pendingFrameCount; + + /** Wraps the {@link #inputSurfaceTexture}. */ + private final Surface inputSurface; + /** Associated with an OpenGL external texture. */ + private final SurfaceTexture inputSurfaceTexture; + /** Transformation matrix associated with the {@link #inputSurfaceTexture}. */ + private final float[] textureTransformMatrix; + + /** + * Contains an {@link ExternalCopyFrameProcessor} at the 0th index and optionally other {@link + * GlFrameProcessor GlFrameProcessors} at indices >= 1. + */ + private final ImmutableList frameProcessors; + /** + * Identifiers of a framebuffer object associated with the intermediate textures that receive + * output from the previous {@link GlFrameProcessor}, and provide input for the following {@link + * GlFrameProcessor}. + */ + private final int[] framebuffers; + + private Size outputSize; + /** + * Wraps the output {@link Surface} that is populated with the output of the final {@link + * GlFrameProcessor} for each frame. + */ + private @MonotonicNonNull EGLSurface eglSurface; + + private int debugPreviewWidth; + private int debugPreviewHeight; + /** + * Wraps a debug {@link SurfaceView} that is populated with the output of the final {@link + * GlFrameProcessor} for each frame. + */ + private @MonotonicNonNull EGLSurface debugPreviewEglSurface; + + private boolean inputStreamEnded; + /** Prevents further frame processing tasks from being scheduled after {@link #release()}. */ + private volatile boolean releaseRequested; + + private FrameProcessorChain( + EGLDisplay eglDisplay, + EGLContext eglContext, + ExecutorService singleThreadExecutorService, + int inputExternalTexId, + int[] framebuffers, + ImmutableList frameProcessors, + boolean enableExperimentalHdrEditing) { + checkState(!frameProcessors.isEmpty()); + + this.eglDisplay = eglDisplay; + this.eglContext = eglContext; + this.singleThreadExecutorService = singleThreadExecutorService; + this.framebuffers = framebuffers; + this.frameProcessors = frameProcessors; + this.enableExperimentalHdrEditing = enableExperimentalHdrEditing; + + futures = new ConcurrentLinkedQueue<>(); + pendingFrameCount = new AtomicInteger(); + inputSurfaceTexture = new SurfaceTexture(inputExternalTexId); + inputSurface = new Surface(inputSurfaceTexture); + textureTransformMatrix = new float[16]; + outputSize = getLast(frameProcessors).getOutputSize(); + debugPreviewWidth = C.LENGTH_UNSET; + debugPreviewHeight = C.LENGTH_UNSET; + } + + /** Returns the output {@link Size}. */ + public Size getOutputSize() { + return outputSize; + } + + /** + * Sets the output {@link Surface}. + * + *

    This method may override the output size of the final {@link GlFrameProcessor}. + * + * @param outputSurface The output {@link Surface}. + * @param outputWidth The output width, in pixels. + * @param outputHeight The output height, in pixels. + * @param debugSurfaceView Optional debug {@link SurfaceView} to show output. + */ + public void setOutputSurface( + Surface outputSurface, + int outputWidth, + int outputHeight, + @Nullable SurfaceView debugSurfaceView) { + // TODO(b/218488308): Don't override output size for encoder fallback. Instead allow the final + // GlFrameProcessor to be re-configured or append another GlFrameProcessor. + outputSize = new Size(outputWidth, outputHeight); + + if (debugSurfaceView != null) { + debugPreviewWidth = debugSurfaceView.getWidth(); + debugPreviewHeight = debugSurfaceView.getHeight(); + } + + futures.add( + singleThreadExecutorService.submit( + () -> createOpenGlSurfaces(outputSurface, debugSurfaceView))); + + inputSurfaceTexture.setOnFrameAvailableListener( + surfaceTexture -> { + if (releaseRequested) { + // Frames can still become available after a transformation is cancelled but they can be + // ignored. + return; + } + try { + futures.add(singleThreadExecutorService.submit(this::processFrame)); + } catch (RejectedExecutionException e) { + if (!releaseRequested) { + throw e; + } + } + }); + } + + /** Returns the input {@link Surface}. */ + public Surface getInputSurface() { + return inputSurface; + } + + /** + * Informs the {@code FrameProcessorChain} that a frame will be queued to its input surface. + * + *

    Should be called before rendering a frame to the frame processor chain's input surface. + * + * @throws IllegalStateException If called after {@link #signalEndOfInputStream()}. + */ + public void registerInputFrame() { + checkState(!inputStreamEnded); + pendingFrameCount.incrementAndGet(); + } + + /** + * Checks whether any exceptions occurred during asynchronous frame processing and rethrows the + * first exception encountered. + */ + public void getAndRethrowBackgroundExceptions() throws TransformationException { + @Nullable Future oldestGlProcessingFuture = futures.peek(); + while (oldestGlProcessingFuture != null && oldestGlProcessingFuture.isDone()) { + futures.poll(); + try { + oldestGlProcessingFuture.get(); + } catch (ExecutionException e) { + throw TransformationException.createForFrameProcessorChain( + e, TransformationException.ERROR_CODE_GL_PROCESSING_FAILED); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw TransformationException.createForFrameProcessorChain( + e, TransformationException.ERROR_CODE_GL_PROCESSING_FAILED); + } + oldestGlProcessingFuture = futures.peek(); + } + } + + /** + * Returns the number of input frames that have been {@linkplain #registerInputFrame() registered} + * but not completely processed yet. + */ + public int getPendingFrameCount() { + return pendingFrameCount.get(); + } + + /** Returns whether all frames have been processed. */ + public boolean isEnded() { + return inputStreamEnded && getPendingFrameCount() == 0; + } + + /** Informs the {@code FrameProcessorChain} that no further input frames should be accepted. */ + public void signalEndOfInputStream() { + inputStreamEnded = true; + } + + /** + * Releases all resources. + * + *

    If the frame processor chain is released before it has {@linkplain #isEnded() ended}, it + * will attempt to cancel processing any input frames that have already become available. Input + * frames that become available after release are ignored. + */ + public void release() { + releaseRequested = true; + while (!futures.isEmpty()) { + checkNotNull(futures.poll()).cancel(/* mayInterruptIfRunning= */ true); + } + futures.add( + singleThreadExecutorService.submit( + () -> { + for (int i = 0; i < frameProcessors.size(); i++) { + frameProcessors.get(i).release(); + } + GlUtil.destroyEglContext(eglDisplay, eglContext); + })); + if (inputSurfaceTexture != null) { + inputSurfaceTexture.release(); + } + if (inputSurface != null) { + inputSurface.release(); + } + singleThreadExecutorService.shutdown(); + } + + /** + * Creates the OpenGL surfaces. + * + *

    This method should only be called on the {@linkplain #THREAD_NAME background thread}. + */ + private void createOpenGlSurfaces(Surface outputSurface, @Nullable SurfaceView debugSurfaceView) { + checkState(Thread.currentThread().getName().equals(THREAD_NAME)); + checkStateNotNull(eglDisplay); + + if (enableExperimentalHdrEditing) { + // TODO(b/209404935): Don't assume BT.2020 PQ input/output. + eglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, outputSurface); + if (debugSurfaceView != null) { + debugPreviewEglSurface = + GlUtil.getEglSurfaceBt2020Pq(eglDisplay, checkNotNull(debugSurfaceView.getHolder())); + } + } else { + eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface); + if (debugSurfaceView != null) { + debugPreviewEglSurface = + GlUtil.getEglSurface(eglDisplay, checkNotNull(debugSurfaceView.getHolder())); + } + } + } + + /** + * Processes an input frame. + * + *

    This method should only be called on the {@linkplain #THREAD_NAME background thread}. + */ + @RequiresNonNull("inputSurfaceTexture") + private void processFrame() { + checkState(Thread.currentThread().getName().equals(THREAD_NAME)); + checkStateNotNull(eglSurface, "No output surface set."); + + inputSurfaceTexture.updateTexImage(); + long presentationTimeNs = inputSurfaceTexture.getTimestamp(); + long presentationTimeUs = presentationTimeNs / 1000; + inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); + ((ExternalCopyFrameProcessor) frameProcessors.get(0)) + .setTextureTransformMatrix(textureTransformMatrix); + + for (int i = 0; i < frameProcessors.size() - 1; i++) { + Size intermediateSize = frameProcessors.get(i).getOutputSize(); + GlUtil.focusFramebuffer( + eglDisplay, + eglContext, + eglSurface, + framebuffers[i], + intermediateSize.getWidth(), + intermediateSize.getHeight()); + clearOutputFrame(); + frameProcessors.get(i).updateProgramAndDraw(presentationTimeUs); + } + GlUtil.focusEglSurface( + eglDisplay, eglContext, eglSurface, outputSize.getWidth(), outputSize.getHeight()); + clearOutputFrame(); + getLast(frameProcessors).updateProgramAndDraw(presentationTimeUs); + + EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeNs); + EGL14.eglSwapBuffers(eglDisplay, eglSurface); + + if (debugPreviewEglSurface != null) { + GlUtil.focusEglSurface( + eglDisplay, eglContext, debugPreviewEglSurface, debugPreviewWidth, debugPreviewHeight); + clearOutputFrame(); + // The four-vertex triangle strip forms a quad. + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); + EGL14.eglSwapBuffers(eglDisplay, debugPreviewEglSurface); + } + + checkState(pendingFrameCount.getAndDecrement() > 0); + } + + private static void clearOutputFrame() { + GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GlUtil.checkGlError(); + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java index 214d7b121f..595bb743d3 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java @@ -208,6 +208,41 @@ import java.nio.ByteBuffer; } isStarted = false; + try { + stopMuxer(mediaMuxer); + } catch (RuntimeException e) { + // It doesn't matter that stopping the muxer throws if the transformation is being cancelled. + if (!forCancellation) { + throw new MuxerException("Failed to stop the muxer", e); + } + } finally { + mediaMuxer.release(); + } + } + + /** + * Converts a {@linkplain MimeTypes MIME type} into a {@linkplain MediaMuxer.OutputFormat + * MediaMuxer output format}. + * + * @param mimeType The {@linkplain MimeTypes MIME type} to convert. + * @return The corresponding {@linkplain MediaMuxer.OutputFormat MediaMuxer output format}. + * @throws IllegalArgumentException If the {@linkplain MimeTypes MIME type} is not supported as + * output format. + */ + private static int mimeTypeToMuxerOutputFormat(String mimeType) { + if (mimeType.equals(MimeTypes.VIDEO_MP4)) { + return MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4; + } else if (SDK_INT >= 21 && mimeType.equals(MimeTypes.VIDEO_WEBM)) { + return MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM; + } else { + throw new IllegalArgumentException("Unsupported output MIME type: " + mimeType); + } + } + + // Accesses MediaMuxer state via reflection to ensure that muxer resources can be released even + // if stopping fails. + @SuppressLint("PrivateApi") + private static void stopMuxer(MediaMuxer mediaMuxer) { try { mediaMuxer.stop(); } catch (RuntimeException e) { @@ -215,7 +250,7 @@ import java.nio.ByteBuffer; // Set the muxer state to stopped even if mediaMuxer.stop() failed so that // mediaMuxer.release() doesn't attempt to stop the muxer and therefore doesn't throw the // same exception without releasing its resources. This is already implemented in MediaMuxer - // from API level 30. + // from API level 30. See also b/80338884. try { Field muxerStoppedStateField = MediaMuxer.class.getDeclaredField("MUXER_STATE_STOPPED"); muxerStoppedStateField.setAccessible(true); @@ -227,31 +262,8 @@ import java.nio.ByteBuffer; // Do nothing. } } - // It doesn't matter that stopping the muxer throws if the transformation is being cancelled. - if (!forCancellation) { - throw new MuxerException("Failed to stop the muxer", e); - } - } finally { - mediaMuxer.release(); - } - } - - /** - * Converts a {@link MimeTypes MIME type} into a {@link MediaMuxer.OutputFormat MediaMuxer output - * format}. - * - * @param mimeType The {@link MimeTypes MIME type} to convert. - * @return The corresponding {@link MediaMuxer.OutputFormat MediaMuxer output format}. - * @throws IllegalArgumentException If the {@link MimeTypes MIME type} is not supported as output - * format. - */ - private static int mimeTypeToMuxerOutputFormat(String mimeType) { - if (mimeType.equals(MimeTypes.VIDEO_MP4)) { - return MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4; - } else if (SDK_INT >= 21 && mimeType.equals(MimeTypes.VIDEO_WEBM)) { - return MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM; - } else { - throw new IllegalArgumentException("Unsupported output MIME type: " + mimeType); + // Rethrow the original error. + throw e; } } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java index 55bd315111..31ff97ad14 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java @@ -15,167 +15,57 @@ */ package androidx.media3.transformer; -import static androidx.media3.common.util.Assertions.checkStateNotNull; - -import android.content.Context; -import android.graphics.Matrix; -import android.opengl.GLES20; -import androidx.media3.common.util.GlProgram; -import androidx.media3.common.util.GlUtil; +import android.util.Size; +import androidx.media3.common.util.UnstableApi; import java.io.IOException; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** Manages a GLSL shader program for applying a transformation matrix to a frame. */ -/* package */ class GlFrameProcessor { - - static { - GlUtil.glAssertionsEnabled = true; - } +/** + * Manages a GLSL shader program for processing a frame. + * + *

    Methods must be called in the following order: + * + *

      + *
    1. The constructor, for implementation-specific arguments. + *
    2. {@link #initialize(int,int,int)}, to set up graphics initialization. + *
    3. {@link #updateProgramAndDraw(long)}, to process one frame. + *
    4. {@link #release()}, upon conclusion of processing. + *
    + */ +@UnstableApi +public interface GlFrameProcessor { /** - * Returns a 4x4, column-major Matrix float array, from an input {@link Matrix}. This is useful - * for converting to the 4x4 column-major format commonly used in OpenGL. - */ - private static float[] getGlMatrixArray(Matrix matrix) { - float[] matrix3x3Array = new float[9]; - matrix.getValues(matrix3x3Array); - float[] matrix4x4Array = getMatrix4x4Array(matrix3x3Array); - - // Transpose from row-major to column-major representations. - float[] transposedMatrix4x4Array = new float[16]; - android.opengl.Matrix.transposeM( - transposedMatrix4x4Array, /* mTransOffset= */ 0, matrix4x4Array, /* mOffset= */ 0); - - return transposedMatrix4x4Array; - } - - /** - * Returns a 4x4 matrix array containing the 3x3 matrix array's contents. + * Performs all initialization that requires OpenGL, such as, loading and compiling a GLSL shader + * program. * - *

    The 3x3 matrix array is expected to be in 2 dimensions, and the 4x4 matrix array is expected - * to be in 3 dimensions. The output will have the third row/column's values be an identity - * matrix's values, so that vertex transformations using this matrix will not affect the z axis. - *
    - * Input format: [a, b, c, d, e, f, g, h, i]
    - * Output format: [a, b, 0, c, d, e, 0, f, 0, 0, 1, 0, g, h, 0, i] + *

    This method may only be called if there is a current OpenGL context. + * + * @param inputTexId Identifier of a 2D OpenGL texture. + * @param inputWidth The input width, in pixels. + * @param inputHeight The input height, in pixels. */ - private static float[] getMatrix4x4Array(float[] matrix3x3Array) { - float[] matrix4x4Array = new float[16]; - matrix4x4Array[10] = 1; - for (int inputRow = 0; inputRow < 3; inputRow++) { - for (int inputColumn = 0; inputColumn < 3; inputColumn++) { - int outputRow = (inputRow == 2) ? 3 : inputRow; - int outputColumn = (inputColumn == 2) ? 3 : inputColumn; - matrix4x4Array[outputRow * 4 + outputColumn] = matrix3x3Array[inputRow * 3 + inputColumn]; - } - } - return matrix4x4Array; - } - - private static final String VERTEX_SHADER_TRANSFORMATION_PATH = - "shaders/vertex_shader_transformation.glsl"; - private static final String FRAGMENT_SHADER_COPY_EXTERNAL_PATH = - "shaders/fragment_shader_copy_external.glsl"; - private static final String VERTEX_SHADER_TRANSFORMATION_ES3_PATH = - "shaders/vertex_shader_transformation_es3.glsl"; - private static final String FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH = - "shaders/fragment_shader_copy_external_yuv_es3.glsl"; - // Color transform coefficients from - // https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/libstagefright/colorconversion/ColorConverter.cpp;l=668-670;drc=487adf977a50cac3929eba15fad0d0f461c7ff0f. - private static final float[] MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM = { - 1.168f, 1.168f, 1.168f, - 0.0f, -0.188f, 2.148f, - 1.683f, -0.652f, 0.0f, - }; - - private final Context context; - private final Matrix transformationMatrix; - private final boolean enableExperimentalHdrEditing; - - private @MonotonicNonNull GlProgram glProgram; + void initialize(int inputTexId, int inputWidth, int inputHeight) throws IOException; /** - * Creates a new instance. + * Returns the output {@link Size} of frames processed through {@link + * #updateProgramAndDraw(long)}. * - * @param context A {@link Context}. - * @param transformationMatrix The transformation matrix to apply to each frame. - * @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal. + *

    This method may only be called after the frame processor has been {@link + * #initialize(int,int,int) initialized}. */ - public GlFrameProcessor( - Context context, Matrix transformationMatrix, boolean enableExperimentalHdrEditing) { - this.context = context; - this.transformationMatrix = transformationMatrix; - this.enableExperimentalHdrEditing = enableExperimentalHdrEditing; - } - - /** - * Does any initialization necessary such as loading and compiling a GLSL shader programs. - * - *

    This method may only be called after creating the OpenGL context and focusing a render - * target. - */ - public void initialize() throws IOException { - // TODO(b/205002913): check the loaded program is consistent with the attributes - // and uniforms expected in the code. - String vertexShaderFilePath = - enableExperimentalHdrEditing - ? VERTEX_SHADER_TRANSFORMATION_ES3_PATH - : VERTEX_SHADER_TRANSFORMATION_PATH; - String fragmentShaderFilePath = - enableExperimentalHdrEditing - ? FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH - : FRAGMENT_SHADER_COPY_EXTERNAL_PATH; - - glProgram = new GlProgram(context, vertexShaderFilePath, fragmentShaderFilePath); - // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. - glProgram.setBufferAttribute( - "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); - glProgram.setBufferAttribute( - "aTexCoords", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); - if (enableExperimentalHdrEditing) { - // In HDR editing mode the decoder output is sampled in YUV. - glProgram.setFloatsUniform("uColorTransform", MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM); - } - float[] transformationMatrixArray = getGlMatrixArray(transformationMatrix); - glProgram.setFloatsUniform("uTransformationMatrix", transformationMatrixArray); - } - - /** - * Sets the texture transform matrix for converting an external surface texture's coordinates to - * sampling locations. - * - * @param textureTransformMatrix The external surface texture's {@link - * android.graphics.SurfaceTexture#getTransformMatrix(float[]) transform matrix}. - */ - public void setTextureTransformMatrix(float[] textureTransformMatrix) { - checkStateNotNull(glProgram); - glProgram.setFloatsUniform("uTexTransform", textureTransformMatrix); - } + Size getOutputSize(); /** * Updates the shader program's vertex attributes and uniforms, binds them, and draws. * - *

    The frame processor must be {@link #initialize() initialized}. The caller is responsible for - * focussing the correct render target before calling this method. + *

    This method may only be called after the frame processor has been {@link + * #initialize(int,int,int) initialized}. The caller is responsible for focussing the correct + * render target before calling this method. * - * @param inputTexId The identifier of an OpenGL texture that the fragment shader can sample from. + * @param presentationTimeUs The presentation timestamp of the current frame, in microseconds. */ - // TODO(b/214975934): Also pass presentationTimeNs. - public void updateProgramAndDraw(int inputTexId) { - checkStateNotNull(glProgram); - glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* unit= */ 0); - glProgram.use(); - glProgram.bindAttributesAndUniforms(); - GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0); - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); - // The four-vertex triangle strip forms a quad. - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); - } + void updateProgramAndDraw(long presentationTimeUs); /** Releases all resources. */ - public void release() { - if (glProgram != null) { - glProgram.delete(); - } - } + void release(); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Muxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Muxer.java index df8138f837..86bb493476 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Muxer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Muxer.java @@ -27,12 +27,12 @@ import java.nio.ByteBuffer; /** * Abstracts media muxing operations. * - *

    Query whether {@link Factory#supportsOutputMimeType(String) container MIME type} and {@link - * Factory#supportsSampleMimeType(String, String) sample MIME types} are supported and {@link - * #addTrack(Format) add all tracks}, then {@link #writeSampleData(int, ByteBuffer, boolean, long) - * write sample data} to mux samples. Once any sample data has been written, it is not possible to - * add tracks. After writing all sample data, {@link #release(boolean) release} the instance to - * finish writing to the output and return any resources to the system. + *

    Query whether {@linkplain Factory#supportsOutputMimeType(String) container MIME type} and + * {@linkplain Factory#supportsSampleMimeType(String, String) sample MIME types} are supported and + * {@linkplain #addTrack(Format) add all tracks}, then {@linkplain #writeSampleData(int, ByteBuffer, + * boolean, long) write sample data} to mux samples. Once any sample data has been written, it is + * not possible to add tracks. After writing all sample data, {@linkplain #release(boolean) release} + * the instance to finish writing to the output and return any resources to the system. */ /* package */ interface Muxer { @@ -55,7 +55,7 @@ import java.nio.ByteBuffer; * Returns a new muxer writing to a file. * * @param path The path to the output file. - * @param outputMimeType The container {@link MimeTypes MIME type} of the output file. + * @param outputMimeType The container {@linkplain MimeTypes MIME type} of the output file. * @throws IllegalArgumentException If the path is invalid or the MIME type is not supported. * @throws IOException If an error occurs opening the output file for writing. */ @@ -68,7 +68,7 @@ import java.nio.ByteBuffer; * output. The file referenced by this ParcelFileDescriptor should not be used before the * muxer is released. It is the responsibility of the caller to close the * ParcelFileDescriptor. This can be done after this method returns. - * @param outputMimeType The {@link MimeTypes MIME type} of the output. + * @param outputMimeType The {@linkplain MimeTypes MIME type} of the output. * @throws IllegalArgumentException If the file descriptor is invalid or the MIME type is not * supported. * @throws IOException If an error occurs opening the output file descriptor for writing. @@ -76,18 +76,20 @@ import java.nio.ByteBuffer; Muxer create(ParcelFileDescriptor parcelFileDescriptor, String outputMimeType) throws IOException; - /** Returns whether the {@link MimeTypes MIME type} provided is a supported output format. */ + /** + * Returns whether the {@linkplain MimeTypes MIME type} provided is a supported output format. + */ boolean supportsOutputMimeType(String mimeType); /** - * Returns whether the sample {@link MimeTypes MIME type} is supported with the given container - * {@link MimeTypes MIME type}. + * Returns whether the sample {@linkplain MimeTypes MIME type} is supported with the given + * container {@linkplain MimeTypes MIME type}. */ boolean supportsSampleMimeType(@Nullable String sampleMimeType, String containerMimeType); /** - * Returns the supported sample {@link MimeTypes MIME types} for the given {@link C.TrackType} - * and container {@link MimeTypes MIME type}. + * Returns the supported sample {@linkplain MimeTypes MIME types} for the given {@link + * C.TrackType} and container {@linkplain MimeTypes MIME type}. */ ImmutableList getSupportedSampleMimeTypes( @C.TrackType int trackType, String containerMimeType); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java index 364337b910..97941b5ccf 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java @@ -17,6 +17,7 @@ package androidx.media3.transformer; import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Util.maxValue; import static androidx.media3.common.util.Util.minValue; import android.util.SparseIntArray; @@ -47,6 +48,7 @@ import java.nio.ByteBuffer; private final Muxer muxer; private final Muxer.Factory muxerFactory; private final SparseIntArray trackTypeToIndex; + private final SparseIntArray trackTypeToSampleCount; private final SparseLongArray trackTypeToTimeUs; private final SparseLongArray trackTypeToBytesWritten; private final String containerMimeType; @@ -61,7 +63,9 @@ import java.nio.ByteBuffer; this.muxer = muxer; this.muxerFactory = muxerFactory; this.containerMimeType = containerMimeType; + trackTypeToIndex = new SparseIntArray(); + trackTypeToSampleCount = new SparseIntArray(); trackTypeToTimeUs = new SparseLongArray(); trackTypeToBytesWritten = new SparseLongArray(); previousTrackType = C.TRACK_TYPE_NONE; @@ -70,10 +74,10 @@ import java.nio.ByteBuffer; /** * Registers an output track. * - *

    All tracks must be registered before any track format is {@link #addTrackFormat(Format) + *

    All tracks must be registered before any track format is {@linkplain #addTrackFormat(Format) * added}. * - * @throws IllegalStateException If a track format was {@link #addTrackFormat(Format) added} + * @throws IllegalStateException If a track format was {@linkplain #addTrackFormat(Format) added} * before calling this method. */ public void registerTrack() { @@ -82,14 +86,14 @@ import java.nio.ByteBuffer; trackCount++; } - /** Returns whether the sample {@link MimeTypes MIME type} is supported. */ + /** Returns whether the sample {@linkplain MimeTypes MIME type} is supported. */ public boolean supportsSampleMimeType(@Nullable String mimeType) { return muxerFactory.supportsSampleMimeType(mimeType, containerMimeType); } /** - * Returns the supported {@link MimeTypes MIME types} for the given {@link C.TrackType track - * type}. + * Returns the supported {@linkplain MimeTypes MIME types} for the given {@linkplain C.TrackType + * track type}. */ public ImmutableList getSupportedSampleMimeTypes(@C.TrackType int trackType) { return muxerFactory.getSupportedSampleMimeTypes(trackType, containerMimeType); @@ -98,9 +102,9 @@ import java.nio.ByteBuffer; /** * Adds a track format to the muxer. * - *

    The tracks must all be {@link #registerTrack() registered} before any format is added and - * all the formats must be added before samples are {@link #writeSample(int, ByteBuffer, boolean, - * long) written}. + *

    The tracks must all be {@linkplain #registerTrack() registered} before any format is added + * and all the formats must be added before samples are {@linkplain #writeSample(int, ByteBuffer, + * boolean, long) written}. * * @param format The {@link Format} to be added. * @throws IllegalStateException If the format is unsupported or if there is already a track @@ -122,6 +126,7 @@ import java.nio.ByteBuffer; int trackIndex = muxer.addTrack(format); trackTypeToIndex.put(trackType, trackIndex); + trackTypeToSampleCount.put(trackType, 0); trackTypeToTimeUs.put(trackType, 0L); trackTypeToBytesWritten.put(trackType, 0L); trackFormatCount++; @@ -133,16 +138,16 @@ import java.nio.ByteBuffer; /** * Attempts to write a sample to the muxer. * - * @param trackType The {@link C.TrackType track type} of the sample. + * @param trackType The {@linkplain C.TrackType track type} of the sample. * @param data The sample to write. * @param isKeyFrame Whether the sample is a key frame. * @param presentationTimeUs The presentation time of the sample in microseconds. * @return Whether the sample was successfully written. This is {@code false} if the muxer hasn't - * {@link #addTrackFormat(Format) received a format} for every {@link #registerTrack() - * registered track}, or if it should write samples of other track types first to ensure a - * good interleaving. - * @throws IllegalStateException If the muxer doesn't have any {@link #endTrack(int) non-ended} - * track of the given track type. + * {@linkplain #addTrackFormat(Format) received a format} for every {@linkplain + * #registerTrack() registered track}, or if it should write samples of other track types + * first to ensure a good interleaving. + * @throws IllegalStateException If the muxer doesn't have any {@linkplain #endTrack(int) + * non-ended} track of the given track type. * @throws Muxer.MuxerException If the underlying muxer fails to write the sample. */ public boolean writeSample( @@ -157,9 +162,12 @@ import java.nio.ByteBuffer; return false; } + trackTypeToSampleCount.put(trackType, trackTypeToSampleCount.get(trackType) + 1); trackTypeToBytesWritten.put( trackType, trackTypeToBytesWritten.get(trackType) + data.remaining()); - trackTypeToTimeUs.put(trackType, presentationTimeUs); + if (trackTypeToTimeUs.get(trackType) < presentationTimeUs) { + trackTypeToTimeUs.put(trackType, presentationTimeUs); + } muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs); previousTrackType = trackType; @@ -215,6 +223,16 @@ import java.nio.ByteBuffer; /* divisor= */ trackDurationUs); } + /** Returns the number of samples written to the track of the provided {@code trackType}. */ + public int getTrackSampleCount(@C.TrackType int trackType) { + return trackTypeToSampleCount.get(trackType, /* valueIfKeyNotFound= */ 0); + } + + /** Returns the duration of the longest track in milliseconds. */ + public long getDurationMs() { + return Util.usToMs(maxValue(trackTypeToTimeUs)); + } + /** * Returns whether the muxer can write a sample of the given track type. * diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java new file mode 100644 index 0000000000..8df3d0f9ec --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java @@ -0,0 +1,165 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import android.content.Context; +import android.graphics.Matrix; +import android.util.Size; +import androidx.annotation.VisibleForTesting; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.UnstableApi; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** Controls how a frame is viewed, by changing resolution. */ +// TODO(b/213190310): Implement crop, aspect ratio changes, etc. +@UnstableApi +public final class PresentationFrameProcessor implements GlFrameProcessor { + + /** A builder for {@link PresentationFrameProcessor} instances. */ + public static final class Builder { + + // Mandatory field. + private final Context context; + + // Optional field. + private int outputHeight; + + /** + * Creates a builder with default values. + * + * @param context The {@link Context}. + */ + public Builder(Context context) { + this.context = context; + outputHeight = C.LENGTH_UNSET; + } + + /** + * Sets the output resolution using the output height. + * + *

    The default value {@link C#LENGTH_UNSET} corresponds to using the same height as the + * input. Output width of the displayed frame will scale to preserve the frame's aspect ratio + * after other transformations. + * + *

    For example, a 1920x1440 frame can be scaled to 640x480 by calling setResolution(480). + * + * @param outputHeight The output height of the displayed frame, in pixels. + * @return This builder. + */ + public Builder setResolution(int outputHeight) { + this.outputHeight = outputHeight; + return this; + } + + public PresentationFrameProcessor build() { + return new PresentationFrameProcessor(context, outputHeight); + } + } + + static { + GlUtil.glAssertionsEnabled = true; + } + + private final Context context; + private final int requestedHeight; + + private @MonotonicNonNull Size outputSize; + private int outputRotationDegrees; + private @MonotonicNonNull Matrix transformationMatrix; + private @MonotonicNonNull AdvancedFrameProcessor advancedFrameProcessor; + + /** + * Creates a new instance. + * + * @param context The {@link Context}. + * @param requestedHeight The height of the output frame, in pixels. + */ + private PresentationFrameProcessor(Context context, int requestedHeight) { + this.context = context; + this.requestedHeight = requestedHeight; + + outputRotationDegrees = C.LENGTH_UNSET; + } + + @Override + public void initialize(int inputTexId, int inputWidth, int inputHeight) throws IOException { + configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + advancedFrameProcessor = new AdvancedFrameProcessor(context, transformationMatrix); + advancedFrameProcessor.initialize(inputTexId, inputWidth, inputHeight); + } + + @Override + public Size getOutputSize() { + return checkStateNotNull(outputSize); + } + + /** + * Returns {@link Format#rotationDegrees} for the output frame. + * + *

    Return values may be {@code 0} or {@code 90} degrees. + * + *

    The frame processor must be {@linkplain #initialize(int,int,int) initialized}. + */ + public int getOutputRotationDegrees() { + checkState(outputRotationDegrees != C.LENGTH_UNSET); + return outputRotationDegrees; + } + + @Override + public void updateProgramAndDraw(long presentationTimeUs) { + checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs); + } + + @Override + public void release() { + if (advancedFrameProcessor != null) { + advancedFrameProcessor.release(); + } + } + + @EnsuresNonNull("transformationMatrix") + @VisibleForTesting // Allows roboletric testing of output size calculation without OpenGL. + /* package */ void configureOutputSizeAndTransformationMatrix(int inputWidth, int inputHeight) { + transformationMatrix = new Matrix(); + int displayWidth = inputWidth; + int displayHeight = inputHeight; + // Scale width and height to desired requestedHeight, preserving aspect ratio. + if (requestedHeight != C.LENGTH_UNSET && requestedHeight != displayHeight) { + displayWidth = Math.round((float) requestedHeight * displayWidth / displayHeight); + displayHeight = requestedHeight; + } + // Encoders commonly support higher maximum widths than maximum heights. Rotate the decoded + // frame before encoding, so the encoded frame's width >= height, and set + // outputRotationDegrees to ensure the frame is displayed in the correct orientation. + if (displayHeight > displayWidth) { + outputRotationDegrees = 90; + // TODO(b/201293185): After fragment shader transformations are implemented, put + // postRotate in a later GlFrameProcessor. + transformationMatrix.postRotate(outputRotationDegrees); + outputSize = new Size(displayHeight, displayWidth); + } else { + outputRotationDegrees = 0; + outputSize = new Size(displayWidth, displayHeight); + } + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java index b2aa387111..63ec1e8c75 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java @@ -40,8 +40,8 @@ import androidx.media3.decoder.DecoderInputBuffer; void queueInputBuffer() throws TransformationException; /** - * Processes the input data and returns whether more data can be processed by calling this method - * again. + * Processes the input data and returns whether it may be possible to process more data by calling + * this method again. */ boolean processData() throws TransformationException; diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java new file mode 100644 index 0000000000..74032637e3 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java @@ -0,0 +1,187 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.content.Context; +import android.graphics.Matrix; +import android.util.Size; +import androidx.annotation.VisibleForTesting; +import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.UnstableApi; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Applies a simple rotation and/or scale in the vertex shader. All input frames' pixels will be + * preserved, potentially changing the width and height of the frame by scaling dimensions to fit. + * The background color will default to black. + */ +@UnstableApi +public final class ScaleToFitFrameProcessor implements GlFrameProcessor { + + /** A builder for {@link ScaleToFitFrameProcessor} instances. */ + public static final class Builder { + // Mandatory field. + private final Context context; + + // Optional fields. + private float scaleX; + private float scaleY; + private float rotationDegrees; + + /** + * Creates a builder with default values. + * + * @param context The {@link Context}. + */ + public Builder(Context context) { + this.context = context; + + scaleX = 1; + scaleY = 1; + rotationDegrees = 0; + } + + /** + * Sets the x and y axis scaling factors to apply to each frame's width and height. + * + *

    The values default to 1, which corresponds to not scaling along both axes. + * + * @param scaleX The multiplier by which the frame will scale horizontally, along the x-axis. + * @param scaleY The multiplier by which the frame will scale vertically, along the y-axis. + * @return This builder. + */ + public Builder setScale(float scaleX, float scaleY) { + this.scaleX = scaleX; + this.scaleY = scaleY; + return this; + } + + /** + * Sets the counterclockwise rotation degrees. + * + *

    The default value, 0, corresponds to not applying any rotation. + * + * @param rotationDegrees The counterclockwise rotation, in degrees. + * @return This builder. + */ + public Builder setRotationDegrees(float rotationDegrees) { + this.rotationDegrees = rotationDegrees; + return this; + } + + public ScaleToFitFrameProcessor build() { + return new ScaleToFitFrameProcessor(context, scaleX, scaleY, rotationDegrees); + } + } + + static { + GlUtil.glAssertionsEnabled = true; + } + + private final Context context; + private final Matrix transformationMatrix; + + private @MonotonicNonNull AdvancedFrameProcessor advancedFrameProcessor; + private @MonotonicNonNull Size outputSize; + private @MonotonicNonNull Matrix adjustedTransformationMatrix; + + /** + * Creates a new instance. + * + * @param context The {@link Context}. + * @param scaleX The multiplier by which the frame will scale horizontally, along the x-axis. + * @param scaleY The multiplier by which the frame will scale vertically, along the y-axis. + * @param rotationDegrees How much to rotate the frame counterclockwise, in degrees. + */ + private ScaleToFitFrameProcessor( + Context context, float scaleX, float scaleY, float rotationDegrees) { + + this.context = context; + this.transformationMatrix = new Matrix(); + this.transformationMatrix.postScale(scaleX, scaleY); + this.transformationMatrix.postRotate(rotationDegrees); + } + + @Override + public void initialize(int inputTexId, int inputWidth, int inputHeight) throws IOException { + configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + advancedFrameProcessor = new AdvancedFrameProcessor(context, adjustedTransformationMatrix); + advancedFrameProcessor.initialize(inputTexId, inputWidth, inputHeight); + } + + @Override + public Size getOutputSize() { + return checkStateNotNull(outputSize); + } + + @Override + public void updateProgramAndDraw(long presentationTimeUs) { + checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs); + } + + @Override + public void release() { + if (advancedFrameProcessor != null) { + advancedFrameProcessor.release(); + } + } + + @EnsuresNonNull("adjustedTransformationMatrix") + @VisibleForTesting // Allows roboletric testing of output size calculation without OpenGL. + /* package */ void configureOutputSizeAndTransformationMatrix(int inputWidth, int inputHeight) { + adjustedTransformationMatrix = new Matrix(transformationMatrix); + + if (transformationMatrix.isIdentity()) { + outputSize = new Size(inputWidth, inputHeight); + return; + } + + float inputAspectRatio = (float) inputWidth / inputHeight; + // Scale frames by inputAspectRatio, to account for OpenGL's normalized device + // coordinates (NDC) (a square from -1 to 1 for both x and y) and preserve rectangular + // display of input pixels during transformations (ex. rotations). With scaling, + // transformationMatrix operations operate on a rectangle for x from -inputAspectRatio to + // inputAspectRatio, and y from -1 to 1. + adjustedTransformationMatrix.preScale(/* sx= */ inputAspectRatio, /* sy= */ 1f); + adjustedTransformationMatrix.postScale(/* sx= */ 1f / inputAspectRatio, /* sy= */ 1f); + + // Modify transformationMatrix to keep input pixels. + float[][] transformOnNdcPoints = {{-1, -1, 0, 1}, {-1, 1, 0, 1}, {1, -1, 0, 1}, {1, 1, 0, 1}}; + float xMin = Float.MAX_VALUE; + float xMax = Float.MIN_VALUE; + float yMin = Float.MAX_VALUE; + float yMax = Float.MIN_VALUE; + for (float[] transformOnNdcPoint : transformOnNdcPoints) { + adjustedTransformationMatrix.mapPoints(transformOnNdcPoint); + xMin = min(xMin, transformOnNdcPoint[0]); + xMax = max(xMax, transformOnNdcPoint[0]); + yMin = min(yMin, transformOnNdcPoint[1]); + yMax = max(yMax, transformOnNdcPoint[1]); + } + + float ndcWidthAndHeight = 2f; // Length from -1 to 1. + float xScale = (xMax - xMin) / ndcWidthAndHeight; + float yScale = (yMax - yMin) / ndcWidthAndHeight; + adjustedTransformationMatrix.postScale(1f / xScale, 1f / yScale); + outputSize = new Size(Math.round(inputWidth * xScale), Math.round(inputHeight * yScale)); + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SefSlowMotionFlattener.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SefSlowMotionFlattener.java index 58405ea29b..8602733a93 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SefSlowMotionFlattener.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SefSlowMotionFlattener.java @@ -247,8 +247,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * output frame rate might be variable. * *

    This method can only be called if all the frames until the current one (included) have been - * {@link #processCurrentFrame(int, long) processed} in order, and if the next frames have not - * been processed yet. + * {@linkplain #processCurrentFrame(int, long) processed} in order, and if the next frames have + * not been processed yet. */ @VisibleForTesting /* package */ long getCurrentFrameOutputTimeUs(long inputTimeUs) { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java index bf4bfa7ebb..aad804edec 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java @@ -147,7 +147,7 @@ public final class TransformationException extends Exception { /** * Caused by the output format for a track not being supported. * - *

    Supported output formats are limited by the muxer's capabilities and the {@link + *

    Supported output formats are limited by the muxer's capabilities and the {@linkplain * Codec.DecoderFactory encoders} available. */ public static final int ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED = 4003; @@ -274,15 +274,15 @@ public final class TransformationException extends Exception { } /** - * Creates an instance for a {@link FrameEditor} related exception. + * Creates an instance for a {@link FrameProcessorChain} related exception. * * @param cause The cause of the failure. * @param errorCode See {@link #errorCode}. * @return The created instance. */ - /* package */ static TransformationException createForFrameEditor( + /* package */ static TransformationException createForFrameProcessorChain( Throwable cause, int errorCode) { - return new TransformationException("FrameEditor error", cause, errorCode); + return new TransformationException("FrameProcessorChain error", cause, errorCode); } /** diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java index cada9eda7c..33a5ecb4c1 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java @@ -18,7 +18,6 @@ package androidx.media3.transformer; import static androidx.media3.common.util.Assertions.checkArgument; -import android.graphics.Matrix; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.MimeTypes; @@ -34,11 +33,14 @@ public final class TransformationRequest { /** A builder for {@link TransformationRequest} instances. */ public static final class Builder { - private Matrix transformationMatrix; private boolean flattenForSlowMotion; + private float scaleX; + private float scaleY; + private float rotationDegrees; private int outputHeight; @Nullable private String audioMimeType; @Nullable private String videoMimeType; + private boolean enableRequestSdrToneMapping; private boolean enableHdrEditing; /** @@ -48,46 +50,29 @@ public final class TransformationRequest { * {@link TransformationRequest}. */ public Builder() { - transformationMatrix = new Matrix(); + scaleX = 1; + scaleY = 1; outputHeight = C.LENGTH_UNSET; } private Builder(TransformationRequest transformationRequest) { - this.transformationMatrix = new Matrix(transformationRequest.transformationMatrix); this.flattenForSlowMotion = transformationRequest.flattenForSlowMotion; + this.scaleX = transformationRequest.scaleX; + this.scaleY = transformationRequest.scaleY; + this.rotationDegrees = transformationRequest.rotationDegrees; this.outputHeight = transformationRequest.outputHeight; this.audioMimeType = transformationRequest.audioMimeType; this.videoMimeType = transformationRequest.videoMimeType; + this.enableRequestSdrToneMapping = transformationRequest.enableRequestSdrToneMapping; this.enableHdrEditing = transformationRequest.enableHdrEditing; } /** - * Sets the transformation matrix. The default value is to apply no change. + * Sets whether the input should be flattened for media containing slow motion markers. * - *

    This can be used to perform operations supported by {@link Matrix}, like scaling and - * rotating the video. - * - *

    The video dimensions will be on the x axis, from -aspectRatio to aspectRatio, and on the y - * axis, from -1 to 1. - * - *

    For now, resolution will not be affected by this method. - * - * @param transformationMatrix The transformation to apply to video frames. - * @return This builder. - */ - public Builder setTransformationMatrix(Matrix transformationMatrix) { - // TODO(b/201293185): Implement an AdvancedFrameEditor to handle translation, as the current - // transformationMatrix is automatically adjusted to focus on the original pixels and - // effectively undo translations. - this.transformationMatrix = new Matrix(transformationMatrix); - return this; - } - - /** - * Sets whether the input should be flattened for media containing slow motion markers. The - * transformed output is obtained by removing the slow motion metadata and by actually slowing - * down the parts of the video and audio streams defined in this metadata. The default value for - * {@code flattenForSlowMotion} is {@code false}. + *

    The transformed output is obtained by removing the slow motion metadata and by actually + * slowing down the parts of the video and audio streams defined in this metadata. The default + * value for {@code flattenForSlowMotion} is {@code false}. * *

    Only Samsung Extension Format (SEF) slow motion metadata type is supported. The * transformation has no effect if the input does not contain this metadata type. @@ -114,25 +99,57 @@ public final class TransformationRequest { } /** - * Sets the output resolution using the output height. The default value {@link C#LENGTH_UNSET} - * corresponds to using the same height as the input. Output width of the displayed video will - * scale to preserve the video's aspect ratio after other transformations. + * Sets the x and y axis scaling factors to apply to each frame's width and height, stretching + * the video along these axes appropriately. + * + *

    The values default to 1, which corresponds to not scaling along both axes. + * + * @param scaleX The multiplier by which the frame will scale horizontally, along the x-axis. + * @param scaleY The multiplier by which the frame will scale vertically, along the y-axis. + * @return This builder. + */ + public Builder setScale(float scaleX, float scaleY) { + this.scaleX = scaleX; + this.scaleY = scaleY; + return this; + } + + /** + * Sets the rotation, in degrees, counterclockwise, to apply to each frame, automatically + * adjusting the frame's width and height to preserve all input pixels. + * + *

    The default value, 0, corresponds to not applying any rotation. + * + * @param rotationDegrees The counterclockwise rotation, in degrees. + * @return This builder. + */ + public Builder setRotationDegrees(float rotationDegrees) { + this.rotationDegrees = rotationDegrees; + return this; + } + + /** + * Sets the output resolution using the output height. + * + *

    The default value {@link C#LENGTH_UNSET} corresponds to using the same height as the + * input. Output width of the displayed video will scale to preserve the video's aspect ratio + * after other transformations. * *

    For example, a 1920x1440 video can be scaled to 640x480 by calling setResolution(480). * * @param outputHeight The output height of the displayed video, in pixels. * @return This builder. - * @throws IllegalArgumentException If the {@code outputHeight} is not supported. */ public Builder setResolution(int outputHeight) { - // TODO(b/201293185): Restructure to input a Presentation class. this.outputHeight = outputHeight; return this; } /** - * Sets the video MIME type of the output. The default value is {@code null} which corresponds - * to using the same MIME type as the input. Supported MIME types are: + * Sets the video MIME type of the output. + * + *

    The default value is {@code null} which corresponds to using the same MIME type as the + * input. Supported MIME types are: * *

      *
    • {@link MimeTypes#VIDEO_H263} @@ -144,7 +161,7 @@ public final class TransformationRequest { * @param videoMimeType The MIME type of the video samples in the output. * @return This builder. * @throws IllegalArgumentException If the {@code videoMimeType} is non-null but not a video - * {@link MimeTypes MIME type}. + * {@linkplain MimeTypes MIME type}. */ public Builder setVideoMimeType(@Nullable String videoMimeType) { checkArgument( @@ -155,8 +172,10 @@ public final class TransformationRequest { } /** - * Sets the audio MIME type of the output. The default value is {@code null} which corresponds - * to using the same MIME type as the input. Supported MIME types are: + * Sets the audio MIME type of the output. + * + *

      The default value is {@code null} which corresponds to using the same MIME type as the + * input. Supported MIME types are: * *

        *
      • {@link MimeTypes#AUDIO_AAC} @@ -167,7 +186,7 @@ public final class TransformationRequest { * @param audioMimeType The MIME type of the audio samples in the output. * @return This builder. * @throws IllegalArgumentException If the {@code audioMimeType} is non-null but not an audio - * {@link MimeTypes MIME type}. + * {@linkplain MimeTypes MIME type}. */ public Builder setAudioMimeType(@Nullable String audioMimeType) { checkArgument( @@ -177,13 +196,30 @@ public final class TransformationRequest { return this; } + /** + * Sets whether to request tone-mapping to standard dynamic range (SDR). If enabled and + * supported, high dynamic range (HDR) input will be tone-mapped into an SDR opto-electrical + * transfer function before processing. + * + *

        The setting has no effect if the input is already in SDR, or if tone-mapping is not + * supported. Currently tone-mapping is only guaranteed to be supported from Android T onwards. + * + * @param enableRequestSdrToneMapping Whether to request tone-mapping down to SDR. + * @return This builder. + */ + public Builder setEnableRequestSdrToneMapping(boolean enableRequestSdrToneMapping) { + this.enableRequestSdrToneMapping = enableRequestSdrToneMapping; + return this; + } + /** * Sets whether to attempt to process any input video stream as a high dynamic range (HDR) * signal. * *

        This method is experimental, and will be renamed or removed in a future release. The HDR * editing feature is under development and is intended for developing/testing HDR processing - * and encoding support. + * and encoding support. HDR editing can't be enabled at the same time as {@linkplain + * #setEnableRequestSdrToneMapping(boolean) SDR tone-mapping}. * * @param enableHdrEditing Whether to attempt to process any input video stream as a high * dynamic range (HDR) signal. @@ -197,27 +233,44 @@ public final class TransformationRequest { /** Builds a {@link TransformationRequest} instance. */ public TransformationRequest build() { return new TransformationRequest( - transformationMatrix, flattenForSlowMotion, + scaleX, + scaleY, + rotationDegrees, outputHeight, audioMimeType, videoMimeType, + enableRequestSdrToneMapping, enableHdrEditing); } } - /** - * A {@link Matrix transformation matrix} to apply to video frames. - * - * @see Builder#setTransformationMatrix(Matrix) - */ - public final Matrix transformationMatrix; /** * Whether the input should be flattened for media containing slow motion markers. * * @see Builder#setFlattenForSlowMotion(boolean) */ public final boolean flattenForSlowMotion; + /** + * The requested scale factor, on the x-axis, of the output video, or 1 if inferred from the + * input. + * + * @see Builder#setScale(float, float) + */ + public final float scaleX; + /** + * The requested scale factor, on the y-axis, of the output video, or 1 if inferred from the + * input. + * + * @see Builder#setScale(float, float) + */ + public final float scaleY; + /** + * The requested rotation, in degrees, of the output video, or 0 if inferred from the input. + * + * @see Builder#setRotationDegrees(float) + */ + public final float rotationDegrees; /** * The requested height of the output video, or {@link C#LENGTH_UNSET} if inferred from the input. * @@ -225,19 +278,22 @@ public final class TransformationRequest { */ public final int outputHeight; /** - * The requested output audio sample {@link MimeTypes MIME type}, or {@code null} if inferred from - * the input. + * The requested output audio sample {@linkplain MimeTypes MIME type}, or {@code null} if inferred + * from the input. * * @see Builder#setAudioMimeType(String) */ @Nullable public final String audioMimeType; /** - * The requested output video sample {@link MimeTypes MIME type}, or {@code null} if inferred from - * the input. + * The requested output video sample {@linkplain MimeTypes MIME type}, or {@code null} if inferred + * from the input. * * @see Builder#setVideoMimeType(String) */ @Nullable public final String videoMimeType; + /** Whether to request tone-mapping to standard dynamic range (SDR). */ + public final boolean enableRequestSdrToneMapping; + /** * Whether to attempt to process any input video stream as a high dynamic range (HDR) signal. * @@ -246,17 +302,24 @@ public final class TransformationRequest { public final boolean enableHdrEditing; private TransformationRequest( - Matrix transformationMatrix, boolean flattenForSlowMotion, + float scaleX, + float scaleY, + float rotationDegrees, int outputHeight, @Nullable String audioMimeType, @Nullable String videoMimeType, + boolean enableRequestSdrToneMapping, boolean enableHdrEditing) { - this.transformationMatrix = transformationMatrix; + checkArgument(!enableHdrEditing || !enableRequestSdrToneMapping); this.flattenForSlowMotion = flattenForSlowMotion; + this.scaleX = scaleX; + this.scaleY = scaleY; + this.rotationDegrees = rotationDegrees; this.outputHeight = outputHeight; this.audioMimeType = audioMimeType; this.videoMimeType = videoMimeType; + this.enableRequestSdrToneMapping = enableRequestSdrToneMapping; this.enableHdrEditing = enableHdrEditing; } @@ -269,21 +332,27 @@ public final class TransformationRequest { return false; } TransformationRequest that = (TransformationRequest) o; - return transformationMatrix.equals(that.transformationMatrix) - && flattenForSlowMotion == that.flattenForSlowMotion + return flattenForSlowMotion == that.flattenForSlowMotion + && scaleX == that.scaleX + && scaleY == that.scaleY + && rotationDegrees == that.rotationDegrees && outputHeight == that.outputHeight && Util.areEqual(audioMimeType, that.audioMimeType) && Util.areEqual(videoMimeType, that.videoMimeType) + && enableRequestSdrToneMapping == that.enableRequestSdrToneMapping && enableHdrEditing == that.enableHdrEditing; } @Override public int hashCode() { - int result = transformationMatrix.hashCode(); - result = 31 * result + (flattenForSlowMotion ? 1 : 0); + int result = (flattenForSlowMotion ? 1 : 0); + result = 31 * result + Float.floatToIntBits(scaleX); + result = 31 * result + Float.floatToIntBits(scaleY); + result = 31 * result + Float.floatToIntBits(rotationDegrees); result = 31 * result + outputHeight; result = 31 * result + (audioMimeType != null ? audioMimeType.hashCode() : 0); result = 31 * result + (videoMimeType != null ? videoMimeType.hashCode() : 0); + result = 31 * result + (enableRequestSdrToneMapping ? 1 : 0); result = 31 * result + (enableHdrEditing ? 1 : 0); return result; } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java index b5ece274a6..7aba68e647 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java @@ -27,16 +27,30 @@ public final class TransformationResult { /** A builder for {@link TransformationResult} instances. */ public static final class Builder { + private long durationMs; private long fileSizeBytes; private int averageAudioBitrate; private int averageVideoBitrate; + private int videoFrameCount; public Builder() { + durationMs = C.TIME_UNSET; fileSizeBytes = C.LENGTH_UNSET; averageAudioBitrate = C.RATE_UNSET_INT; averageVideoBitrate = C.RATE_UNSET_INT; } + /** + * Sets the duration of the video in milliseconds. + * + *

        Input must be positive or {@link C#TIME_UNSET}. + */ + public Builder setDurationMs(long durationMs) { + checkArgument(durationMs > 0 || durationMs == C.TIME_UNSET); + this.durationMs = durationMs; + return this; + } + /** * Sets the file size in bytes. * @@ -70,11 +84,25 @@ public final class TransformationResult { return this; } + /** + * Sets the number of video frames. + * + *

        Input must be positive or {@code 0}. + */ + public Builder setVideoFrameCount(int videoFrameCount) { + checkArgument(videoFrameCount >= 0); + this.videoFrameCount = videoFrameCount; + return this; + } + public TransformationResult build() { - return new TransformationResult(fileSizeBytes, averageAudioBitrate, averageVideoBitrate); + return new TransformationResult( + durationMs, fileSizeBytes, averageAudioBitrate, averageVideoBitrate, videoFrameCount); } } + /** The duration of the file in milliseconds, or {@link C#TIME_UNSET} if unset or unknown. */ + public final long durationMs; /** The size of the file in bytes, or {@link C#LENGTH_UNSET} if unset or unknown. */ public final long fileSizeBytes; /** @@ -85,19 +113,29 @@ public final class TransformationResult { * The average bitrate of the video track data, or {@link C#RATE_UNSET_INT} if unset or unknown. */ public final int averageVideoBitrate; + /** The number of video frames. */ + public final int videoFrameCount; private TransformationResult( - long fileSizeBytes, int averageAudioBitrate, int averageVideoBitrate) { + long durationMs, + long fileSizeBytes, + int averageAudioBitrate, + int averageVideoBitrate, + int videoFrameCount) { + this.durationMs = durationMs; this.fileSizeBytes = fileSizeBytes; this.averageAudioBitrate = averageAudioBitrate; this.averageVideoBitrate = averageVideoBitrate; + this.videoFrameCount = videoFrameCount; } public Builder buildUpon() { return new Builder() + .setDurationMs(durationMs) .setFileSizeBytes(fileSizeBytes) .setAverageAudioBitrate(averageAudioBitrate) - .setAverageVideoBitrate(averageVideoBitrate); + .setAverageVideoBitrate(averageVideoBitrate) + .setVideoFrameCount(videoFrameCount); } @Override @@ -109,16 +147,20 @@ public final class TransformationResult { return false; } TransformationResult result = (TransformationResult) o; - return fileSizeBytes == result.fileSizeBytes + return durationMs == result.durationMs + && fileSizeBytes == result.fileSizeBytes && averageAudioBitrate == result.averageAudioBitrate - && averageVideoBitrate == result.averageVideoBitrate; + && averageVideoBitrate == result.averageVideoBitrate + && videoFrameCount == result.videoFrameCount; } @Override public int hashCode() { - int result = (int) fileSizeBytes; + int result = (int) durationMs; + result = 31 * result + (int) fileSizeBytes; result = 31 * result + averageAudioBitrate; result = 31 * result + averageVideoBitrate; + result = 31 * result + videoFrameCount; return result; } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index e8219e9723..c812cdab23 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -59,11 +59,13 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.video.VideoRendererEventListener; import androidx.media3.extractor.DefaultExtractorsFactory; import androidx.media3.extractor.mp4.Mp4Extractor; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -102,6 +104,7 @@ public final class Transformer { private boolean removeVideo; private String containerMimeType; private TransformationRequest transformationRequest; + private ImmutableList frameProcessors; private ListenerSet listeners; private DebugViewProvider debugViewProvider; private Looper looper; @@ -121,6 +124,7 @@ public final class Transformer { debugViewProvider = DebugViewProvider.NONE; containerMimeType = MimeTypes.VIDEO_MP4; transformationRequest = new TransformationRequest.Builder().build(); + frameProcessors = ImmutableList.of(); } /** @@ -137,7 +141,8 @@ public final class Transformer { encoderFactory = Codec.EncoderFactory.DEFAULT; debugViewProvider = DebugViewProvider.NONE; containerMimeType = MimeTypes.VIDEO_MP4; - this.transformationRequest = new TransformationRequest.Builder().build(); + transformationRequest = new TransformationRequest.Builder().build(); + frameProcessors = ImmutableList.of(); } /** Creates a builder with the values of the provided {@link Transformer}. */ @@ -149,6 +154,7 @@ public final class Transformer { this.removeVideo = transformer.removeVideo; this.containerMimeType = transformer.containerMimeType; this.transformationRequest = transformer.transformationRequest; + this.frameProcessors = transformer.frameProcessors; this.listeners = transformer.listeners; this.looper = transformer.looper; this.encoderFactory = transformer.encoderFactory; @@ -168,6 +174,10 @@ public final class Transformer { /** * Sets the {@link TransformationRequest} which configures the editing and transcoding options. * + *

        Actual applied values may differ, per device capabilities. {@link + * Listener#onFallbackApplied(MediaItem, TransformationRequest, TransformationRequest)} will be + * invoked with the actual applied values. + * * @param transformationRequest The {@link TransformationRequest}. * @return This builder. */ @@ -177,9 +187,28 @@ public final class Transformer { } /** - * Sets the {@link MediaSource.Factory} to be used to retrieve the inputs to transform. The - * default value is a {@link DefaultMediaSourceFactory} built with the context provided in - * {@link #Builder(Context) the constructor}. + * Sets the {@linkplain GlFrameProcessor frame processors} to apply to each frame. + * + *

        The {@linkplain GlFrameProcessor frame processors} are applied before any {@linkplain + * TransformationRequest.Builder#setScale(float, float) scale}, {@linkplain + * TransformationRequest.Builder#setRotationDegrees(float) rotation}, or {@linkplain + * TransformationRequest.Builder#setResolution(int) resolution} changes specified in the {@link + * #setTransformationRequest(TransformationRequest) TransformationRequest} but after {@linkplain + * TransformationRequest.Builder#setFlattenForSlowMotion(boolean) slow-motion flattening}. + * + * @param frameProcessors The {@linkplain GlFrameProcessor frame processors}. + * @return This builder. + */ + public Builder setFrameProcessors(List frameProcessors) { + this.frameProcessors = ImmutableList.copyOf(frameProcessors); + return this; + } + + /** + * Sets the {@link MediaSource.Factory} to be used to retrieve the inputs to transform. + * + *

        The default value is a {@link DefaultMediaSourceFactory} built with the context provided + * in {@linkplain #Builder(Context) the constructor}. * * @param mediaSourceFactory A {@link MediaSource.Factory}. * @return This builder. @@ -190,7 +219,9 @@ public final class Transformer { } /** - * Sets whether to remove the audio from the output. The default value is {@code false}. + * Sets whether to remove the audio from the output. + * + *

        The default value is {@code false}. * *

        The audio and video cannot both be removed because the output would not contain any * samples. @@ -204,7 +235,9 @@ public final class Transformer { } /** - * Sets whether to remove the video from the output. The default value is {@code false}. + * Sets whether to remove the video from the output. + * + *

        The default value is {@code false}. * *

        The audio and video cannot both be removed because the output would not contain any * samples. @@ -276,7 +309,7 @@ public final class Transformer { } /** - * Removes all {@link Transformer.Listener listeners}. + * Removes all {@linkplain Transformer.Listener listeners}. * *

        This is equivalent to {@link Transformer#removeAllListeners()}. * @@ -289,9 +322,10 @@ public final class Transformer { /** * Sets the {@link Looper} that must be used for all calls to the transformer and that is used - * to call listeners on. The default value is the Looper of the thread that this builder was - * created on, or if that thread does not have a Looper, the Looper of the application's main - * thread. + * to call listeners on. + * + *

        The default value is the Looper of the thread that this builder was created on, or if that + * thread does not have a Looper, the Looper of the application's main thread. * * @param looper A {@link Looper}. * @return This builder. @@ -303,8 +337,9 @@ public final class Transformer { } /** - * Sets the {@link Codec.EncoderFactory} that will be used by the transformer. The default value - * is {@link Codec.EncoderFactory#DEFAULT}. + * Sets the {@link Codec.EncoderFactory} that will be used by the transformer. + * + *

        The default value is {@link Codec.EncoderFactory#DEFAULT}. * * @param encoderFactory The {@link Codec.EncoderFactory} instance. * @return This builder. @@ -316,8 +351,10 @@ public final class Transformer { /** * Sets a provider for views to show diagnostic information (if available) during - * transformation. This is intended for debugging. The default value is {@link - * DebugViewProvider#NONE}, which doesn't show any debug info. + * transformation. + * + *

        This is intended for debugging. The default value is {@link DebugViewProvider#NONE}, which + * doesn't show any debug info. * *

        Not all transformations will result in debug views being populated. * @@ -330,8 +367,9 @@ public final class Transformer { } /** - * Sets the {@link Clock} that will be used by the transformer. The default value is {@link - * Clock#DEFAULT}. + * Sets the {@link Clock} that will be used by the transformer. + * + *

        The default value is {@link Clock#DEFAULT}. * * @param clock The {@link Clock} instance. * @return This builder. @@ -344,8 +382,9 @@ public final class Transformer { } /** - * Sets the factory for muxers that write the media container. The default value is a {@link - * FrameworkMuxer.Factory}. + * Sets the factory for muxers that write the media container. + * + *

        The default value is a {@link FrameworkMuxer.Factory}. * * @param muxerFactory A {@link Muxer.Factory}. * @return This builder. @@ -393,6 +432,7 @@ public final class Transformer { removeVideo, containerMimeType, transformationRequest, + frameProcessors, listeners, looper, clock, @@ -514,6 +554,7 @@ public final class Transformer { private final boolean removeVideo; private final String containerMimeType; private final TransformationRequest transformationRequest; + private final ImmutableList frameProcessors; private final Looper looper; private final Clock clock; private final Codec.EncoderFactory encoderFactory; @@ -534,6 +575,7 @@ public final class Transformer { boolean removeVideo, String containerMimeType, TransformationRequest transformationRequest, + ImmutableList frameProcessors, ListenerSet listeners, Looper looper, Clock clock, @@ -548,6 +590,7 @@ public final class Transformer { this.removeVideo = removeVideo; this.containerMimeType = containerMimeType; this.transformationRequest = transformationRequest; + this.frameProcessors = frameProcessors; this.listeners = listeners; this.looper = looper; this.clock = clock; @@ -596,7 +639,7 @@ public final class Transformer { } /** - * Removes all {@link Transformer.Listener listeners}. + * Removes all {@linkplain Transformer.Listener listeners}. * * @throws IllegalStateException If this method is called from the wrong thread. */ @@ -608,14 +651,14 @@ public final class Transformer { /** * Starts an asynchronous operation to transform the given {@link MediaItem}. * - *

        The transformation state is notified through the {@link Builder#addListener(Listener) + *

        The transformation state is notified through the {@linkplain Builder#addListener(Listener) * listener}. * *

        Concurrent transformations on the same Transformer object are not allowed. * *

        The output is an MP4 file. It can contain at most one video track and one audio track. Other - * track types are ignored. For adaptive bitrate {@link MediaSource media sources}, the highest - * bitrate video and audio streams are selected. + * track types are ignored. For adaptive bitrate {@linkplain MediaSource media sources}, the + * highest bitrate video and audio streams are selected. * * @param mediaItem The {@link MediaItem} to transform. * @param path The path to the output file. @@ -631,14 +674,14 @@ public final class Transformer { /** * Starts an asynchronous operation to transform the given {@link MediaItem}. * - *

        The transformation state is notified through the {@link Builder#addListener(Listener) + *

        The transformation state is notified through the {@linkplain Builder#addListener(Listener) * listener}. * *

        Concurrent transformations on the same Transformer object are not allowed. * *

        The output is an MP4 file. It can contain at most one video track and one audio track. Other - * track types are ignored. For adaptive bitrate {@link MediaSource media sources}, the highest - * bitrate video and audio streams are selected. + * track types are ignored. For adaptive bitrate {@linkplain MediaSource media sources}, the + * highest bitrate video and audio streams are selected. * * @param mediaItem The {@link MediaItem} to transform. * @param parcelFileDescriptor A readable and writable {@link ParcelFileDescriptor} of the output. @@ -661,7 +704,6 @@ public final class Transformer { if (player != null) { throw new IllegalStateException("There is already a transformation in progress."); } - MuxerWrapper muxerWrapper = new MuxerWrapper(muxer, muxerFactory, containerMimeType); this.muxerWrapper = muxerWrapper; DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); @@ -679,7 +721,7 @@ public final class Transformer { DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10) .build(); - player = + ExoPlayer.Builder playerBuilder = new ExoPlayer.Builder( context, new TransformerRenderersFactory( @@ -688,6 +730,7 @@ public final class Transformer { removeAudio, removeVideo, transformationRequest, + frameProcessors, encoderFactory, decoderFactory, new FallbackListener(mediaItem, listeners, transformationRequest), @@ -695,9 +738,15 @@ public final class Transformer { .setMediaSourceFactory(mediaSourceFactory) .setTrackSelector(trackSelector) .setLoadControl(loadControl) - .setLooper(looper) - .setClock(clock) - .build(); + .setLooper(looper); + if (clock != Clock.DEFAULT) { + // Transformer.Builder#setClock is also @VisibleForTesting, so if we're using a non-default + // clock we must be in a test context. + @SuppressWarnings("VisibleForTests") + ExoPlayer.Builder unusedForAnnotation = playerBuilder.setClock(clock); + } + + player = playerBuilder.build(); player.setMediaItem(mediaItem); player.addListener(new TransformerPlayerListener(mediaItem, muxerWrapper)); player.prepare(); @@ -717,7 +766,7 @@ public final class Transformer { * Returns the current {@link ProgressState} and updates {@code progressHolder} with the current * progress if it is {@link #PROGRESS_STATE_AVAILABLE available}. * - *

        After a transformation {@link Listener#onTransformationCompleted(MediaItem, + *

        After a transformation {@linkplain Listener#onTransformationCompleted(MediaItem, * TransformationResult) completes}, this method returns {@link * #PROGRESS_STATE_NO_TRANSFORMATION}. * @@ -793,6 +842,7 @@ public final class Transformer { private final boolean removeAudio; private final boolean removeVideo; private final TransformationRequest transformationRequest; + private final ImmutableList frameProcessors; private final Codec.EncoderFactory encoderFactory; private final Codec.DecoderFactory decoderFactory; private final FallbackListener fallbackListener; @@ -804,6 +854,7 @@ public final class Transformer { boolean removeAudio, boolean removeVideo, TransformationRequest transformationRequest, + ImmutableList frameProcessors, Codec.EncoderFactory encoderFactory, Codec.DecoderFactory decoderFactory, FallbackListener fallbackListener, @@ -813,6 +864,7 @@ public final class Transformer { this.removeAudio = removeAudio; this.removeVideo = removeVideo; this.transformationRequest = transformationRequest; + this.frameProcessors = frameProcessors; this.encoderFactory = encoderFactory; this.decoderFactory = decoderFactory; this.fallbackListener = fallbackListener; @@ -848,6 +900,7 @@ public final class Transformer { muxerWrapper, mediaClock, transformationRequest, + frameProcessors, encoderFactory, decoderFactory, fallbackListener, @@ -948,9 +1001,12 @@ public final class Transformer { } else { TransformationResult result = new TransformationResult.Builder() + .setDurationMs(muxerWrapper.getDurationMs()) .setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO)) .setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO)) + .setVideoFrameCount(muxerWrapper.getTrackSampleCount(C.TRACK_TYPE_VIDEO)) .build(); + listeners.queueEvent( /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onTransformationCompleted(mediaItem, result)); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java index 312001975f..29b39076cf 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java @@ -85,6 +85,9 @@ import androidx.media3.extractor.metadata.mp4.SlowMotionData; } private boolean shouldPassthrough(Format inputFormat) { + if (encoderFactory.audioNeedsEncoding()) { + return false; + } if (transformationRequest.audioMimeType != null && !transformationRequest.audioMimeType.equals(inputFormat.sampleMimeType)) { return false; diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java index 36b2bd41fa..d8789fc382 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java @@ -25,6 +25,7 @@ import androidx.media3.common.Format; import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.exoplayer.FormatHolder; import androidx.media3.exoplayer.source.SampleStream.ReadDataResult; +import com.google.common.collect.ImmutableList; import java.nio.ByteBuffer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -34,6 +35,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private static final String TAG = "TVideoRenderer"; private final Context context; + private final ImmutableList frameProcessors; private final Codec.EncoderFactory encoderFactory; private final Codec.DecoderFactory decoderFactory; private final Transformer.DebugViewProvider debugViewProvider; @@ -46,12 +48,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, TransformationRequest transformationRequest, + ImmutableList frameProcessors, Codec.EncoderFactory encoderFactory, Codec.DecoderFactory decoderFactory, FallbackListener fallbackListener, Transformer.DebugViewProvider debugViewProvider) { super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformationRequest, fallbackListener); this.context = context; + this.frameProcessors = frameProcessors; this.encoderFactory = encoderFactory; this.decoderFactory = decoderFactory; this.debugViewProvider = debugViewProvider; @@ -86,6 +90,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; context, inputFormat, transformationRequest, + frameProcessors, decoderFactory, encoderFactory, muxerWrapper.getSupportedSampleMimeTypes(getTrackType()), @@ -99,6 +104,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } private boolean shouldPassthrough(Format inputFormat) { + if (encoderFactory.videoNeedsEncoding()) { + return false; + } + if (transformationRequest.enableRequestSdrToneMapping) { + return false; + } if (transformationRequest.enableHdrEditing) { return false; } @@ -110,14 +121,20 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; && !muxerWrapper.supportsSampleMimeType(inputFormat.sampleMimeType)) { return false; } + if (transformationRequest.rotationDegrees != 0f) { + return false; + } + if (transformationRequest.scaleX != 1f) { + return false; + } + if (transformationRequest.scaleY != 1f) { + return false; + } if (transformationRequest.outputHeight != C.LENGTH_UNSET && transformationRequest.outputHeight != inputFormat.height) { return false; } - if (!transformationRequest.transformationMatrix.isIdentity()) { - // TODO(b/201293185, b/214010296): Move FrameProcessor transformationMatrix calculation / - // adjustments out of the VideoTranscodingSamplePipeline, so that we can skip transcoding when - // adjustments result in identity matrices. + if (!frameProcessors.isEmpty()) { return false; } return true; diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java new file mode 100644 index 0000000000..90db319e17 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java @@ -0,0 +1,286 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkArgument; +import static java.lang.annotation.ElementType.TYPE_USE; + +import android.annotation.SuppressLint; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.media3.common.Format; +import androidx.media3.common.util.UnstableApi; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Represents the video encoder settings. */ +@UnstableApi +public final class VideoEncoderSettings { + + /** A value for various fields to indicate that the field's value is unknown or not applicable. */ + public static final int NO_VALUE = Format.NO_VALUE; + /** The default encoding color profile. */ + public static final int DEFAULT_COLOR_PROFILE = + MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; + /** The default I-frame interval in seconds. */ + public static final float DEFAULT_I_FRAME_INTERVAL_SECONDS = 1.0f; + + /** A default {@link VideoEncoderSettings}. */ + public static final VideoEncoderSettings DEFAULT = new Builder().build(); + + /** + * The allowed values for {@code bitrateMode}, one of + * + *

          + *
        • Constant quality: {@link MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_CQ}. + *
        • Variable bitrate: {@link MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_VBR}. + *
        • Constant bitrate: {@link MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_CBR}. + *
        • Constant bitrate with frame drops: {@link + * MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_CBR_FD}, available from API31. + *
        + */ + @SuppressLint("InlinedApi") + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ, + MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR, + MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR, + MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR_FD + }) + public @interface BitrateMode {} + + /** Builds {@link VideoEncoderSettings} instances. */ + public static final class Builder { + private int bitrate; + private @BitrateMode int bitrateMode; + private int profile; + private int level; + private int colorProfile; + private float iFrameIntervalSeconds; + private int operatingRate; + private int priority; + + /** Creates a new instance. */ + public Builder() { + this.bitrate = NO_VALUE; + this.bitrateMode = MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR; + this.profile = NO_VALUE; + this.level = NO_VALUE; + this.colorProfile = DEFAULT_COLOR_PROFILE; + this.iFrameIntervalSeconds = DEFAULT_I_FRAME_INTERVAL_SECONDS; + this.operatingRate = NO_VALUE; + this.priority = NO_VALUE; + } + + private Builder(VideoEncoderSettings videoEncoderSettings) { + this.bitrate = videoEncoderSettings.bitrate; + this.bitrateMode = videoEncoderSettings.bitrateMode; + this.profile = videoEncoderSettings.profile; + this.level = videoEncoderSettings.level; + this.colorProfile = videoEncoderSettings.colorProfile; + this.iFrameIntervalSeconds = videoEncoderSettings.iFrameIntervalSeconds; + this.operatingRate = videoEncoderSettings.operatingRate; + this.priority = videoEncoderSettings.priority; + } + + /** + * Sets {@link VideoEncoderSettings#bitrate}. The default value is {@link #NO_VALUE}. + * + * @param bitrate The {@link VideoEncoderSettings#bitrate}. + * @return This builder. + */ + public Builder setBitrate(int bitrate) { + this.bitrate = bitrate; + return this; + } + + /** + * Sets {@link VideoEncoderSettings#bitrateMode}. The default value is {@code + * MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR}. + * + *

        Only {@link MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_VBR} and {@link + * MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_CBR} are allowed. + * + * @param bitrateMode The {@link VideoEncoderSettings#bitrateMode}. + * @return This builder. + */ + public Builder setBitrateMode(@BitrateMode int bitrateMode) { + checkArgument( + bitrateMode == MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR + || bitrateMode == MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR); + this.bitrateMode = bitrateMode; + return this; + } + + /** + * Sets {@link VideoEncoderSettings#profile} and {@link VideoEncoderSettings#level}. The default + * values are both {@link #NO_VALUE}. + * + *

        The value must be one of the values defined in {@link MediaCodecInfo.CodecProfileLevel}, + * or {@link #NO_VALUE}. + * + *

        Profile and level settings will be ignored when using {@link DefaultEncoderFactory} and + * encoding to H264. + * + * @param encodingProfile The {@link VideoEncoderSettings#profile}. + * @param encodingLevel The {@link VideoEncoderSettings#level}. + * @return This builder. + */ + public Builder setEncodingProfileLevel(int encodingProfile, int encodingLevel) { + this.profile = encodingProfile; + this.level = encodingLevel; + return this; + } + + /** + * Sets {@link VideoEncoderSettings#colorProfile}. The default value is {@link + * #DEFAULT_COLOR_PROFILE}. + * + *

        The value must be one of the {@code COLOR_*} constants defined in {@link + * MediaCodecInfo.CodecCapabilities}. + * + * @param colorProfile The {@link VideoEncoderSettings#colorProfile}. + * @return This builder. + */ + public Builder setColorProfile(int colorProfile) { + this.colorProfile = colorProfile; + return this; + } + + /** + * Sets {@link VideoEncoderSettings#iFrameIntervalSeconds}. The default value is {@link + * #DEFAULT_I_FRAME_INTERVAL_SECONDS}. + * + * @param iFrameIntervalSeconds The {@link VideoEncoderSettings#iFrameIntervalSeconds}. + * @return This builder. + */ + public Builder setiFrameIntervalSeconds(float iFrameIntervalSeconds) { + this.iFrameIntervalSeconds = iFrameIntervalSeconds; + return this; + } + + /** + * Sets encoding operating rate and priority. The default values are {@link #NO_VALUE}. + * + * @param operatingRate The {@link MediaFormat#KEY_OPERATING_RATE operating rate}. + * @param priority The {@link MediaFormat#KEY_PRIORITY priority}. + * @return This builder. + */ + @VisibleForTesting + public Builder setEncoderPerformanceParameters(int operatingRate, int priority) { + this.operatingRate = operatingRate; + this.priority = priority; + return this; + } + + /** Builds the instance. */ + public VideoEncoderSettings build() { + return new VideoEncoderSettings( + bitrate, + bitrateMode, + profile, + level, + colorProfile, + iFrameIntervalSeconds, + operatingRate, + priority); + } + } + + /** The encoding bitrate. */ + public final int bitrate; + /** One of {@linkplain BitrateMode the allowed modes}. */ + public final @BitrateMode int bitrateMode; + /** The encoding profile. */ + public final int profile; + /** The encoding level. */ + public final int level; + /** The encoding color profile. */ + public final int colorProfile; + /** The encoding I-Frame interval in seconds. */ + public final float iFrameIntervalSeconds; + /** The encoder {@link MediaFormat#KEY_OPERATING_RATE operating rate}. */ + public final int operatingRate; + /** The encoder {@link MediaFormat#KEY_PRIORITY priority}. */ + public final int priority; + + private VideoEncoderSettings( + int bitrate, + int bitrateMode, + int profile, + int level, + int colorProfile, + float iFrameIntervalSeconds, + int operatingRate, + int priority) { + this.bitrate = bitrate; + this.bitrateMode = bitrateMode; + this.profile = profile; + this.level = level; + this.colorProfile = colorProfile; + this.iFrameIntervalSeconds = iFrameIntervalSeconds; + this.operatingRate = operatingRate; + this.priority = priority; + } + + /** + * Returns a {@link VideoEncoderSettings.Builder} initialized with the values of this instance. + */ + public Builder buildUpon() { + return new Builder(this); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VideoEncoderSettings)) { + return false; + } + VideoEncoderSettings that = (VideoEncoderSettings) o; + return bitrate == that.bitrate + && bitrateMode == that.bitrateMode + && profile == that.profile + && level == that.level + && colorProfile == that.colorProfile + && iFrameIntervalSeconds == that.iFrameIntervalSeconds + && operatingRate == that.operatingRate + && priority == that.priority; + } + + @Override + public int hashCode() { + int result = 7; + result = 31 * result + bitrate; + result = 31 * result + bitrateMode; + result = 31 * result + profile; + result = 31 * result + level; + result = 31 * result + colorProfile; + result = 31 * result + Float.floatToIntBits(iFrameIntervalSeconds); + result = 31 * result + operatingRate; + result = 31 * result + priority; + return result; + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java index 6362378472..c0dc977c8e 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java @@ -17,20 +17,15 @@ package androidx.media3.transformer; import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.common.util.Util.SDK_INT; -import static java.lang.Math.max; -import static java.lang.Math.min; import android.content.Context; -import android.graphics.Matrix; import android.media.MediaCodec; -import android.media.MediaFormat; +import android.util.Size; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.util.Util; import androidx.media3.decoder.DecoderInputBuffer; +import com.google.common.collect.ImmutableList; import java.util.List; import org.checkerframework.dataflow.qual.Pure; @@ -39,21 +34,25 @@ import org.checkerframework.dataflow.qual.Pure; */ /* package */ final class VideoTranscodingSamplePipeline implements SamplePipeline { + private static final int FRAME_COUNT_UNLIMITED = -1; + private final int outputRotationDegrees; private final DecoderInputBuffer decoderInputBuffer; private final Codec decoder; + private final int maxPendingFrameCount; - @Nullable private final FrameEditor frameEditor; + private final FrameProcessorChain frameProcessorChain; private final Codec encoder; private final DecoderInputBuffer encoderOutputBuffer; - private boolean waitingForFrameEditorInput; + private boolean signaledEndOfStreamToEncoder; public VideoTranscodingSamplePipeline( Context context, Format inputFormat, TransformationRequest transformationRequest, + ImmutableList frameProcessors, Codec.DecoderFactory decoderFactory, Codec.EncoderFactory encoderFactory, List allowedOutputMimeTypes, @@ -70,114 +69,66 @@ import org.checkerframework.dataflow.qual.Pure; (inputFormat.rotationDegrees % 180 == 0) ? inputFormat.width : inputFormat.height; int decodedHeight = (inputFormat.rotationDegrees % 180 == 0) ? inputFormat.height : inputFormat.width; - float decodedAspectRatio = (float) decodedWidth / decodedHeight; - Matrix transformationMatrix = new Matrix(transformationRequest.transformationMatrix); - - int outputWidth = decodedWidth; - int outputHeight = decodedHeight; - if (!transformationMatrix.isIdentity()) { - // Scale frames by decodedAspectRatio, to account for FrameEditor's normalized device - // coordinates (NDC) (a square from -1 to 1 for both x and y) and preserve rectangular display - // of input pixels during transformations (ex. rotations). With scaling, transformationMatrix - // operations operate on a rectangle for x from -decodedAspectRatio to decodedAspectRatio, and - // y from -1 to 1. - transformationMatrix.preScale(/* sx= */ decodedAspectRatio, /* sy= */ 1f); - transformationMatrix.postScale(/* sx= */ 1f / decodedAspectRatio, /* sy= */ 1f); - - float[][] transformOnNdcPoints = {{-1, -1, 0, 1}, {-1, 1, 0, 1}, {1, -1, 0, 1}, {1, 1, 0, 1}}; - float xMin = Float.MAX_VALUE; - float xMax = Float.MIN_VALUE; - float yMin = Float.MAX_VALUE; - float yMax = Float.MIN_VALUE; - for (float[] transformOnNdcPoint : transformOnNdcPoints) { - transformationMatrix.mapPoints(transformOnNdcPoint); - xMin = min(xMin, transformOnNdcPoint[0]); - xMax = max(xMax, transformOnNdcPoint[0]); - yMin = min(yMin, transformOnNdcPoint[1]); - yMax = max(yMax, transformOnNdcPoint[1]); - } - - float xCenter = (xMax + xMin) / 2f; - float yCenter = (yMax + yMin) / 2f; - transformationMatrix.postTranslate(-xCenter, -yCenter); - - float ndcWidthAndHeight = 2f; // Length from -1 to 1. - float xScale = (xMax - xMin) / ndcWidthAndHeight; - float yScale = (yMax - yMin) / ndcWidthAndHeight; - transformationMatrix.postScale(1f / xScale, 1f / yScale); - outputWidth = Math.round(decodedWidth * xScale); - outputHeight = Math.round(decodedHeight * yScale); - } - // Scale width and height to desired transformationRequest.outputHeight, preserving - // aspect ratio. - if (transformationRequest.outputHeight != C.LENGTH_UNSET - && transformationRequest.outputHeight != outputHeight) { - outputWidth = - Math.round((float) transformationRequest.outputHeight * outputWidth / outputHeight); - outputHeight = transformationRequest.outputHeight; - } - - // Encoders commonly support higher maximum widths than maximum heights. Rotate the decoded - // video before encoding, so the encoded video's width >= height, and set outputRotationDegrees - // to ensure the video is displayed in the correct orientation. - int requestedEncoderWidth; - int requestedEncoderHeight; - boolean swapEncodingDimensions = outputHeight > outputWidth; - if (swapEncodingDimensions) { - outputRotationDegrees = 90; - requestedEncoderWidth = outputHeight; - requestedEncoderHeight = outputWidth; - // TODO(b/201293185): After fragment shader transformations are implemented, put - // postRotate in a later vertex shader. - transformationMatrix.postRotate(outputRotationDegrees); - } else { - outputRotationDegrees = 0; - requestedEncoderWidth = outputWidth; - requestedEncoderHeight = outputHeight; - } + // TODO(b/213190310): Don't create a ScaleToFitFrameProcessor if scale and rotation are unset. + ScaleToFitFrameProcessor scaleToFitFrameProcessor = + new ScaleToFitFrameProcessor.Builder(context) + .setScale(transformationRequest.scaleX, transformationRequest.scaleY) + .setRotationDegrees(transformationRequest.rotationDegrees) + .build(); + PresentationFrameProcessor presentationFrameProcessor = + new PresentationFrameProcessor.Builder(context) + .setResolution(transformationRequest.outputHeight) + .build(); + frameProcessorChain = + FrameProcessorChain.create( + context, + inputFormat.pixelWidthHeightRatio, + /* inputWidth= */ decodedWidth, + /* inputHeight= */ decodedHeight, + new ImmutableList.Builder() + .addAll(frameProcessors) + .add(scaleToFitFrameProcessor) + .add(presentationFrameProcessor) + .build(), + transformationRequest.enableHdrEditing); + Size requestedEncoderSize = frameProcessorChain.getOutputSize(); + outputRotationDegrees = presentationFrameProcessor.getOutputRotationDegrees(); Format requestedEncoderFormat = new Format.Builder() - .setWidth(requestedEncoderWidth) - .setHeight(requestedEncoderHeight) + .setWidth(requestedEncoderSize.getWidth()) + .setHeight(requestedEncoderSize.getHeight()) .setRotationDegrees(0) + .setFrameRate(inputFormat.frameRate) .setSampleMimeType( transformationRequest.videoMimeType != null ? transformationRequest.videoMimeType : inputFormat.sampleMimeType) .build(); + encoder = encoderFactory.createForVideoEncoding(requestedEncoderFormat, allowedOutputMimeTypes); Format encoderSupportedFormat = encoder.getConfigurationFormat(); fallbackListener.onTransformationRequestFinalized( createFallbackTransformationRequest( transformationRequest, - /* resolutionIsHeight= */ !swapEncodingDimensions, + /* hasOutputFormatRotation= */ outputRotationDegrees == 0, requestedEncoderFormat, encoderSupportedFormat)); - if (transformationRequest.enableHdrEditing - || inputFormat.height != encoderSupportedFormat.height - || inputFormat.width != encoderSupportedFormat.width - || !transformationMatrix.isIdentity()) { - frameEditor = - FrameEditor.create( - context, - encoderSupportedFormat.width, - encoderSupportedFormat.height, - inputFormat.pixelWidthHeightRatio, - transformationMatrix, - /* outputSurface= */ encoder.getInputSurface(), - transformationRequest.enableHdrEditing, - debugViewProvider); - } else { - frameEditor = null; - } + frameProcessorChain.setOutputSurface( + /* outputSurface= */ encoder.getInputSurface(), + /* outputWidth= */ encoderSupportedFormat.width, + /* outputHeight= */ encoderSupportedFormat.height, + debugViewProvider.getDebugPreviewSurfaceView( + encoderSupportedFormat.width, encoderSupportedFormat.height)); decoder = decoderFactory.createForVideoDecoding( inputFormat, - frameEditor == null ? encoder.getInputSurface() : frameEditor.getInputSurface()); + frameProcessorChain.getInputSurface(), + transformationRequest.enableRequestSdrToneMapping); + maxPendingFrameCount = getMaxPendingFrameCount(); } @Override @@ -193,79 +144,27 @@ import org.checkerframework.dataflow.qual.Pure; @Override public boolean processData() throws TransformationException { - if (hasProcessedAllInputData()) { + frameProcessorChain.getAndRethrowBackgroundExceptions(); + if (frameProcessorChain.isEnded()) { + if (!signaledEndOfStreamToEncoder) { + encoder.signalEndOfInputStream(); + signaledEndOfStreamToEncoder = true; + } + return false; + } + if (decoder.isEnded()) { return false; } - if (SDK_INT >= 29) { - return processDataV29(); - } else { - return processDataDefault(); - } - } - - /** - * Processes input data from API 29. - * - *

        In this method the decoder could decode multiple frames in one invocation; as compared to - * {@link #processDataDefault()}, in which one frame is decoded in each invocation. Consequently, - * if {@link FrameEditor} processes frames slower than the decoder, decoded frames are queued up - * in the decoder's output surface. - * - *

        Prior to API 29, decoders may drop frames to keep their output surface from growing out of - * bound; while after API 29, the {@link MediaFormat#KEY_ALLOW_FRAME_DROP} key prevents frame - * dropping even when the surface is full. As dropping random frames is not acceptable in {@code - * Transformer}, using this method requires API level 29 or higher. - */ - @RequiresApi(29) - private boolean processDataV29() throws TransformationException { - if (frameEditor != null) { - // Processes as many frames as possible. FrameEditor's output surface will block when it's - // full, so there will be no frame drop and the surface will not grow out of bound. - while (frameEditor.canProcessData()) { - frameEditor.processData(); - } - } - - while (decoder.getOutputBufferInfo() != null) { - if (frameEditor != null) { - frameEditor.registerInputFrame(); - } - decoder.releaseOutputBuffer(/* render= */ true); + boolean processedData = false; + while (maybeProcessDecoderOutput()) { + processedData = true; } if (decoder.isEnded()) { - signalEndOfInputStream(); + frameProcessorChain.signalEndOfInputStream(); } - - return frameEditor != null && frameEditor.canProcessData(); - } - - /** Processes input data. */ - private boolean processDataDefault() throws TransformationException { - if (frameEditor != null) { - if (frameEditor.canProcessData()) { - waitingForFrameEditorInput = false; - frameEditor.processData(); - return true; - } - if (waitingForFrameEditorInput) { - return false; - } - } - - boolean decoderHasOutputBuffer = decoder.getOutputBufferInfo() != null; - if (decoderHasOutputBuffer) { - if (frameEditor != null) { - frameEditor.registerInputFrame(); - waitingForFrameEditorInput = true; - } - decoder.releaseOutputBuffer(/* render= */ true); - } - if (decoder.isEnded()) { - signalEndOfInputStream(); - return false; - } - return decoderHasOutputBuffer && !waitingForFrameEditorInput; + // If the decoder produced output, signal that it may be possible to process data again. + return processedData; } @Override @@ -302,43 +201,86 @@ import org.checkerframework.dataflow.qual.Pure; @Override public void release() { - if (frameEditor != null) { - frameEditor.release(); - } + frameProcessorChain.release(); decoder.release(); encoder.release(); } + /** + * Creates a fallback transformation request to execute, based on device-specific support. + * + * @param transformationRequest The requested transformation. + * @param hasOutputFormatRotation Whether the input video will be rotated to landscape during + * processing, with {@link Format#rotationDegrees} of 90 added to the output format. + * @param requestedFormat The requested format. + * @param supportedFormat A format supported by the device. + */ @Pure private static TransformationRequest createFallbackTransformationRequest( TransformationRequest transformationRequest, - boolean resolutionIsHeight, + boolean hasOutputFormatRotation, Format requestedFormat, - Format actualFormat) { + Format supportedFormat) { // TODO(b/210591626): Also update bitrate etc. once encoder configuration and fallback are // implemented. - if (Util.areEqual(requestedFormat.sampleMimeType, actualFormat.sampleMimeType) - && ((!resolutionIsHeight && requestedFormat.width == actualFormat.width) - || (resolutionIsHeight && requestedFormat.height == actualFormat.height))) { + if (Util.areEqual(requestedFormat.sampleMimeType, supportedFormat.sampleMimeType) + && (hasOutputFormatRotation + ? requestedFormat.width == supportedFormat.width + : requestedFormat.height == supportedFormat.height)) { return transformationRequest; } return transformationRequest .buildUpon() - .setVideoMimeType(actualFormat.sampleMimeType) - .setResolution(resolutionIsHeight ? requestedFormat.height : requestedFormat.width) + .setVideoMimeType(supportedFormat.sampleMimeType) + .setResolution(hasOutputFormatRotation ? requestedFormat.width : requestedFormat.height) .build(); } - private boolean hasProcessedAllInputData() { - return decoder.isEnded() && (frameEditor == null || frameEditor.isEnded()); + /** + * Feeds at most one decoder output frame to the next step of the pipeline. + * + * @return Whether a frame was processed. + * @throws TransformationException If a problem occurs while processing the frame. + */ + private boolean maybeProcessDecoderOutput() throws TransformationException { + if (decoder.getOutputBufferInfo() == null) { + return false; + } + + if (maxPendingFrameCount != FRAME_COUNT_UNLIMITED + && frameProcessorChain.getPendingFrameCount() == maxPendingFrameCount) { + return false; + } + + frameProcessorChain.registerInputFrame(); + decoder.releaseOutputBuffer(/* render= */ true); + return true; } - private void signalEndOfInputStream() throws TransformationException { - if (frameEditor != null) { - frameEditor.signalEndOfInputStream(); + /** + * Returns the maximum number of frames that may be pending in the output {@link + * FrameProcessorChain} at a time, or {@link #FRAME_COUNT_UNLIMITED} if it's not necessary to + * enforce a limit. + */ + private static int getMaxPendingFrameCount() { + if (Util.SDK_INT < 29) { + // Prior to API 29, decoders may drop frames to keep their output surface from growing out of + // bounds, while from API 29, the {@link MediaFormat#KEY_ALLOW_FRAME_DROP} key prevents frame + // dropping even when the surface is full. We never want frame dropping so allow a maximum of + // one frame to be pending at a time. + // TODO(b/226330223): Investigate increasing this limit. + return 1; } - if (frameEditor == null || frameEditor.isEnded()) { - encoder.signalEndOfInputStream(); + if (Util.SDK_INT < 31 + && ("OnePlus".equals(Util.MANUFACTURER) || "samsung".equals(Util.MANUFACTURER))) { + // Some OMX decoders don't correctly track their number of output buffers available, and get + // stuck if too many frames are rendered without being processed, so we limit the number of + // pending frames to avoid getting stuck. This value is experimentally determined. See also + // b/213455700. + return 10; } + // Otherwise don't limit the number of frames that can be pending at a time, to maximize + // throughput. + return FRAME_COUNT_UNLIMITED; } } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java new file mode 100644 index 0000000000..c7e9f9b2b1 --- /dev/null +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static org.junit.Assert.assertThrows; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link AdvancedFrameProcessor}. + * + *

        See {@code AdvancedFrameProcessorPixelTest} for pixel tests testing {@link + * AdvancedFrameProcessor} given a transformation matrix. + */ +@RunWith(AndroidJUnit4.class) +public final class AdvancedFrameProcessorTest { + + @Test + public void construct_withInvalidMatrixSize_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> new AdvancedFrameProcessor(getApplicationContext(), new float[4])); + } + + @Test + public void construct_withValidMatrixSize_completesSuccessfully() { + new AdvancedFrameProcessor(getApplicationContext(), new float[16]); + } +} diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java new file mode 100644 index 0000000000..6fd88d043b --- /dev/null +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.transformer; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.MediaCodecInfoBuilder; +import org.robolectric.shadows.ShadowMediaCodecList; + +/** Unit test for {@link DefaultEncoderFactory}. */ +@RunWith(AndroidJUnit4.class) +public class DefaultEncoderFactoryTest { + + @Before + public void setUp() { + MediaFormat avcFormat = new MediaFormat(); + avcFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); + MediaCodecInfo.CodecProfileLevel profileLevel = new MediaCodecInfo.CodecProfileLevel(); + profileLevel.profile = MediaCodecInfo.CodecProfileLevel.AVCProfileHigh; + // Using Level4 gives us 8192 16x16 blocks. If using width 1920 uses 120 blocks, 8192 / 120 = 68 + // blocks will be left for encoding height 1088. + profileLevel.level = MediaCodecInfo.CodecProfileLevel.AVCLevel4; + + ShadowMediaCodecList.addCodec( + MediaCodecInfoBuilder.newBuilder() + .setName("test.transformer.avc.encoder") + .setIsEncoder(true) + .setCapabilities( + MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder() + .setMediaFormat(avcFormat) + .setIsEncoder(true) + .setColorFormats( + new int[] {MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible}) + .setProfileLevels(new MediaCodecInfo.CodecProfileLevel[] {profileLevel}) + .build()) + .build()); + } + + @Test + public void createForVideoEncoding_withFallbackOnAndSupportedInputFormat_configuresEncoder() + throws Exception { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); + Format actualVideoFormat = + new DefaultEncoderFactory() + .createForVideoEncoding( + requestedVideoFormat, + /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H264)) + .getConfigurationFormat(); + + assertThat(actualVideoFormat.sampleMimeType).isEqualTo(MimeTypes.VIDEO_H264); + assertThat(actualVideoFormat.width).isEqualTo(1920); + assertThat(actualVideoFormat.height).isEqualTo(1080); + // 1920 * 1080 * 30 * 0.1 + assertThat(actualVideoFormat.averageBitrate).isEqualTo(6_220_800); + } + + @Test + public void createForVideoEncoding_withFallbackOnAndUnsupportedMimeType_configuresEncoder() + throws Exception { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H265, 1920, 1080, 30); + Format actualVideoFormat = + new DefaultEncoderFactory() + .createForVideoEncoding( + requestedVideoFormat, + /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H264)) + .getConfigurationFormat(); + + assertThat(actualVideoFormat.sampleMimeType).isEqualTo(MimeTypes.VIDEO_H264); + assertThat(actualVideoFormat.width).isEqualTo(1920); + assertThat(actualVideoFormat.height).isEqualTo(1080); + // 1920 * 1080 * 30 * 0.1 + assertThat(actualVideoFormat.averageBitrate).isEqualTo(6_220_800); + } + + @Test + public void createForVideoEncoding_withFallbackOnAndUnsupportedResolution_configuresEncoder() + throws Exception { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 3840, 2160, 60); + Format actualVideoFormat = + new DefaultEncoderFactory() + .createForVideoEncoding( + requestedVideoFormat, + /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H264)) + .getConfigurationFormat(); + + assertThat(actualVideoFormat.width).isEqualTo(1920); + assertThat(actualVideoFormat.height).isEqualTo(1080); + } + + @Test + public void createForVideoEncoding_withNoSupportedEncoder_throws() { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); + + TransformationException exception = + assertThrows( + TransformationException.class, + () -> + new DefaultEncoderFactory() + .createForVideoEncoding( + requestedVideoFormat, + /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H265))); + + assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(exception.errorCode) + .isEqualTo(TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED); + } + + @Test + public void createForVideoEncoding_withNoAvailableEncoderFromEncoderSelector_throws() { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); + assertThrows( + TransformationException.class, + () -> + new DefaultEncoderFactory( + /* videoEncoderSelector= */ mimeType -> ImmutableList.of(), + /* enableFallback= */ true) + .createForVideoEncoding( + requestedVideoFormat, + /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H264))); + } + + private static Format createVideoFormat(String mimeType, int width, int height, int frameRate) { + return new Format.Builder() + .setWidth(width) + .setHeight(height) + .setFrameRate(frameRate) + .setRotationDegrees(0) + .setSampleMimeType(mimeType) + .build(); + } +} diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/EncoderUtilTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/EncoderUtilTest.java new file mode 100644 index 0000000000..4e6824f439 --- /dev/null +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/EncoderUtilTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.transformer; + +import static com.google.common.truth.Truth.assertThat; + +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.util.Size; +import androidx.annotation.Nullable; +import androidx.media3.common.MimeTypes; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.MediaCodecInfoBuilder; +import org.robolectric.shadows.ShadowMediaCodecList; + +/** Unit test for {@link EncoderUtil}. */ +@RunWith(AndroidJUnit4.class) +public class EncoderUtilTest { + private static final String MIME_TYPE = MimeTypes.VIDEO_H264; + + @Before + public void setUp() { + MediaFormat avcFormat = new MediaFormat(); + avcFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); + MediaCodecInfo.CodecProfileLevel profileLevel = new MediaCodecInfo.CodecProfileLevel(); + profileLevel.profile = MediaCodecInfo.CodecProfileLevel.AVCProfileHigh; + // Using Level4 gives us 8192 16x16 blocks. If using width 1920 uses 120 blocks, 8192 / 120 = 68 + // blocks will be left for encoding height 1088. + profileLevel.level = MediaCodecInfo.CodecProfileLevel.AVCLevel4; + + ShadowMediaCodecList.addCodec( + MediaCodecInfoBuilder.newBuilder() + .setName("test.transformer.avc.encoder") + .setIsEncoder(true) + .setCapabilities( + MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder() + .setMediaFormat(avcFormat) + .setIsEncoder(true) + .setColorFormats( + new int[] {MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible}) + .setProfileLevels(new MediaCodecInfo.CodecProfileLevel[] {profileLevel}) + .build()) + .build()); + } + + @Test + public void getSupportedResolution_withSupportedResolution_succeeds() { + ImmutableList supportedEncoders = EncoderUtil.getSupportedEncoders(MIME_TYPE); + MediaCodecInfo encoderInfo = supportedEncoders.get(0); + + @Nullable + Size closestSupportedResolution = + EncoderUtil.getSupportedResolution(encoderInfo, MIME_TYPE, 1920, 1080); + + assertThat(closestSupportedResolution).isNotNull(); + assertThat(closestSupportedResolution.getWidth()).isEqualTo(1920); + assertThat(closestSupportedResolution.getHeight()).isEqualTo(1080); + } + + @Test + public void getSupportedResolution_withUnalignedSize_findsMostCloselySupportedResolution() { + ImmutableList supportedEncoders = EncoderUtil.getSupportedEncoders(MIME_TYPE); + MediaCodecInfo encoderInfo = supportedEncoders.get(0); + + @Nullable + Size closestSupportedResolution = + EncoderUtil.getSupportedResolution(encoderInfo, MIME_TYPE, 1919, 1081); + + assertThat(closestSupportedResolution).isNotNull(); + assertThat(closestSupportedResolution.getWidth()).isEqualTo(1920); + assertThat(closestSupportedResolution.getHeight()).isEqualTo(1080); + } + + @Test + public void getSupportedResolution_withWidthTooBig_findsTwoThirdsOfTheOriginalSize() { + ImmutableList supportedEncoders = EncoderUtil.getSupportedEncoders(MIME_TYPE); + MediaCodecInfo encoderInfo = supportedEncoders.get(0); + + @Nullable + Size closestSupportedResolution = + EncoderUtil.getSupportedResolution(encoderInfo, MIME_TYPE, 1920, 1920); + + assertThat(closestSupportedResolution).isNotNull(); + assertThat(closestSupportedResolution.getWidth()).isEqualTo(1440); + assertThat(closestSupportedResolution.getHeight()).isEqualTo(1440); + } + + @Test + public void getSupportedResolution_withWidthTooBig2_findsHalfOfTheOriginalSize() { + ImmutableList supportedEncoders = EncoderUtil.getSupportedEncoders(MIME_TYPE); + MediaCodecInfo encoderInfo = supportedEncoders.get(0); + + @Nullable + Size closestSupportedResolution = + EncoderUtil.getSupportedResolution(encoderInfo, MIME_TYPE, 3840, 2160); + + assertThat(closestSupportedResolution).isNotNull(); + assertThat(closestSupportedResolution.getWidth()).isEqualTo(1920); + assertThat(closestSupportedResolution.getHeight()).isEqualTo(1080); + } +} diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationFrameProcessorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationFrameProcessorTest.java new file mode 100644 index 0000000000..8157208f62 --- /dev/null +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationFrameProcessorTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.util.Size; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link PresentationFrameProcessor}. + * + *

        See {@code AdvancedFrameProcessorPixelTest} for pixel tests testing {@link + * AdvancedFrameProcessor} given a transformation matrix. + */ +@RunWith(AndroidJUnit4.class) +public final class PresentationFrameProcessorTest { + @Test + public void getOutputSize_noEditsLandscape_leavesFramesUnchanged() { + int inputWidth = 200; + int inputHeight = 150; + PresentationFrameProcessor presentationFrameProcessor = + new PresentationFrameProcessor.Builder(getApplicationContext()).build(); + + presentationFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + Size outputSize = presentationFrameProcessor.getOutputSize(); + + assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0); + assertThat(outputSize.getWidth()).isEqualTo(inputWidth); + assertThat(outputSize.getHeight()).isEqualTo(inputHeight); + } + + @Test + public void getOutputSize_noEditsSquare_leavesFramesUnchanged() { + int inputWidth = 150; + int inputHeight = 150; + PresentationFrameProcessor presentationFrameProcessor = + new PresentationFrameProcessor.Builder(getApplicationContext()).build(); + + presentationFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + Size outputSize = presentationFrameProcessor.getOutputSize(); + + assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0); + assertThat(outputSize.getWidth()).isEqualTo(inputWidth); + assertThat(outputSize.getHeight()).isEqualTo(inputHeight); + } + + @Test + public void getOutputSize_noEditsPortrait_flipsOrientation() { + int inputWidth = 150; + int inputHeight = 200; + PresentationFrameProcessor presentationFrameProcessor = + new PresentationFrameProcessor.Builder(getApplicationContext()).build(); + + presentationFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + Size outputSize = presentationFrameProcessor.getOutputSize(); + + assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(90); + assertThat(outputSize.getWidth()).isEqualTo(inputHeight); + assertThat(outputSize.getHeight()).isEqualTo(inputWidth); + } + + @Test + public void getOutputSize_setResolution_changesDimensions() { + int inputWidth = 200; + int inputHeight = 150; + int requestedHeight = 300; + PresentationFrameProcessor presentationFrameProcessor = + new PresentationFrameProcessor.Builder(getApplicationContext()) + .setResolution(requestedHeight) + .build(); + + presentationFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + Size outputSize = presentationFrameProcessor.getOutputSize(); + + assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0); + assertThat(outputSize.getWidth()).isEqualTo(requestedHeight * inputWidth / inputHeight); + assertThat(outputSize.getHeight()).isEqualTo(requestedHeight); + } + + @Test + public void getOutputRotationDegreesBeforeConfigure_throwsIllegalStateException() { + PresentationFrameProcessor presentationFrameProcessor = + new PresentationFrameProcessor.Builder(getApplicationContext()).build(); + + // configureOutputSize not called before getOutputRotationDegrees. + assertThrows(IllegalStateException.class, presentationFrameProcessor::getOutputRotationDegrees); + } +} diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/ScaleToFitFrameProcessorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/ScaleToFitFrameProcessorTest.java new file mode 100644 index 0000000000..989a73a1b7 --- /dev/null +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/ScaleToFitFrameProcessorTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; + +import android.util.Size; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link ScaleToFitFrameProcessor}. + * + *

        See {@code AdvancedFrameProcessorPixelTest} for pixel tests testing {@link + * AdvancedFrameProcessor} given a transformation matrix. + */ +@RunWith(AndroidJUnit4.class) +public final class ScaleToFitFrameProcessorTest { + + @Test + public void getOutputSize_noEdits_leavesFramesUnchanged() { + int inputWidth = 200; + int inputHeight = 150; + ScaleToFitFrameProcessor scaleToFitFrameProcessor = + new ScaleToFitFrameProcessor.Builder(getApplicationContext()).build(); + + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); + + assertThat(outputSize.getWidth()).isEqualTo(inputWidth); + assertThat(outputSize.getHeight()).isEqualTo(inputHeight); + } + + @Test + public void getOutputSize_scaleNarrow_decreasesWidth() { + int inputWidth = 200; + int inputHeight = 150; + ScaleToFitFrameProcessor scaleToFitFrameProcessor = + new ScaleToFitFrameProcessor.Builder(getApplicationContext()) + .setScale(/* scaleX= */ .5f, /* scaleY= */ 1f) + .build(); + + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); + + assertThat(outputSize.getWidth()).isEqualTo(Math.round(inputWidth * .5f)); + assertThat(outputSize.getHeight()).isEqualTo(inputHeight); + } + + @Test + public void getOutputSize_scaleWide_increasesWidth() { + int inputWidth = 200; + int inputHeight = 150; + ScaleToFitFrameProcessor scaleToFitFrameProcessor = + new ScaleToFitFrameProcessor.Builder(getApplicationContext()) + .setScale(/* scaleX= */ 2f, /* scaleY= */ 1f) + .build(); + + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); + + assertThat(outputSize.getWidth()).isEqualTo(inputWidth * 2); + assertThat(outputSize.getHeight()).isEqualTo(inputHeight); + } + + @Test + public void getOutputSize_scaleTall_increasesHeight() { + int inputWidth = 200; + int inputHeight = 150; + ScaleToFitFrameProcessor scaleToFitFrameProcessor = + new ScaleToFitFrameProcessor.Builder(getApplicationContext()) + .setScale(/* scaleX= */ 1f, /* scaleY= */ 2f) + .build(); + + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); + + assertThat(outputSize.getWidth()).isEqualTo(inputWidth); + assertThat(outputSize.getHeight()).isEqualTo(inputHeight * 2); + } + + @Test + public void getOutputSize_rotate90_swapsDimensions() { + int inputWidth = 200; + int inputHeight = 150; + ScaleToFitFrameProcessor scaleToFitFrameProcessor = + new ScaleToFitFrameProcessor.Builder(getApplicationContext()) + .setRotationDegrees(90) + .build(); + + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); + + assertThat(outputSize.getWidth()).isEqualTo(inputHeight); + assertThat(outputSize.getHeight()).isEqualTo(inputWidth); + } + + @Test + public void getOutputSize_rotate45_changesDimensions() { + int inputWidth = 200; + int inputHeight = 150; + ScaleToFitFrameProcessor scaleToFitFrameProcessor = + new ScaleToFitFrameProcessor.Builder(getApplicationContext()) + .setRotationDegrees(45) + .build(); + long expectedOutputWidthHeight = 247; + + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); + + assertThat(outputSize.getWidth()).isEqualTo(expectedOutputWidthHeight); + assertThat(outputSize.getHeight()).isEqualTo(expectedOutputWidthHeight); + } +} diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformationRequestTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformationRequestTest.java index 848e5a0c73..ff90ef809b 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformationRequestTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformationRequestTest.java @@ -18,7 +18,6 @@ package androidx.media3.transformer; import static com.google.common.truth.Truth.assertThat; -import android.graphics.Matrix; import androidx.media3.common.MimeTypes; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; @@ -35,15 +34,12 @@ public class TransformationRequestTest { } private static TransformationRequest createTestTransformationRequest() { - Matrix transformationMatrix = new Matrix(); - transformationMatrix.preRotate(36); - transformationMatrix.postTranslate((float) 0.5, (float) -0.2); - return new TransformationRequest.Builder() .setFlattenForSlowMotion(true) .setAudioMimeType(MimeTypes.AUDIO_AAC) .setVideoMimeType(MimeTypes.VIDEO_H264) - .setTransformationMatrix(transformationMatrix) + .setRotationDegrees(45) + .setScale(/* scaleX= */ 1f, /* scaleY= */ 2f) .build(); } } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java index 000f8c83ed..510b74af10 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -49,6 +49,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 com.google.common.primitives.Ints; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Files; @@ -64,7 +65,9 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.shadows.MediaCodecInfoBuilder; import org.robolectric.shadows.ShadowMediaCodec; +import org.robolectric.shadows.ShadowMediaCodecList; /** End-to-end test for {@link Transformer}. */ @RunWith(AndroidJUnit4.class) @@ -401,26 +404,6 @@ public final class TransformerEndToEndTest { .isEqualTo(TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED); } - @Test - public void startTransformation_withVideoEncoderFormatUnsupported_completesWithError() - throws Exception { - Transformer transformer = - createTransformerBuilder(/* enableFallback= */ false) - .setTransformationRequest( - new TransformationRequest.Builder() - .setVideoMimeType(MimeTypes.VIDEO_H263) // unsupported encoder MIME type - .build()) - .build(); - MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); - - transformer.startTransformation(mediaItem, outputPath); - TransformationException exception = TransformerTestRunner.runUntilError(transformer); - - assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); - assertThat(exception.errorCode) - .isEqualTo(TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED); - } - @Test public void startTransformation_withIoError_completesWithError() throws Exception { Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); @@ -750,10 +733,26 @@ public final class TransformerEndToEndTest { /* inputBufferSize= */ 10_000, /* outputBufferSize= */ 10_000, /* codec= */ (in, out) -> out.put(in)); - ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AAC, codecConfig); - ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AC3, codecConfig); - ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AMR_NB, codecConfig); - ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AAC, codecConfig); + addCodec( + MimeTypes.AUDIO_AAC, + codecConfig, + /* colorFormats= */ ImmutableList.of(), + /* isDecoder= */ true); + addCodec( + MimeTypes.AUDIO_AC3, + codecConfig, + /* colorFormats= */ ImmutableList.of(), + /* isDecoder= */ true); + addCodec( + MimeTypes.AUDIO_AMR_NB, + codecConfig, + /* colorFormats= */ ImmutableList.of(), + /* isDecoder= */ true); + addCodec( + MimeTypes.AUDIO_AAC, + codecConfig, + /* colorFormats= */ ImmutableList.of(), + /* isDecoder= */ false); ShadowMediaCodec.CodecConfig throwingCodecConfig = new ShadowMediaCodec.CodecConfig( @@ -776,9 +775,49 @@ public final class TransformerEndToEndTest { } }); - ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AMR_WB, throwingCodecConfig); - ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AMR_NB, throwingCodecConfig); - ShadowMediaCodec.addEncoder(MimeTypes.VIDEO_H263, throwingCodecConfig); + addCodec( + MimeTypes.AUDIO_AMR_WB, + throwingCodecConfig, + /* colorFormats= */ ImmutableList.of(), + /* isDecoder= */ true); + addCodec( + MimeTypes.AUDIO_AMR_NB, + throwingCodecConfig, + /* colorFormats= */ ImmutableList.of(), + /* isDecoder= */ false); + } + + private static void addCodec( + String mimeType, + ShadowMediaCodec.CodecConfig codecConfig, + List colorFormats, + boolean isDecoder) { + String codecName = + Util.formatInvariant( + isDecoder ? "exo.%s.decoder" : "exo.%s.encoder", mimeType.replace('/', '-')); + if (isDecoder) { + ShadowMediaCodec.addDecoder(codecName, codecConfig); + } else { + ShadowMediaCodec.addEncoder(codecName, codecConfig); + } + + MediaFormat mediaFormat = new MediaFormat(); + mediaFormat.setString(MediaFormat.KEY_MIME, mimeType); + MediaCodecInfoBuilder.CodecCapabilitiesBuilder codecCapabilities = + MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder() + .setMediaFormat(mediaFormat) + .setIsEncoder(!isDecoder); + + if (!colorFormats.isEmpty()) { + codecCapabilities.setColorFormats(Ints.toArray(colorFormats)); + } + + ShadowMediaCodecList.addCodec( + MediaCodecInfoBuilder.newBuilder() + .setName(codecName) + .setIsEncoder(!isDecoder) + .setCapabilities(codecCapabilities.build()) + .build()); } private static void removeEncodersAndDecoders() { diff --git a/libraries/ui/build.gradle b/libraries/ui/build.gradle index e1e5229f33..c911f434eb 100644 --- a/libraries/ui/build.gradle +++ b/libraries/ui/build.gradle @@ -16,7 +16,7 @@ apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" android.buildTypes.debug.testCoverageEnabled true dependencies { - implementation project(modulePrefix + 'lib-exoplayer') + implementation project(modulePrefix + 'lib-common') implementation 'androidx.media:media:' + androidxMediaVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java index 35f5cabd5d..8cb8816540 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java @@ -56,6 +56,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import androidx.media3.common.C; +import androidx.media3.common.Format; import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.Player; @@ -74,15 +75,12 @@ import androidx.media3.common.util.Util; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Formatter; -import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -1127,8 +1125,11 @@ public class PlayerControlView extends FrameLayout { if (!trackGroupInfo.isTrackSupported(trackIndex)) { continue; } - String trackName = - trackNameProvider.getTrackName(trackGroupInfo.getTrackFormat(trackIndex)); + Format trackFormat = trackGroupInfo.getTrackFormat(trackIndex); + if ((trackFormat.selectionFlags & C.SELECTION_FLAG_FORCED) != 0) { + continue; + } + String trackName = trackNameProvider.getTrackName(trackFormat); tracks.add(new TrackInformation(tracksInfo, trackGroupIndex, trackIndex, trackName)); } } @@ -1867,11 +1868,8 @@ public class PlayerControlView extends FrameLayout { player.setTrackSelectionParameters( trackSelectionParameters .buildUpon() - .setDisabledTrackTypes( - new ImmutableSet.Builder<@C.TrackType Integer>() - .addAll(trackSelectionParameters.disabledTrackTypes) - .add(C.TRACK_TYPE_TEXT) - .build()) + .clearOverridesOfType(C.TRACK_TYPE_TEXT) + .setIgnoredTextSelectionFlags(~C.SELECTION_FLAG_FORCED) .build()); settingsWindow.dismiss(); } @@ -1910,15 +1908,12 @@ public class PlayerControlView extends FrameLayout { } TrackSelectionParameters trackSelectionParameters = player.getTrackSelectionParameters(); - Set<@C.TrackType Integer> disabledTrackTypes = - new HashSet<>(trackSelectionParameters.disabledTrackTypes); - disabledTrackTypes.remove(C.TRACK_TYPE_AUDIO); castNonNull(player) .setTrackSelectionParameters( trackSelectionParameters .buildUpon() .clearOverridesOfType(C.TRACK_TYPE_AUDIO) - .setDisabledTrackTypes(disabledTrackTypes) + .setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, /* disabled= */ false) .build()); settingsAdapter.setSubTextAtPosition( SETTINGS_AUDIO_TRACK_SELECTION_POSITION, @@ -1995,6 +1990,7 @@ public class PlayerControlView extends FrameLayout { @Override public void onBindViewHolder(SubSettingViewHolder holder, int position) { + @Nullable Player player = PlayerControlView.this.player; if (player == null) { return; } @@ -2003,29 +1999,23 @@ public class PlayerControlView extends FrameLayout { } else { TrackInformation track = tracks.get(position - 1); TrackGroup trackGroup = track.trackGroupInfo.getTrackGroup(); - TrackSelectionParameters params = checkNotNull(player).getTrackSelectionParameters(); + TrackSelectionParameters params = player.getTrackSelectionParameters(); boolean explicitlySelected = params.overrides.get(trackGroup) != null && track.isSelected(); holder.textView.setText(track.trackName); holder.checkView.setVisibility(explicitlySelected ? VISIBLE : INVISIBLE); holder.itemView.setOnClickListener( v -> { - if (player == null) { - return; - } TrackSelectionParameters trackSelectionParameters = player.getTrackSelectionParameters(); - Set<@C.TrackType Integer> disabledTrackTypes = - new HashSet<>(trackSelectionParameters.disabledTrackTypes); - disabledTrackTypes.remove(track.trackGroupInfo.getTrackType()); - checkNotNull(player) - .setTrackSelectionParameters( - trackSelectionParameters - .buildUpon() - .setOverrideForType( - new TrackSelectionOverride( - trackGroup, ImmutableList.of(track.trackIndex))) - .setDisabledTrackTypes(disabledTrackTypes) - .build()); + player.setTrackSelectionParameters( + trackSelectionParameters + .buildUpon() + .setOverrideForType( + new TrackSelectionOverride( + trackGroup, ImmutableList.of(track.trackIndex))) + .setTrackTypeDisabled( + track.trackGroupInfo.getTrackType(), /* disabled= */ false) + .build()); onTrackSelection(track.trackName); settingsWindow.dismiss(); }); diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlViewLayoutManager.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlViewLayoutManager.java index 2bee086110..e9c5b2e082 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlViewLayoutManager.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlViewLayoutManager.java @@ -604,13 +604,15 @@ import java.util.List; } if (timeBar != null) { - MarginLayoutParams timeBarParams = (MarginLayoutParams) timeBar.getLayoutParams(); int timeBarMarginBottom = playerControlView .getResources() .getDimensionPixelSize(R.dimen.exo_styled_progress_margin_bottom); - timeBarParams.bottomMargin = (isMinimalMode ? 0 : timeBarMarginBottom); - timeBar.setLayoutParams(timeBarParams); + @Nullable MarginLayoutParams timeBarParams = (MarginLayoutParams) timeBar.getLayoutParams(); + if (timeBarParams != null) { + timeBarParams.bottomMargin = (isMinimalMode ? 0 : timeBarMarginBottom); + timeBar.setLayoutParams(timeBarParams); + } if (timeBar instanceof DefaultTimeBar) { DefaultTimeBar defaultTimeBar = (DefaultTimeBar) timeBar; if (isMinimalMode) { diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java index ed6f7412f4..ef7e22325c 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java @@ -43,6 +43,7 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.ColorInt; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -156,22 +157,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *

      • Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)} *
      • Default: {@code false} *
      - *
    • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below - * for more details. - *
        - *
      • Corresponding method: None - *
      • Default: {@code R.layout.exo_player_view} - *
      - *
    • {@code controller_layout_id} - Specifies the id of the layout resource to be - * inflated by the child {@link PlayerControlView}. See below for more details. - *
        - *
      • Corresponding method: None - *
      • Default: {@code R.layout.exo_player_control_view} - *
      *
    • All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can * also be set on a PlayerView, and will be propagated to the inflated {@link * PlayerControlView} unless the layout is overridden to specify a custom {@code - * exo_controller} (see below). + * exo_controller}. *
    * *

    Overriding drawables

    @@ -180,30 +169,30 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * by drawables with the same names defined in your application. See the {@link PlayerControlView} * documentation for a list of drawables that can be overridden. */ -@UnstableApi public class PlayerView extends FrameLayout implements AdViewProvider { /** * Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link * #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}. */ + @UnstableApi @Documented @Retention(RetentionPolicy.SOURCE) @Target(TYPE_USE) @IntDef({SHOW_BUFFERING_NEVER, SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS}) public @interface ShowBuffering {} /** The buffering view is never shown. */ - public static final int SHOW_BUFFERING_NEVER = 0; + @UnstableApi public static final int SHOW_BUFFERING_NEVER = 0; /** * The buffering view is shown when the player is in the {@link Player#STATE_BUFFERING buffering} * state and {@link Player#getPlayWhenReady() playWhenReady} is {@code true}. */ - public static final int SHOW_BUFFERING_WHEN_PLAYING = 1; + @UnstableApi public static final int SHOW_BUFFERING_WHEN_PLAYING = 1; /** * The buffering view is always shown when the player is in the {@link Player#STATE_BUFFERING * buffering} state. */ - public static final int SHOW_BUFFERING_ALWAYS = 2; + @UnstableApi public static final int SHOW_BUFFERING_ALWAYS = 2; private static final int SURFACE_TYPE_NONE = 0; private static final int SURFACE_TYPE_SURFACE_VIEW = 1; @@ -239,8 +228,6 @@ public class PlayerView extends FrameLayout implements AdViewProvider { private boolean controllerHideOnTouch; private int textureViewRotation; private boolean isTouching; - private static final int PICTURE_TYPE_FRONT_COVER = 3; - private static final int PICTURE_TYPE_NOT_SET = -1; public PlayerView(Context context) { this(context, /* attrs= */ null); @@ -445,6 +432,9 @@ public class PlayerView extends FrameLayout implements AdViewProvider { controller.hideImmediately(); controller.addVisibilityListener(/* listener= */ componentListener); } + if (useController) { + setClickable(true); + } updateContentDescription(); } @@ -455,6 +445,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param oldPlayerView The old view to detach from the player. * @param newPlayerView The new view to attach to the player. */ + @UnstableApi public static void switchTargetView( Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) { if (oldPlayerView == newPlayerView) { @@ -550,18 +541,21 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param resizeMode The {@link ResizeMode}. */ + @UnstableApi public void setResizeMode(@ResizeMode int resizeMode) { Assertions.checkStateNotNull(contentFrame); contentFrame.setResizeMode(resizeMode); } /** Returns the {@link ResizeMode}. */ + @UnstableApi public @ResizeMode int getResizeMode() { Assertions.checkStateNotNull(contentFrame); return contentFrame.getResizeMode(); } /** Returns whether artwork is displayed if present in the media. */ + @UnstableApi public boolean getUseArtwork() { return useArtwork; } @@ -571,6 +565,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param useArtwork Whether artwork is displayed. */ + @UnstableApi public void setUseArtwork(boolean useArtwork) { Assertions.checkState(!useArtwork || artworkView != null); if (this.useArtwork != useArtwork) { @@ -580,6 +575,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { } /** Returns the default artwork to display. */ + @UnstableApi @Nullable public Drawable getDefaultArtwork() { return defaultArtwork; @@ -591,6 +587,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param defaultArtwork the default artwork to display */ + @UnstableApi public void setDefaultArtwork(@Nullable Drawable defaultArtwork) { if (this.defaultArtwork != defaultArtwork) { this.defaultArtwork = defaultArtwork; @@ -599,6 +596,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { } /** Returns whether the playback controls can be shown. */ + @UnstableApi public boolean getUseController() { return useController; } @@ -607,10 +605,15 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * Sets whether the playback controls can be shown. If set to {@code false} the playback controls * are never visible and are disconnected from the player. * + *

    This call will update whether the view is clickable. After the call, the view will be + * clickable if playback controls can be shown or if the view has a registered click listener. + * * @param useController Whether the playback controls can be shown. */ + @UnstableApi public void setUseController(boolean useController) { Assertions.checkState(!useController || controller != null); + setClickable(useController || hasOnClickListeners()); if (this.useController == useController) { return; } @@ -629,7 +632,8 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param color The background color. */ - public void setShutterBackgroundColor(int color) { + @UnstableApi + public void setShutterBackgroundColor(@ColorInt int color) { if (shutterView != null) { shutterView.setBackgroundColor(color); } @@ -654,6 +658,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param keepContentOnPlayerReset Whether the currently displayed video frame or media artwork is * kept visible when the player is reset. */ + @UnstableApi public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) { if (this.keepContentOnPlayerReset != keepContentOnPlayerReset) { this.keepContentOnPlayerReset = keepContentOnPlayerReset; @@ -669,6 +674,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * {@link #SHOW_BUFFERING_NEVER}, {@link #SHOW_BUFFERING_WHEN_PLAYING} and {@link * #SHOW_BUFFERING_ALWAYS}. */ + @UnstableApi public void setShowBuffering(@ShowBuffering int showBuffering) { if (this.showBuffering != showBuffering) { this.showBuffering = showBuffering; @@ -681,6 +687,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param errorMessageProvider The error message provider. */ + @UnstableApi public void setErrorMessageProvider( @Nullable ErrorMessageProvider errorMessageProvider) { if (this.errorMessageProvider != errorMessageProvider) { @@ -695,6 +702,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param message The message to display, or {@code null} to clear a previously set message. */ + @UnstableApi public void setCustomErrorMessage(@Nullable CharSequence message) { Assertions.checkState(errorMessageView != null); customErrorMessage = message; @@ -732,11 +740,13 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param event A key event. * @return Whether the key event was handled. */ + @UnstableApi public boolean dispatchMediaKeyEvent(KeyEvent event) { return useController() && controller.dispatchMediaKeyEvent(event); } /** Returns whether the controller is currently fully visible. */ + @UnstableApi public boolean isControllerFullyVisible() { return controller != null && controller.isFullyVisible(); } @@ -748,11 +758,13 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet, * is paused, has ended or failed. */ + @UnstableApi public void showController() { showController(shouldShowControllerIndefinitely()); } /** Hides the playback controls. Does nothing if playback controls are disabled. */ + @UnstableApi public void hideController() { if (controller != null) { controller.hide(); @@ -767,6 +779,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @return The timeout in milliseconds. A non-positive value will cause the controller to remain * visible indefinitely. */ + @UnstableApi public int getControllerShowTimeoutMs() { return controllerShowTimeoutMs; } @@ -778,6 +791,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the * controller to remain visible indefinitely. */ + @UnstableApi public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { Assertions.checkStateNotNull(controller); this.controllerShowTimeoutMs = controllerShowTimeoutMs; @@ -788,6 +802,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { } /** Returns whether the playback controls are hidden by touch events. */ + @UnstableApi public boolean getControllerHideOnTouch() { return controllerHideOnTouch; } @@ -797,6 +812,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param controllerHideOnTouch Whether the playback controls are hidden by touch events. */ + @UnstableApi public void setControllerHideOnTouch(boolean controllerHideOnTouch) { Assertions.checkStateNotNull(controller); this.controllerHideOnTouch = controllerHideOnTouch; @@ -808,6 +824,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * ends, or fails. If set to false, the playback controls can be manually operated with {@link * #showController()} and {@link #hideController()}. */ + @UnstableApi public boolean getControllerAutoShow() { return controllerAutoShow; } @@ -819,6 +836,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param controllerAutoShow Whether the playback controls are allowed to show automatically. */ + @UnstableApi public void setControllerAutoShow(boolean controllerAutoShow) { this.controllerAutoShow = controllerAutoShow; } @@ -829,6 +847,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. */ + @UnstableApi public void setControllerHideDuringAds(boolean controllerHideDuringAds) { this.controllerHideDuringAds = controllerHideDuringAds; } @@ -839,6 +858,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param listener The listener to be notified about visibility changes, or null to remove the * current listener. */ + @UnstableApi public void setControllerVisibilityListener( @Nullable PlayerControlView.VisibilityListener listener) { Assertions.checkStateNotNull(controller); @@ -860,6 +880,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param listener The listener to be notified when the fullscreen button is clicked, or null to * remove the current listener and hide the fullscreen button. */ + @UnstableApi public void setControllerOnFullScreenModeChangedListener( @Nullable PlayerControlView.OnFullScreenModeChangedListener listener) { Assertions.checkStateNotNull(controller); @@ -871,6 +892,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showRewindButton Whether the rewind button is shown. */ + @UnstableApi public void setShowRewindButton(boolean showRewindButton) { Assertions.checkStateNotNull(controller); controller.setShowRewindButton(showRewindButton); @@ -881,6 +903,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showFastForwardButton Whether the fast forward button is shown. */ + @UnstableApi public void setShowFastForwardButton(boolean showFastForwardButton) { Assertions.checkStateNotNull(controller); controller.setShowFastForwardButton(showFastForwardButton); @@ -891,6 +914,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showPreviousButton Whether the previous button is shown. */ + @UnstableApi public void setShowPreviousButton(boolean showPreviousButton) { Assertions.checkStateNotNull(controller); controller.setShowPreviousButton(showPreviousButton); @@ -901,6 +925,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showNextButton Whether the next button is shown. */ + @UnstableApi public void setShowNextButton(boolean showNextButton) { Assertions.checkStateNotNull(controller); controller.setShowNextButton(showNextButton); @@ -911,6 +936,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. */ + @UnstableApi public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { Assertions.checkStateNotNull(controller); controller.setRepeatToggleModes(repeatToggleModes); @@ -921,6 +947,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showShuffleButton Whether the shuffle button is shown. */ + @UnstableApi public void setShowShuffleButton(boolean showShuffleButton) { Assertions.checkStateNotNull(controller); controller.setShowShuffleButton(showShuffleButton); @@ -931,6 +958,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showSubtitleButton Whether the subtitle button is shown. */ + @UnstableApi public void setShowSubtitleButton(boolean showSubtitleButton) { Assertions.checkStateNotNull(controller); controller.setShowSubtitleButton(showSubtitleButton); @@ -941,6 +969,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showVrButton Whether the vr button is shown. */ + @UnstableApi public void setShowVrButton(boolean showVrButton) { Assertions.checkStateNotNull(controller); controller.setShowVrButton(showVrButton); @@ -951,6 +980,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showMultiWindowTimeBar Whether to show all windows. */ + @UnstableApi public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { Assertions.checkStateNotNull(controller); controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); @@ -966,6 +996,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad * markers. */ + @UnstableApi public void setExtraAdGroupMarkers( @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { Assertions.checkStateNotNull(controller); @@ -978,6 +1009,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param listener The listener to be notified about aspect ratios changes of the video content or * the content frame. */ + @UnstableApi public void setAspectRatioListener( @Nullable AspectRatioFrameLayout.AspectRatioListener listener) { Assertions.checkStateNotNull(contentFrame); @@ -1001,6 +1033,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @return The {@link SurfaceView}, {@link TextureView}, {@code SphericalGLSurfaceView}, {@code * VideoDecoderGLSurfaceView} or {@code null}. */ + @UnstableApi @Nullable public View getVideoSurfaceView() { return surfaceView; @@ -1013,6 +1046,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and * the overlay is not present. */ + @UnstableApi @Nullable public FrameLayout getOverlayFrameLayout() { return overlayFrameLayout; @@ -1024,35 +1058,16 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the * subtitle view is not present. */ + @UnstableApi @Nullable public SubtitleView getSubtitleView() { return subtitleView; } - @Override - public boolean onTouchEvent(MotionEvent event) { - if (!useController() || player == null) { - return false; - } - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - isTouching = true; - return true; - case MotionEvent.ACTION_UP: - if (isTouching) { - isTouching = false; - return performClick(); - } - return false; - default: - return false; - } - } - @Override public boolean performClick() { - super.performClick(); - return toggleControllerVisibility(); + toggleControllerVisibility(); + return super.performClick(); } @Override @@ -1097,6 +1112,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param contentFrame The content frame, or {@code null}. * @param aspectRatio The aspect ratio to apply. */ + @UnstableApi protected void onContentAspectRatioChanged( @Nullable AspectRatioFrameLayout contentFrame, float aspectRatio) { if (contentFrame != null) { @@ -1148,18 +1164,15 @@ public class PlayerView extends FrameLayout implements AdViewProvider { return false; } - private boolean toggleControllerVisibility() { + private void toggleControllerVisibility() { if (!useController() || player == null) { - return false; + return; } if (!controller.isFullyVisible()) { maybeShowController(true); - return true; } else if (controllerHideOnTouch) { controller.hide(); - return true; } - return false; } /** Shows the playback controls, but only if forced or shown indefinitely. */ diff --git a/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionDialogBuilder.java b/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionDialogBuilder.java index 97427aaa5d..86291df78c 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionDialogBuilder.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionDialogBuilder.java @@ -15,8 +15,6 @@ */ package androidx.media3.ui; -import static androidx.media3.common.util.Assertions.checkNotNull; - import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; @@ -25,17 +23,22 @@ import android.view.LayoutInflater; import android.view.View; import androidx.annotation.Nullable; import androidx.annotation.StyleRes; +import androidx.media3.common.C; import androidx.media3.common.Format; -import androidx.media3.common.TrackGroupArray; +import androidx.media3.common.Player; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.TrackSelectionOverride; +import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.TracksInfo.TrackGroupInfo; import androidx.media3.common.util.UnstableApi; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride; -import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo; -import androidx.media3.exoplayer.trackselection.TrackSelectionUtil; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; /** Builder for a dialog with a {@link TrackSelectionView}. */ @UnstableApi @@ -47,25 +50,24 @@ public final class TrackSelectionDialogBuilder { /** * Called when tracks are selected. * - * @param isDisabled Whether the renderer is disabled. - * @param overrides List of selected track selection overrides for the renderer. + * @param isDisabled Whether the disabled option is selected. + * @param overrides The selected track overrides. */ - void onTracksSelected(boolean isDisabled, List overrides); + void onTracksSelected(boolean isDisabled, Map overrides); } private final Context context; - @StyleRes private int themeResId; private final CharSequence title; - private final MappedTrackInfo mappedTrackInfo; - private final int rendererIndex; + private final List trackGroupInfos; private final DialogCallback callback; + @StyleRes private int themeResId; private boolean allowAdaptiveSelections; private boolean allowMultipleOverrides; private boolean showDisableOption; @Nullable private TrackNameProvider trackNameProvider; private boolean isDisabled; - private List overrides; + private Map overrides; @Nullable private Comparator trackFormatComparator; /** @@ -73,59 +75,53 @@ public final class TrackSelectionDialogBuilder { * * @param context The context of the dialog. * @param title The title of the dialog. - * @param mappedTrackInfo The {@link MappedTrackInfo} containing the track information. - * @param rendererIndex The renderer index in the {@code mappedTrackInfo} for which the track - * selection is shown. + * @param trackGroupInfos The {@link TrackGroupInfo TrackGroupInfos} for the track groups. * @param callback The {@link DialogCallback} invoked when a track selection has been made. */ public TrackSelectionDialogBuilder( Context context, CharSequence title, - MappedTrackInfo mappedTrackInfo, - int rendererIndex, + List trackGroupInfos, DialogCallback callback) { this.context = context; this.title = title; - this.mappedTrackInfo = mappedTrackInfo; - this.rendererIndex = rendererIndex; + this.trackGroupInfos = ImmutableList.copyOf(trackGroupInfos); this.callback = callback; - overrides = Collections.emptyList(); + overrides = Collections.emptyMap(); } /** - * Creates a builder for a track selection dialog which automatically updates a {@link - * DefaultTrackSelector}. + * Creates a builder for a track selection dialog. * * @param context The context of the dialog. * @param title The title of the dialog. - * @param trackSelector A {@link DefaultTrackSelector} whose current selection is used to set up - * the dialog and which is updated when new tracks are selected in the dialog. - * @param rendererIndex The renderer index in the {@code trackSelector} for which the track - * selection is shown. + * @param player The {@link Player} whose tracks should be selected. + * @param trackType The type of tracks to show for selection. */ public TrackSelectionDialogBuilder( - Context context, CharSequence title, DefaultTrackSelector trackSelector, int rendererIndex) { + Context context, CharSequence title, Player player, @C.TrackType int trackType) { this.context = context; this.title = title; - this.mappedTrackInfo = checkNotNull(trackSelector.getCurrentMappedTrackInfo()); - this.rendererIndex = rendererIndex; - - TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - DefaultTrackSelector.Parameters selectionParameters = trackSelector.getParameters(); - isDisabled = selectionParameters.getRendererDisabled(rendererIndex); - SelectionOverride override = - selectionParameters.getSelectionOverride(rendererIndex, rendererTrackGroups); - overrides = override == null ? Collections.emptyList() : Collections.singletonList(override); - - this.callback = - (newIsDisabled, newOverrides) -> - trackSelector.setParameters( - TrackSelectionUtil.updateParametersWithOverride( - selectionParameters, - rendererIndex, - rendererTrackGroups, - newIsDisabled, - newOverrides.isEmpty() ? null : newOverrides.get(0))); + List allTrackGroupInfos = player.getCurrentTracksInfo().getTrackGroupInfos(); + trackGroupInfos = new ArrayList<>(); + for (int i = 0; i < allTrackGroupInfos.size(); i++) { + TrackGroupInfo trackGroupInfo = allTrackGroupInfos.get(i); + if (trackGroupInfo.getTrackType() == trackType) { + trackGroupInfos.add(trackGroupInfo); + } + } + overrides = Collections.emptyMap(); + callback = + (isDisabled, overrides) -> { + TrackSelectionParameters.Builder parametersBuilder = + player.getTrackSelectionParameters().buildUpon(); + parametersBuilder.setTrackTypeDisabled(trackType, isDisabled); + parametersBuilder.clearOverridesOfType(trackType); + for (TrackSelectionOverride override : overrides.values()) { + parametersBuilder.addOverride(override); + } + player.setTrackSelectionParameters(parametersBuilder.build()); + }; } /** @@ -151,27 +147,28 @@ public final class TrackSelectionDialogBuilder { } /** - * Sets the initial selection override to show. + * Sets the single initial override. * - * @param override The initial override to show, or null for no override. + * @param override The initial override, or {@code null} for no override. * @return This builder, for convenience. */ - public TrackSelectionDialogBuilder setOverride(@Nullable SelectionOverride override) { + public TrackSelectionDialogBuilder setOverride(@Nullable TrackSelectionOverride override) { return setOverrides( - override == null ? Collections.emptyList() : Collections.singletonList(override)); + override == null ? Collections.emptyMap() : ImmutableMap.of(override.trackGroup, override)); } /** - * Sets the list of initial selection overrides to show. + * Sets the initial track overrides. Any overrides that do not correspond to track groups + * described by {@code trackGroupInfos} that have been given to this instance will be ignored. If + * {@link #setAllowMultipleOverrides(boolean)} hasn't been set to {@code true} then all but one + * override will be ignored. The retained override will be the one whose track group is described + * first in {@code trackGroupInfos}. * - *

    Note that only the first override will be used unless {@link - * #setAllowMultipleOverrides(boolean)} is set to {@code true}. - * - * @param overrides The list of initial overrides to show. There must be at most one override for - * each track group. + * @param overrides The initially selected track overrides. * @return This builder, for convenience. */ - public TrackSelectionDialogBuilder setOverrides(List overrides) { + public TrackSelectionDialogBuilder setOverrides( + Map overrides) { this.overrides = overrides; return this; } @@ -301,12 +298,7 @@ public final class TrackSelectionDialogBuilder { selectionView.setTrackNameProvider(trackNameProvider); } selectionView.init( - mappedTrackInfo, - rendererIndex, - isDisabled, - overrides, - trackFormatComparator, - /* listener= */ null); + trackGroupInfos, isDisabled, overrides, trackFormatComparator, /* listener= */ null); return (dialog, which) -> callback.onTracksSelected(selectionView.getIsDisabled(), selectionView.getOverrides()); } diff --git a/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionView.java b/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionView.java index 9864ae6f8e..0016d77a20 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/TrackSelectionView.java @@ -18,29 +18,25 @@ package androidx.media3.ui; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; -import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.widget.CheckedTextView; import android.widget.LinearLayout; import androidx.annotation.AttrRes; import androidx.annotation.Nullable; -import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.TrackGroup; -import androidx.media3.common.TrackGroupArray; +import androidx.media3.common.TrackSelectionOverride; +import androidx.media3.common.TracksInfo.TrackGroupInfo; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; -import androidx.media3.exoplayer.RendererCapabilities; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride; -import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.HashMap; import java.util.List; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.RequiresNonNull; +import java.util.Map; /** A view for making track selections. */ @UnstableApi @@ -52,10 +48,36 @@ public class TrackSelectionView extends LinearLayout { /** * Called when the selected tracks changed. * - * @param isDisabled Whether the renderer is disabled. - * @param overrides List of selected track selection overrides for the renderer. + * @param isDisabled Whether the disabled option is selected. + * @param overrides The selected track overrides. */ - void onTrackSelectionChanged(boolean isDisabled, List overrides); + void onTrackSelectionChanged( + boolean isDisabled, Map overrides); + } + + /** + * Returns the subset of {@code overrides} that apply to track groups in {@code trackGroupInfos}. + * If {@code allowMultipleOverrides} is {@code} then at most one override is retained, which will + * be the one whose track group is first in {@code trackGroupInfos}. + * + * @param overrides The overrides to filter. + * @param trackGroupInfos The track groups whose overrides should be retained. + * @param allowMultipleOverrides Whether more than one override can be retained. + * @return The filtered overrides. + */ + public static Map filterOverrides( + Map overrides, + List trackGroupInfos, + boolean allowMultipleOverrides) { + HashMap filteredOverrides = new HashMap<>(); + for (int i = 0; i < trackGroupInfos.size(); i++) { + TrackGroupInfo trackGroupInfo = trackGroupInfos.get(i); + @Nullable TrackSelectionOverride override = overrides.get(trackGroupInfo.getTrackGroup()); + if (override != null && (allowMultipleOverrides || filteredOverrides.isEmpty())) { + filteredOverrides.put(override.trackGroup, override); + } + } + return filteredOverrides; } private final int selectableItemBackgroundResourceId; @@ -63,7 +85,8 @@ public class TrackSelectionView extends LinearLayout { private final CheckedTextView disableView; private final CheckedTextView defaultView; private final ComponentListener componentListener; - private final SparseArray overrides; + private final List trackGroupInfos; + private final Map overrides; private boolean allowAdaptiveSelections; private boolean allowMultipleOverrides; @@ -71,9 +94,6 @@ public class TrackSelectionView extends LinearLayout { private TrackNameProvider trackNameProvider; private CheckedTextView[][] trackViews; - private @MonotonicNonNull MappedTrackInfo mappedTrackInfo; - private int rendererIndex; - private TrackGroupArray trackGroups; private boolean isDisabled; @Nullable private Comparator trackInfoComparator; @Nullable private TrackSelectionListener listener; @@ -94,9 +114,6 @@ public class TrackSelectionView extends LinearLayout { Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); setOrientation(LinearLayout.VERTICAL); - - overrides = new SparseArray<>(); - // Don't save view hierarchy as it needs to be reinitialized with a call to init. setSaveFromParentEnabled(false); @@ -110,7 +127,8 @@ public class TrackSelectionView extends LinearLayout { inflater = LayoutInflater.from(context); componentListener = new ComponentListener(); trackNameProvider = new DefaultTrackNameProvider(getResources()); - trackGroups = TrackGroupArray.EMPTY; + trackGroupInfos = new ArrayList<>(); + overrides = new HashMap<>(); // View for disabling the renderer. disableView = @@ -155,26 +173,28 @@ public class TrackSelectionView extends LinearLayout { /** * Sets whether tracks from multiple track groups can be selected. This results in multiple {@link - * SelectionOverride SelectionOverrides} to be returned by {@link #getOverrides()}. + * TrackSelectionOverride TrackSelectionOverrides} being returned by {@link #getOverrides()}. * - * @param allowMultipleOverrides Whether multiple track selection overrides can be selected. + * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected. */ public void setAllowMultipleOverrides(boolean allowMultipleOverrides) { if (this.allowMultipleOverrides != allowMultipleOverrides) { this.allowMultipleOverrides = allowMultipleOverrides; if (!allowMultipleOverrides && overrides.size() > 1) { - for (int i = overrides.size() - 1; i > 0; i--) { - overrides.remove(i); - } + // Re-filter the overrides to retain only one of them. + Map filteredOverrides = + filterOverrides(overrides, trackGroupInfos, /* allowMultipleOverrides= */ false); + overrides.clear(); + overrides.putAll(filteredOverrides); } updateViews(); } } /** - * Sets whether an option is available for disabling the renderer. + * Sets whether the disabled option can be selected. * - * @param showDisableOption Whether the disable option is shown. + * @param showDisableOption Whether the disabled option can be selected. */ public void setShowDisableOption(boolean showDisableOption) { disableView.setVisibility(showDisableOption ? View.VISIBLE : View.GONE); @@ -192,57 +212,47 @@ public class TrackSelectionView extends LinearLayout { } /** - * Initialize the view to select tracks for a specified renderer using {@link MappedTrackInfo} and - * a set of {@link DefaultTrackSelector.Parameters}. + * Initialize the view to select tracks from a specified list of track groups. * - * @param mappedTrackInfo The {@link MappedTrackInfo}. - * @param rendererIndex The index of the renderer. - * @param isDisabled Whether the renderer should be initially shown as disabled. - * @param overrides List of initial overrides to be shown for this renderer. There must be at most - * one override for each track group. If {@link #setAllowMultipleOverrides(boolean)} hasn't - * been set to {@code true}, only the first override is used. + * @param trackGroupInfos {@link TrackGroupInfo TrackGroupInfos} for the track groups. + * @param isDisabled Whether the disabled option should be initially selected. + * @param overrides The initially selected track overrides. Any overrides that do not correspond + * to track groups described in {@code trackGroupInfos} will be ignored. If {@link + * #setAllowMultipleOverrides(boolean)} hasn't been set to {@code true} then all but one + * override will be ignored. The retained override will be the one whose track group is + * described first in {@code trackGroupInfos}. * @param trackFormatComparator An optional comparator used to determine the display order of the * tracks within each track group. - * @param listener An optional listener for track selection updates. + * @param listener An optional listener to receive selection updates. */ public void init( - MappedTrackInfo mappedTrackInfo, - int rendererIndex, + List trackGroupInfos, boolean isDisabled, - List overrides, + Map overrides, @Nullable Comparator trackFormatComparator, @Nullable TrackSelectionListener listener) { - this.mappedTrackInfo = mappedTrackInfo; - this.rendererIndex = rendererIndex; this.isDisabled = isDisabled; this.trackInfoComparator = trackFormatComparator == null ? null - : (o1, o2) -> trackFormatComparator.compare(o1.format, o2.format); + : (o1, o2) -> trackFormatComparator.compare(o1.getFormat(), o2.getFormat()); this.listener = listener; - int maxOverrides = allowMultipleOverrides ? overrides.size() : Math.min(overrides.size(), 1); - for (int i = 0; i < maxOverrides; i++) { - SelectionOverride override = overrides.get(i); - this.overrides.put(override.groupIndex, override); - } + + this.trackGroupInfos.clear(); + this.trackGroupInfos.addAll(trackGroupInfos); + this.overrides.clear(); + this.overrides.putAll(filterOverrides(overrides, trackGroupInfos, allowMultipleOverrides)); updateViews(); } - /** Returns whether the renderer is disabled. */ + /** Returns whether the disabled option is selected. */ public boolean getIsDisabled() { return isDisabled; } - /** - * Returns the list of selected track selection overrides. There will be at most one override for - * each track group. - */ - public List getOverrides() { - List overrideList = new ArrayList<>(overrides.size()); - for (int i = 0; i < overrides.size(); i++) { - overrideList.add(overrides.valueAt(i)); - } - return overrideList; + /** Returns the selected track overrides. */ + public Map getOverrides() { + return overrides; } // Private methods. @@ -253,7 +263,7 @@ public class TrackSelectionView extends LinearLayout { removeViewAt(i); } - if (mappedTrackInfo == null) { + if (trackGroupInfos.isEmpty()) { // The view is not initialized. disableView.setEnabled(false); defaultView.setEnabled(false); @@ -262,19 +272,18 @@ public class TrackSelectionView extends LinearLayout { disableView.setEnabled(true); defaultView.setEnabled(true); - trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - // Add per-track views. - trackViews = new CheckedTextView[trackGroups.length][]; + trackViews = new CheckedTextView[trackGroupInfos.size()][]; boolean enableMultipleChoiceForMultipleOverrides = shouldEnableMultiGroupSelection(); - for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { - TrackGroup group = trackGroups.get(groupIndex); - boolean enableMultipleChoiceForAdaptiveSelections = shouldEnableAdaptiveSelection(groupIndex); - trackViews[groupIndex] = new CheckedTextView[group.length]; + for (int trackGroupIndex = 0; trackGroupIndex < trackGroupInfos.size(); trackGroupIndex++) { + TrackGroupInfo trackGroupInfo = trackGroupInfos.get(trackGroupIndex); + boolean enableMultipleChoiceForAdaptiveSelections = + shouldEnableAdaptiveSelection(trackGroupInfo); + trackViews[trackGroupIndex] = new CheckedTextView[trackGroupInfo.length]; - TrackInfo[] trackInfos = new TrackInfo[group.length]; - for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - trackInfos[trackIndex] = new TrackInfo(groupIndex, trackIndex, group.getFormat(trackIndex)); + TrackInfo[] trackInfos = new TrackInfo[trackGroupInfo.length]; + for (int trackIndex = 0; trackIndex < trackGroupInfo.length; trackIndex++) { + trackInfos[trackIndex] = new TrackInfo(trackGroupInfo, trackIndex); } if (trackInfoComparator != null) { Arrays.sort(trackInfos, trackInfoComparator); @@ -291,17 +300,16 @@ public class TrackSelectionView extends LinearLayout { CheckedTextView trackView = (CheckedTextView) inflater.inflate(trackViewLayoutId, this, false); trackView.setBackgroundResource(selectableItemBackgroundResourceId); - trackView.setText(trackNameProvider.getTrackName(trackInfos[trackIndex].format)); + trackView.setText(trackNameProvider.getTrackName(trackInfos[trackIndex].getFormat())); trackView.setTag(trackInfos[trackIndex]); - if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex) - == C.FORMAT_HANDLED) { + if (trackGroupInfo.isTrackSupported(trackIndex)) { trackView.setFocusable(true); trackView.setOnClickListener(componentListener); } else { trackView.setFocusable(false); trackView.setEnabled(false); } - trackViews[groupIndex][trackIndex] = trackView; + trackViews[trackGroupIndex][trackIndex] = trackView; addView(trackView); } } @@ -313,11 +321,12 @@ public class TrackSelectionView extends LinearLayout { disableView.setChecked(isDisabled); defaultView.setChecked(!isDisabled && overrides.size() == 0); for (int i = 0; i < trackViews.length; i++) { - SelectionOverride override = overrides.get(i); + @Nullable + TrackSelectionOverride override = overrides.get(trackGroupInfos.get(i).getTrackGroup()); for (int j = 0; j < trackViews[i].length; j++) { if (override != null) { TrackInfo trackInfo = (TrackInfo) Assertions.checkNotNull(trackViews[i][j].getTag()); - trackViews[i][j].setChecked(override.containsTrack(trackInfo.trackIndex)); + trackViews[i][j].setChecked(override.trackIndices.contains(trackInfo.trackIndex)); } else { trackViews[i][j].setChecked(false); } @@ -352,74 +361,52 @@ public class TrackSelectionView extends LinearLayout { private void onTrackViewClicked(View view) { isDisabled = false; TrackInfo trackInfo = (TrackInfo) Assertions.checkNotNull(view.getTag()); - int groupIndex = trackInfo.groupIndex; + TrackGroup trackGroup = trackInfo.trackGroupInfo.getTrackGroup(); int trackIndex = trackInfo.trackIndex; - SelectionOverride override = overrides.get(groupIndex); - Assertions.checkNotNull(mappedTrackInfo); + @Nullable TrackSelectionOverride override = overrides.get(trackGroup); if (override == null) { // Start new override. if (!allowMultipleOverrides && overrides.size() > 0) { // Removed other overrides if we don't allow multiple overrides. overrides.clear(); } - overrides.put(groupIndex, new SelectionOverride(groupIndex, trackIndex)); + overrides.put( + trackGroup, new TrackSelectionOverride(trackGroup, ImmutableList.of(trackIndex))); } else { // An existing override is being modified. - int overrideLength = override.length; - int[] overrideTracks = override.tracks; + ArrayList trackIndices = new ArrayList<>(override.trackIndices); boolean isCurrentlySelected = ((CheckedTextView) view).isChecked(); - boolean isAdaptiveAllowed = shouldEnableAdaptiveSelection(groupIndex); + boolean isAdaptiveAllowed = shouldEnableAdaptiveSelection(trackInfo.trackGroupInfo); boolean isUsingCheckBox = isAdaptiveAllowed || shouldEnableMultiGroupSelection(); if (isCurrentlySelected && isUsingCheckBox) { // Remove the track from the override. - if (overrideLength == 1) { - // The last track is being removed, so the override becomes empty. - overrides.remove(groupIndex); + trackIndices.remove((Integer) trackIndex); + if (trackIndices.isEmpty()) { + // The last track has been removed, so remove the whole override. + overrides.remove(trackGroup); } else { - int[] tracks = getTracksRemoving(overrideTracks, trackIndex); - overrides.put(groupIndex, new SelectionOverride(groupIndex, tracks)); + overrides.put(trackGroup, new TrackSelectionOverride(trackGroup, trackIndices)); } } else if (!isCurrentlySelected) { if (isAdaptiveAllowed) { // Add new track to adaptive override. - int[] tracks = getTracksAdding(overrideTracks, trackIndex); - overrides.put(groupIndex, new SelectionOverride(groupIndex, tracks)); + trackIndices.add(trackIndex); + overrides.put(trackGroup, new TrackSelectionOverride(trackGroup, trackIndices)); } else { // Replace existing track in override. - overrides.put(groupIndex, new SelectionOverride(groupIndex, trackIndex)); + overrides.put( + trackGroup, new TrackSelectionOverride(trackGroup, ImmutableList.of(trackIndex))); } } } } - @RequiresNonNull("mappedTrackInfo") - private boolean shouldEnableAdaptiveSelection(int groupIndex) { - return allowAdaptiveSelections - && trackGroups.get(groupIndex).length > 1 - && mappedTrackInfo.getAdaptiveSupport( - rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false) - != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; + private boolean shouldEnableAdaptiveSelection(TrackGroupInfo trackGroupInfo) { + return allowAdaptiveSelections && trackGroupInfo.isAdaptiveSupported(); } private boolean shouldEnableMultiGroupSelection() { - return allowMultipleOverrides && trackGroups.length > 1; - } - - private static int[] getTracksAdding(int[] tracks, int addedTrack) { - tracks = Arrays.copyOf(tracks, tracks.length + 1); - tracks[tracks.length - 1] = addedTrack; - return tracks; - } - - private static int[] getTracksRemoving(int[] tracks, int removedTrack) { - int[] newTracks = new int[tracks.length - 1]; - int trackCount = 0; - for (int track : tracks) { - if (track != removedTrack) { - newTracks[trackCount++] = track; - } - } - return newTracks; + return allowMultipleOverrides && trackGroupInfos.size() > 1; } // Internal classes. @@ -433,14 +420,16 @@ public class TrackSelectionView extends LinearLayout { } private static final class TrackInfo { - public final int groupIndex; + public final TrackGroupInfo trackGroupInfo; public final int trackIndex; - public final Format format; - public TrackInfo(int groupIndex, int trackIndex, Format format) { - this.groupIndex = groupIndex; + public TrackInfo(TrackGroupInfo trackGroupInfo, int trackIndex) { + this.trackGroupInfo = trackGroupInfo; this.trackIndex = trackIndex; - this.format = format; + } + + public Format getFormat() { + return trackGroupInfo.getTrackFormat(trackIndex); } } } diff --git a/libraries/ui/src/main/res/layout/exo_legacy_player_view.xml b/libraries/ui/src/main/res/layout/exo_legacy_player_view.xml deleted file mode 100644 index 4ef9caefb3..0000000000 --- a/libraries/ui/src/main/res/layout/exo_legacy_player_view.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - -