diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 817b4d0fb1..74fbe78811 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -17,10 +17,14 @@ body: label: Version description: What version of Media3 (or ExoPlayer) are you using? options: - - Media3 1.2.1 - - Media3 1.2.0 - Media3 main branch - Media3 pre-release (alpha, beta or RC not in this list) + - Media3 1.4.1 + - Media3 1.4.0 + - Media3 1.3.1 + - Media3 1.3.0 + - Media3 1.2.1 + - Media3 1.2.0 - Media3 1.1.1 / ExoPlayer 2.19.1 - Media3 1.1.0 / ExoPlayer 2.19.0 - Media3 1.0.2 / ExoPlayer 2.18.7 diff --git a/README.md b/README.md index a57b9daa51..dba5092b90 100644 --- a/README.md +++ b/README.md @@ -116,23 +116,20 @@ First, clone the repository into a local directory: ```sh git clone https://github.com/androidx/media.git -cd media ``` Next, add the following to your project's `settings.gradle.kts` file, replacing `path/to/media` with the path to your local copy: ```kotlin -gradle.extra.apply { - set("androidxMediaModulePrefix", "media-") -} +(gradle as ExtensionAware).extra["androidxMediaModulePrefix"] = "media3-" apply(from = file("path/to/media/core_settings.gradle")) ``` Or in Gradle Groovy DSL `settings.gradle`: ```groovy -gradle.ext.androidxMediaModulePrefix = 'media-' +gradle.ext.androidxMediaModulePrefix = 'media3-' apply from: file("path/to/media/core_settings.gradle") ``` @@ -141,17 +138,37 @@ You can depend on them from `build.gradle.kts` as you would on any other local module, for example: ```kotlin -implementation(project(":media-lib-exoplayer")) -implementation(project(":media-lib-exoplayer-dash")) -implementation(project(":media-lib-ui")) +implementation(project(":media3-lib-exoplayer")) +implementation(project(":media3-lib-exoplayer-dash")) +implementation(project(":media3-lib-ui")) ``` Or in Gradle Groovy DSL `build.gradle`: ```groovy -implementation project(':media-lib-exoplayer') -implementation project(':media-lib-exoplayer-dash') -implementation project(':media-lib-ui') +implementation project(':media3-lib-exoplayer') +implementation project(':media3-lib-exoplayer-dash') +implementation project(':media3-lib-ui') +``` + +#### MIDI module + +By default the [MIDI module](libraries/decoder_midi) is disabled as a local +dependency, because it requires additional Maven repository config. If you want +to use it as a local dependency, please configure the JitPack repository as +[described in the module README](libraries/decoder_midi/README.md#getting-the-module), +and then enable building the module in your `settings.gradle.kts` file: + +```kotlin +gradle.extra.apply { + set("androidxMediaEnableMidiModule", true) +} +``` + +Or in Gradle Groovy DSL `settings.gradle`: + +```groovy +gradle.ext.androidxMediaEnableMidiModule = true ``` ## Developing AndroidX Media diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 372e3faf53..988f7ba628 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,752 @@ # Release notes +## 1.4 + +### 1.4.1 (2024-08-23) + +This release includes the following changes since the +[1.4.0 release](#140-2024-07-24): + +* ExoPlayer: + * Handle preload callbacks asynchronously in `PreloadMediaSource` + ([#1568](https://github.com/androidx/media/issues/1568)). + * Allow playback regardless of buffered duration when loading fails + ([#1571](https://github.com/androidx/media/issues/1571)). +* Extractors: + * MP3: Fix `Searched too many bytes` error by correctly ignoring trailing + non-MP3 data based on the length field in an `Info` frame + ([#1480](https://github.com/androidx/media/issues/1480)). +* Text: + * TTML: Fix handling of percentage `tts:fontSize` values to ensure they + are correctly inherited from parent nodes with percentage `tts:fontSize` + values. + * Fix `IndexOutOfBoundsException` in `LegacySubtitleUtil` due to + incorrectly handling the case of the requested output start time being + greater than or equal to the final event time in the `Subtitle` + ([#1516](https://github.com/androidx/media/issues/1516)). +* DRM: + * Fix `android.media.MediaCodec$CryptoException: Operation not supported + in this configuration: ERROR_DRM_CANNOT_HANDLE` error on API 31+ devices + playing L1 Widevine content. This error is caused by an incomplete + implementation of the framework + [`MediaDrm.requiresSecureDecoder`](https://developer.android.com/reference/android/media/MediaDrm#requiresSecureDecoder\(java.lang.String\)) + method ([#1603](https://github.com/androidx/media/issues/1603)). +* Effect: + * Add a `release()` method to `GlObjectsProvider`. +* Session: + * Transform a double-tap of `KEYCODE_HEADSETHOOK` into a 'seek to next' + action, as + [documented](https://developer.android.com/reference/androidx/media3/session/MediaSession#media-key-events-mapping) + ([#1493](https://github.com/androidx/media/issues/1493)). + * Handle `KEYCODE_HEADSETHOOK` as a 'play' command in + `MediaButtonReceiver` when deciding whether to ignore it to avoid a + `ForegroundServiceDidNotStartInTimeException` + ([#1581](https://github.com/androidx/media/issues/1581)). +* RTSP Extension: + * Skip invalid Media Descriptions in SDP parsing + ([#1087](https://github.com/androidx/media/issues/1472)). + +### 1.4.0 (2024-07-24) + +This release includes the following changes since the +[1.3.1 release](#131-2024-04-11): + +* Common Library: + * Forward presumed no-op seek calls to the protected `BasePlayer.seekTo()` + and `SimpleBasePlayer.handleSeek()` methods instead of ignoring them. If + you are implementing these methods in a custom player, you may need to + handle these additional calls with `mediaItemIndex == C.INDEX_UNSET`. + * Remove compile dependency on enhanced Java 8 desugaring + ([#1312](https://github.com/androidx/media/issues/1312)). + * Ensure the duration passed to `MediaItem.Builder.setImageDurationMs()` + is ignored for a non-image `MediaItem` (as documented). + * Add `Format.customData` to store app-provided custom information about + `Format` instances. +* ExoPlayer: + * Add `BasePreloadManager` which coordinates the preloading for multiple + sources based on the priorities defined by their `rankingData`. + Customization is possible by extending this class. Add + `DefaultPreloadManager` which uses `PreloadMediaSource` to preload media + samples of the sources into memory, and uses an integer `rankingData` + that indicates the index of an item on the UI. + * Add `PlayerId` to most methods of `LoadControl` to enable `LoadControl` + implementations to support multiple players. + * Remove `Buffer.isDecodeOnly()` and `C.BUFFER_FLAG_DECODE_ONLY`. There is + no need to set this flag as renderers and decoders will decide to skip + buffers based on timestamp. Custom `Renderer` implementations should + check if the buffer time is at least + `BaseRenderer.getLastResetPositionUs()` to decide whether a sample + should be shown. Custom `SimpleDecoder` implementations can check + `isAtLeastOutputStartTimeUs()` if needed or mark other buffers with + `DecoderOutputBuffer.shouldBeSkipped` to skip them. + * Allow a null value to be returned by + `TargetPreloadStatusControl.getTargetPreloadStatus(T)` to indicate not + to preload a `MediaSource` with the given `rankingData`. + * Add `remove(MediaSource)` to `BasePreloadManager`. + * Add `reset()` to `BasePreloadManager` to release all the holding sources + while keep the preload manager instance. + * Add `ExoPlayer.setPriority()` (and `Builder.setPriority()`) to define + the priority value used in `PriorityTaskManager` and for MediaCodec + importance from API 35. + * Fix issue with updating the last rebuffer time which resulted in + incorrect `bs` (buffer starvation) key in CMCD + ([#1124](https://github.com/androidx/media/issues/1124)). + * Add + `PreloadMediaSource.PreloadControl.onLoadedToTheEndOfSource(PreloadMediaSource)` + to indicate that the source has loaded to the end. This allows the + `DefaultPreloadManager` and the custom + `PreloadMediaSource.PreloadControl` implementations to preload the next + source or take other actions. + * Fix bug where silence skipping at the end of items can trigger a + playback exception. + * Add `clear` to `PreloadMediaSource` to discard the preloading period. + * Add new error code + `PlaybackException.ERROR_CODE_DECODING_RESOURCES_RECLAIMED` that is used + when codec resources are reclaimed for higher priority tasks. + * Let `AdsMediaSource` load preroll ads before initial content media + preparation completes + ([#1358](https://github.com/androidx/media/issues/1358)). + * Fix bug where playback moved to `STATE_ENDED` when re-preparing a + multi-period DASH live stream after the original period was already + removed from the manifest. + * Rename `onTimelineRefreshed()` to `onSourcePrepared()` and + `onPrepared()` to `onTracksSelected()` in + `PreloadMediaSource.PreloadControl`. Also rename the IntDefs in + `DefaultPreloadManager.Stage` accordingly. + * Add experimental support for dynamic scheduling to better align work + with CPU wake-cycles and delay waking up to when renderers can progress. + You can enable this using `experimentalSetDynamicSchedulingEnabled()` + when setting up your ExoPlayer instance. + * Add `Renderer.getDurationToProgressUs()`. A `Renderer` can implement + this method to return to ExoPlayer the duration that playback must + advance for the renderer to progress. If `ExoPlayer` is set with + `experimentalSetDynamicSchedulingEnabled()` then `ExoPlayer` will call + this method when calculating the time to schedule its work task. + * Add `MediaCodecAdapter#OnBufferAvailableListener` to alert when input + and output buffers are available for use by `MediaCodecRenderer`. + `MediaCodecRenderer` will signal `ExoPlayer` when receiving these + callbacks and if `ExoPlayer` is set with + `experimentalSetDynamicSchedulingEnabled()`, then `ExoPlayer` will + schedule its work loop as renderers can make progress. + * Use data class for `LoadControl` methods instead of individual + parameters. + * Add `ExoPlayer.isReleased()` to check whether `Exoplayer.release()` has + been called. + * Add `ExoPlayer.Builder.setMaxSeekToPreviousPositionMs()` to configure + the maximum position for which `seekToPrevious()` seeks to the previous + item ([#1425](https://github.com/androidx/media/issues/1425)). + * Fix some audio focus inconsistencies, e.g. not reporting full or + transient focus loss while the player is paused + ([#1436](https://github.com/androidx/media/issues/1436)). + * Fix potential `IndexOutOfBoundsException` caused by extractors reporting + additional tracks after the initial preparation step + ([#1476](https://github.com/androidx/media/issues/1476)). + * `Effects` in `ExoPlayer.setVideoEffect()` will receive the timestamps + with the renderer offset removed + ([#1098](https://github.com/androidx/media/issues/1098)). + * Fix potential `IllegalArgumentException` when handling player error that + happened while reading ahead into another playlist item + ([#1483](https://github.com/androidx/media/issues/1483)). +* Transformer: + * Add `audioConversionProcess` and `videoConversionProcess` to + `ExportResult` indicating how the respective track in the output file + was made. + * Relax trim optimization H.264 level checks. + * Add support for changing between SDR and HDR input media in a sequence. + * Add support for composition-level audio effects. + * Add support for transcoding Ultra HDR images into HDR videos. + * Fix issue where the `DefaultAudioMixer` does not output the correct + amount of bytes after being reset and reused. + * Work around a decoder bug where the number of audio channels was capped + at stereo when handling PCM input. + * When selecting tracks in `ExoPlayerAssetLoader`, ignore audio channel + count constraints as they only apply for playback. + * Replace `androidx.media3.transformer.Muxer` interface with + `androidx.media3.muxer.Muxer` and remove + `androidx.media3.transformer.Muxer`. + * Fix HEIC image loading from content URI schemes. + ([#1373](https://github.com/androidx/media/issues/1373)). + * Adjust audio track duration in `AudioGraphInput` to improve AV sync. + * Remove `ExportResult.processedInputs` field. If you use this field for + codec details, then use `DefaultDecoderFactory.listener` instead. In + case of a codec exception, codec details will be available in the + `ExportException.codecInfo`. +* Extractors: + * MPEG-TS: Roll forward the change ensuring the last frame is rendered by + passing the last access unit of a stream to the sample queue + ([#7909](https://github.com/google/ExoPlayer/issues/7909)). + Incorporating fixes to resolve the issues that emerged in I-frame only + HLS streams([#1150](https://github.com/androidx/media/issues/1150)) and + H.262 HLS streams + ([#1126](https://github.com/androidx/media/issues/1126)). + * MP3: Prefer the data size from an `Info` frame over the size reported by + the underlying stream (e.g. file size, or HTTP `Content-Length` header). + This helps to exclude non-playable trailer data (e.g. album artwork) + from constant bitrate seeking calculations, making seeks more accurate + ([#1376](https://github.com/androidx/media/issues/1376)). + * MP3: Use the frame count and other data in an `Info` frame (if present) + to compute an average bitrate for constant bitrate seeking, rather than + extrapolating from the bitrate of the frame after the `Info` frame, + which may be artificially small, e.g. `PCUT` frame + ([#1376](https://github.com/androidx/media/issues/1376)). + * Fix PCM audio format extraction in AVI containers. +* Audio: + * Fix DTS:X Profile 2 encoding attributes for passthrough playback + ([#1299](https://github.com/androidx/media/pull/1299)). + * For offloaded playback, reset the tracking field for stream completion + in `DefaultAudioSink` prior to calling `AudioTrack.stop()` so that + `AudioTrack.StreamEventCallback#onPresentationEnded` correctly + identifies when all pending data has been played. + * Fix bug in `SilenceSkippingAudioProcessor` where transitions between + different audio formats (for example stereo to mono) can cause the + processor to throw an exception + ([#1352](https://github.com/androidx/media/issues/1352)). + * Implement `MediaCodecAudioRenderer.getDurationToProgressUs()` so that + ExoPlayer will dynamically schedule its main work loop to when the + MediaCodecAudioRenderer can make progress. +* Video: + * Fix issue where `Listener.onRenderedFirstFrame()` arrives too early when + switching surfaces mid-playback. + * Fix decoder fallback logic for Dolby Vision to use a compatible AV1 + decoder if needed + ([#1389](https://github.com/androidx/media/pull/1389)). + * Fix codec exception that may be caused by enabling a video renderer + mid-playback. +* Text: + * Fix issue where subtitles starting before a seek position are skipped. + This issue was only introduced in Media3 1.4.0-alpha01. + * Change default subtitle parsing behavior so it happens during extraction + instead of during rendering (see + [ExoPlayer's architecture diagram](https://developer.android.com/media/media3/exoplayer/glossary#exoplayer) + for the difference between extraction and rendering). + * This change can be overridden by calling **both** + `MediaSource.Factory.experimentalParseSubtitlesDuringExtraction(false)` + and `TextRenderer.experimentalSetLegacyDecodingEnabled(true)`. See + the + [docs on customization](https://developer.android.com/media/media3/exoplayer/customization) + for how to plumb these components into an `ExoPlayer` instance. + These methods (and all support for legacy subtitle decoding) will be + removed in a future release. + * Apps with custom `SubtitleDecoder` implementations need to update + them to implement `SubtitleParser` instead (and + `SubtitleParser.Factory` instead of `SubtitleDecoderFactory`). + * PGS: Fix run-length decoding to resolve `0` as a color index, instead of + a literal color value + ([#1367](https://github.com/androidx/media/pull/1367)). + * CEA-708: Ignore `rowLock` value. The CEA-708-E S-2023 spec states that + `rowLock` and `columnLock` should both be assumed to be true, regardless + of the values present in the stream (`columnLock` support is not + implemented, so it's effectively assumed to always be false). + * This was originally included in the `1.3.0-alpha01` release notes, + but the change was accidentally reverted before the `1.3.0-rc01` + release. This is now fixed, so the change is present again. + * CEA-708: Avoid duplicate newlines being added by ExoPlayer's naive + handling of the 'set pen location' command + ([#1315](https://github.com/androidx/media/pull/1315)). + * Fix an `IllegalArgumentException` from `LegacySubtitleUtil` when a + WebVTT subtitle sample contains no cues, e.g. as part of a DASH stream + ([#1516](https://github.com/androidx/media/issues/1516)). +* Metadata: + * Fix mapping of MP4 to ID3 sort tags. Previously the 'album sort' + (`soal`), 'artist sort' (`soar`) and 'album artist sort' (`soaa`) MP4 + tags were wrongly mapped to the `TSO2`, `TSOA` and `TSOP` ID3 tags + ([#1302](https://github.com/androidx/media/issues/1302)). + * Fix reading of MP4 (/iTunes) numeric `gnre` (genre) and `tmpo` (tempo) + tags when the value is more than one byte long. + * Propagate ID3 `TCON` frame to `MediaMetadata.genre` + ([#1305](https://github.com/androidx/media/issues/1305)). +* Image: + * Add support for non-square DASH thumbnail grids + ([#1300](https://github.com/androidx/media/pull/1300)). + * Add support for AVIF for API 34+. + * Allow `null` as parameter for `ExoPlayer.setImageOutput()` to clear a + previously set `ImageOutput`. +* DataSource: + * Implement support for `android.resource://package/id` raw resource URIs + where `package` is different to the package of the current application. + This wasn't previously documented to work, but is a more efficient way + of accessing resources in another package than by name. + * Eagerly check `url` is non-null in the `DataSpec` constructors. This + parameter was already annotated to be non-null. + * Allow `ByteArrayDataSource` to resolve a URI to a byte array during + `open()`, instead of being hard-coded at construction + ([#1405](https://github.com/androidx/media/issues/1405)). +* DRM: + * Allow setting a `LoadErrorHandlingPolicy` on + `DefaultDrmSessionManagerProvider` + ([#1271](https://github.com/androidx/media/issues/1271)). +* Effect: + * Support multiple speed changes within the same `EditedMediaItem` or + `Composition` in `SpeedChangeEffect`. + * Support for HLG and PQ output from ultra HDR bitmap input. + * Add support for EGL_GL_COLORSPACE_BT2020_HLG_EXT, which improves HLG + surface output in ExoPlayer.setVideoEffect and Transformer's Debug + SurfaceView. + * Update Overlay matrix implementation to make it consistent with the + documentation by flipping the x and y values applied in + `setOverlayFrameAnchor()`. If using + `OverlaySettings.Builder.setOverlayFrameAnchor()`, flip their x and y + values by multiplying them by `-1`. + * Fix bug where `TimestampWrapper` crashes when used with + `ExoPlayer#setVideoEffects` + ([#821](https://github.com/androidx/media/issues/821)). + * Change default SDR color working space from linear colors to electrical + BT 709 SDR video. Also provide third option to retain the original + colorspace. + * Allow defining indeterminate z-order of EditedMediaItemSequences + ([#1055](https://github.com/androidx/media/pull/1055)). + * Maintain a consistent luminance range across different pieces of HDR + content (uses the HLG range). + * Add support for Ultra HDR (bitmap) overlays on HDR content. + * Allow `SeparableConvolution` effects to be used before API 26. + * Remove unused `OverlaySettings.useHdr` since dynamic range of overlay + and frame must match. + * Add HDR support for `TextOverlay`. Luminance of the text overlay can be + adjusted with `OverlaySettings.Builder.setHdrLuminanceMultiplier()`. +* IMA extension: + * Promote API that is required for apps to play + [DAI ad streams](https://developers.google.com/ad-manager/dynamic-ad-insertion/full-service) + to stable. + * Add `replaceAdTagParameters(Map )` to + `ImaServerSideAdInsertionMediaSource.AdLoader` that allows replacing ad + tag parameters at runtime. + * Fix bug where `VideoAdPlayer.VideoAdPlayerCallback.onError()` was not + called when a player error happened during ad playback + ([#1334](https://github.com/androidx/media/issues/1334)). + * Bump IMA SDK version to 3.33.0 to fix a `NullPointerException` when + using `data://` ad tag URIs + ([#700](https://github.com/androidx/media/issues/700)). +* Session: + * Change default of `CommandButton.enabled` to `true` and ensure the value + can stay false for controllers even if the associated command is + available. + * Add icon constants for `CommandButton` that should be used instead of + custom icon resources. + * Add `MediaSessionService.isPlaybackOngoing()` to let apps query whether + the service needs to be stopped in `onTaskRemoved()` + ([#1219](https://github.com/androidx/media/issues/1219)). + * Add `MediaSessionService.pauseAllPlayersAndStopSelf()` that conveniently + allows to pause playback of all sessions and call `stopSelf()` to + terminate the lifecycle of the `MediaSessionService`. + * Override `MediaSessionService.onTaskRemoved(Intent)` to provide a safe + default implementation that keeps the service running in the foreground + if playback is ongoing or stops the service otherwise. + * Hide seekbar in the media notification for live streams by not setting + the duration into the platform session metadata + ([#1256](https://github.com/androidx/media/issues/1256)). + * Align conversion of `MediaMetadata` to `MediaDescriptionCompat`, to use + the same preferred order and logic when selecting metadata properties as + in media1. + * Add `MediaSession.sendError()` that allows sending non-fatal errors to + Media3 controller. When using the notification controller (see + `MediaSession.getMediaNotificationControllerInfo()`), the custom error + is used to update the `PlaybackState` of the platform session to an + error state with the given error information + ([#543](https://github.com/androidx/media/issues/543)). + * Add `MediaSession.Callback.onPlayerInteractionFinished()` to inform + sessions when a series of player interactions from a specific controller + finished. + * Add `SessionError` and use it in `SessionResult` and `LibraryResult` + instead of the error code to provide more information about the error + and how to resolve the error if possible. + * Publish the code for the media3 controller test app that can be used to + test interactions with apps publishing a media session. + * Propagate extras passed to media3's + `MediaSession[Builder].setSessionExtras()` to a media1 controller's + `PlaybackStateCompat.getExtras()`. + * Map fatal and non-fatal errors to and from the platform session. A + `PlaybackException` is mapped to a fatal error state of the + `PlaybackStateCompat`. A `SessionError` sent to the media notification + controller with `MediaSession.sendError(ControllerInfo, SessionError)` + is mapped to a non-fatal error in `PlaybackStateCompat` which means that + error code and message are set but the state of the platform session + remains different to `STATE_ERROR`. + * Allow the session activity to be set per controller to override the + global session activity. The session activity can be defined for a + controller at connection time by creating a `ConnectionResult` with + `AcceptedResultBuilder.setSessionActivivty(PendingIntent)`. Once + connected, the session activity can be updated with + `MediaSession.setSessionActivity(ControllerInfo, PendingIntent)`. + * Improve error replication of calls to `MediaLibrarySession.Callback`. + Error replication can now be configured by using + `MediaLibrarySession.Builder.setLibraryErrorReplicationMode()` for + choosing the error type or opt-ing out of error replication which is on + by default. +* UI: + * Add image display support to `PlayerView` when connected to an + `ExoPlayer` ([#1144](https://github.com/androidx/media/issues/1144)). + * Add customization of various icons in `PlayerControlView` through xml + attributes to allow different drawables per `PlayerView` instance, + rather than global overrides + ([#1200](https://github.com/androidx/media/issues/1200)). + * Work around a platform bug causing stretched/cropped video when using + `SurfaceView` inside a Compose `AndroidView` on API 34 + ([#1237](https://github.com/androidx/media/issues/1237)). +* Downloads: + * Ensure that `DownloadHelper` does not leak unreleased `Renderer` + instances, which can eventually result in an app crashing with + `IllegalStateException: Too many receivers, total of 1000, registered + for pid` ([#1224](https://github.com/androidx/media/issues/1224)). +* Cronet Extension: + * Fix `SocketTimeoutException` in `CronetDataSource`. In some versions of + Cronet, the request provided by the callback is not always the same. + This leads to callback not completing and request timing out + (https://issuetracker.google.com/328442628). +* HLS Extension: + * Fix bug where pending EMSG samples waiting for a discontinuity were + delegated in `HlsSampleStreamWrapper` with an incorrect offset causing + an `IndexOutOfBoundsException` or an `IllegalArgumentException` + ([#1002](https://github.com/androidx/media/issues/1002)). + * Fix bug where non-primary playlists keep reloading for LL-HLS streams + ([#1240](https://github.com/androidx/media/issues/1240)). + * Fix bug where enabling CMCD for HLS with initialization segments + resulted in `Source Error` and `IllegalArgumentException`. + * Fix bug where non-primary playing playlists are not refreshed during + live playback ([#1240](https://github.com/androidx/media/issues/1240)). + * Fix bug where enabling CMCD for HLS live streams causes + `ArrayIndexOutOfBoundsException` + ([#1395](https://github.com/androidx/media/issues/1395)). +* DASH Extension: + * Fix bug where re-preparing a multi-period live stream can throw an + `IndexOutOfBoundsException` + ([#1329](https://github.com/androidx/media/issues/1329)). + * Add support for `dashif:Laurl` license urls + ([#1345](https://github.com/androidx/media/issues/1345)). +* Cast Extension: + * Fix bug that converted the album title of the `MediaQueueItem` to the + artist in the Media3 media item + ([#1255](https://github.com/androidx/media/pull/1255)). +* Test Utilities: + * Implement `onInit()` and `onRelease()` in `FakeRenderer`. + * Change `TestPlayerRunHelper.runUntil()/playUntil()` methods to fail on + nonfatal errors (e.g. those reported to + `AnalyticsListener.onVideoCodecError()`). Use the new + `TestPlayerRunHelper.run(player).ignoringNonFatalErrors().untilXXX()` + method chain to disable this behavior. +* Demo app: + * Use `DefaultPreloadManager` in the short form demo app. + * Allow setting repeat mode with `Intent` arguments from command line + ([#1266](https://github.com/androidx/media/pull/1266)). + * Use `HttpEngineDataSource` as the `HttpDataSource` when supported by the + device. +* Remove deprecated symbols: + * Remove `CronetDataSourceFactory`. Use `CronetDataSource.Factory` + instead. + * Remove some `DataSpec` constructors. Use `DataSpec.Builder` instead. + * Remove `setContentTypePredicate(Predicate)` method from + `DefaultHttpDataSource`, `OkHttpDataSource` and `CronetDataSource`. Use + the equivalent method on each `XXXDataSource.Factory` instead. + * Remove `OkHttpDataSource` constructors and `OkHttpDataSourceFactory`. + Use `OkHttpDataSource.Factory` instead. + * Remove `PlayerMessage.setHandler(Handler)`. Use `setLooper(Looper)` + instead. + * Remove `Timeline.Window.isLive` field. Use the `isLive()` method + instead. + * Remove `DefaultHttpDataSource` constructors. Use + `DefaultHttpDataSource.Factory` instead. + * Remove `DashMediaSource.DEFAULT_LIVE_PRESENTATION_DELAY_MS`. Use + `DashMediaSource.DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS` instead. + * Remove `MediaCodecInfo.isSeamlessAdaptationSupported(Format, Format, + boolean)`. Use `MediaCodecInfo.canReuseCodec(Format, Format)` instead. + * Remove `DrmSessionManager.DUMMY` and `getDummyDrmSessionManager()` + method. Use `DrmSessionManager.DRM_UNSUPPORTED` instead. + * Remove `AnalyticsListener.onAudioInputFormatChanged(EventTime, Format)`, + `AnalyticsListener.onVideoInputFormatChanged(EventTime, Format)`, + `AudioRendererEventListener.onAudioInputFormatChanged(Format)`, + `VideoRendererEventListener.onVideoInputFormatChanged(Format)`. Use the + overloads that take a `DecoderReuseEvaluation` instead. + * Remove `RendererSupport.FormatSupport` IntDef and `FORMAT_HANDLED`, + `FORMAT_EXCEEDS_CAPABILITIES`, `FORMAT_UNSUPPORTED_DRM`, + `FORMAT_UNSUPPORTED_SUBTYPE`, `FORMAT_UNSUPPORTED_TYPE` constants. Use + the equivalent IntDef and constants in `androidx.media3.common.C` + instead (e.g. `C.FORMAT_HANDLED`). + * Remove `Bundleable` interface. This includes removing all + `Bundleable.Creator CREATOR` constant fields. Callers should use + the `Bundle toBundle()` and `static Foo fromBundle(Bundle)` methods on + each type instead. + +### 1.4.0-rc01 (2024-07-11) + +Use the 1.4.0 [stable version](#140-2024-07-24). + +### 1.4.0-beta01 (2024-06-21) + +Use the 1.4.0 [stable version](#140-2024-07-24). + +### 1.4.0-alpha02 (2024-06-06) + +Use the 1.4.0 [stable version](#140-2024-07-24). + +### 1.4.0-alpha01 (2024-04-11) + +Use the 1.4.0 [stable version](#140-2024-07-24). + +## 1.3 + +### 1.3.1 (2024-04-11) + +This release includes the following changes since the +[1.3.0 release](#130-2024-03-06): + +* Common Library: + * Add `Format.labels` to allow localized or other alternative labels. +* ExoPlayer: + * Fix issue where `PreloadMediaPeriod` cannot retain the streams when it + is preloaded again. + * Apply the correct corresponding `TrackSelectionResult` to the playing + period in track reselection. + * Start early-enabled renderers only after advancing the playing period + when transitioning between media items + ([#1017](https://github.com/androidx/media/issues/1017)). + * Add missing return type to proguard `-keepclasseswithmembers` rule for + `DefaultVideoFrameProcessor.Factory.Builder.build()` + ([#1187](https://github.com/androidx/media/issues/1187)). +* Transformer: + * Add workaround for exception thrown due to `MediaMuxer` not supporting + negative presentation timestamps before API 30. +* Track Selection: + * `DefaultTrackSelector`: Prefer video tracks with a 'reasonable' frame + rate (>=10fps) over those with a lower or unset frame rate. This ensures + the player selects the 'real' video track in MP4s extracted from motion + photos that can contain two HEVC tracks where one has a higher + resolution but a very small number of frames + ([#1051](https://github.com/androidx/media/issues/1051)). +* Extractors: + * Fix issue where padding was not skipped when reading odd-sized chunks + from WAV files ([#1117](https://github.com/androidx/media/pull/1117)). + * MP3: Populate `Format.averageBitrate` from metadata frames such as + `XING` and `VBRI`. + * MPEG-TS: Revert a change that aimed to ensure the last frame is rendered + by passing the last access unit of a stream to the sample queue + ([#7909](https://github.com/google/ExoPlayer/issues/7909)). This is due + to the change causing new problems with I-frame only HLS streams + ([#1150](https://github.com/androidx/media/issues/1150)) and H.262 HLS + streams ([#1126](https://github.com/androidx/media/issues/1126)). +* Audio: + * Allow renderer recovery by disabling offload if audio track fails to + initialize in offload mode. + * For offloaded playback, use the `AudioTrack.StreamEventCallback` method + `onPresentationEnded` to identify when all pending data has been played. +* Video: + * Add workaround for a device issue on Galaxy Tab S7 FE, Chromecast with + Google TV, and Lenovo M10 FHD Plus that causes 60fps H265 streams to be + marked as unsupported + * Add workaround that ensures the first frame is always rendered while + tunneling even if the device does not do this automatically as required + by the API ([#1169](https://github.com/androidx/media/issues/1169)). + ([#966](https://github.com/androidx/media/issues/966)). + * Fix issue where HDR color info handling causes codec misbehavior and + prevents adaptive format switches for SDR video tracks + ([#1158](https://github.com/androidx/media/issues/1158)). +* Text: + * WebVTT: Prevent directly consecutive cues from creating spurious + additional `CuesWithTiming` instances from `WebvttParser.parse` + ([#1177](https://github.com/androidx/media/issues/1177)). +* DRM: + * Work around a `NoSuchMethodError` which can be thrown by the `MediaDrm` + framework instead of `ResourceBusyException` or + `NotProvisionedException` on some Android 14 devices + ([#1145](https://github.com/androidx/media/issues/1145)). +* Effect: + * Improved PQ to SDR tone-mapping by converting color spaces. +* Session: + * Fix issue where the current position jumps back when the controller + replaces the current item + ([#951](https://github.com/androidx/media/issues/951)). + * Fix issue where `MediaMetadata` with just non-null `extras` is not + transmitted between media controllers and sessions + ([#1176](https://github.com/androidx/media/issues/1176)). +* UI: + * Fallback to include audio track language name if `Locale` cannot + identify a display name + ([#988](https://github.com/androidx/media/issues/988)). +* DASH Extension: + * Populate all `Label` elements from the manifest into `Format.labels` + ([#1054](https://github.com/androidx/media/pull/1054)). +* RTSP Extension: + * Skip empty session information values (i-tags) in SDP parsing + ([#1087](https://github.com/androidx/media/issues/1087)). +* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.): + * Disable the MIDI extension as a local dependency by default because it + requires an additional Maven repository to be configured. Users who need + this module from a local dependency + [can re-enable it](https://github.com/androidx/media/blob/main/README.md#midi-module). + +### 1.3.0 (2024-03-06) + +This release includes the following changes since the +[1.2.1 release](#121-2024-01-09): + +* Common Library: + * Implement support for `android.resource://package/[type/]name` raw + resource URIs where `package` is different to the package of the current + application. This has always been documented to work, but wasn't + correctly implemented until now. + * Normalize MIME types set by app code or read from media to be fully + lower-case. + * Define ads with a full `MediaItem` instead of a single `Uri` in + `AdPlaybackState`. + * Increase `minSdk` to 19 (Android KitKat). This is + [aligned with all other AndroidX libraries](https://android-developers.googleblog.com/2023/10/androidx-minsdkversion-19.html), + and is required for us to upgrade to the latest versions of our AndroidX + dependencies. + * Populate both `artworkUri` and `artworkData` in + `MediaMetadata.Builder.populate(MediaMetadata)` when at least one of + them is non-null ([#964](https://github.com/androidx/media/issues/964)). +* ExoPlayer: + * Add `PreloadMediaSource` and `PreloadMediaPeriod` that allows apps to + preload a content media source at a specific start position before + playback. `PreloadMediaSource` takes care of preparing the content media + source to receive the `Timeline`, preparing and caching the period at + the given start position, selecting tracks and loading media data for + the period. Apps control the preload progress by implementing + `PreloadMediaSource.PreloadControl` and set the preloaded source to the + player for playback. + * Add `ExoPlayer.setImageOutput` that allows apps to set + `ImageRenderer.ImageOutput`. + * `DefaultRenderersFactory` now provides an `ImageRenderer` to the player + by default with null `ImageOutput` and `ImageDecoder.Factory.DEFAULT`. + * Emit `Player.Listener.onPositionDiscontinuity` event when silence is + skipped ([#765](https://github.com/androidx/media/issues/765)). + * Add experimental support for parsing subtitles during extraction. You + can enable this using + `MediaSource.Factory.experimentalParseSubtitlesDuringExtraction()`. + * Support adaptive media sources with `PreloadMediaSource`. + * Implement `HttpEngineDataSource`, an `HttpDataSource` using the + [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine) + API. + * Prevent subclassing `CompositeSequenceableLoader`. This component was + [previously made extensible](https://github.com/androidx/media/commit/0de57cbfae7165dd3bb829e323d089cd312b4b1b) + but was never subclassed within the library. Customizations can be done + by wrapping an instance using the + [decorator pattern](https://en.wikipedia.org/wiki/Decorator_pattern) and + implementing a custom `CompositeSequenceableLoaderFactory`. + * Fix issue where repeating the same time causes metadata from this item + to be cleared ([#1007](https://github.com/androidx/media/issues/1007)). + * Rename `experimentalSetSubtitleParserFactory` methods on + `BundledChunkExtractor.Factory` and `DefaultHlsExtractorFactory` to + `setSubtitleParserFactory` and disallow passing `null`. Use the new + `experimentalParseSubtitlesDuringExtraction(boolean)` methods to control + parsing behaviour. + * Add support for customising the `SubtitleParser.Factory` used during + extraction. This can be achieved with + `MediaSource.Factory.setSubtitleParserFactory()`. + * Add source prefix to all `Format.id` fields generated from + `MergingMediaSource`. This helps to identify which source produced a + `Format` ([#883](https://github.com/androidx/media/issues/883)). + * Fix the regex used for validating custom Common Media Client Data (CMCD) + key names by modifying it to only check for hyphen + ([#1028](https://github.com/androidx/media/issues/1028)). + * Stop double-encoding CMCD query parameters + ([#1075](https://github.com/androidx/media/issues/1075)). +* Transformer: + * Add support for flattening H.265/HEVC SEF slow motion videos. + * Increase transmuxing speed, especially for 'remove video' edits. + * Add API to ensure that the output file starts on a video frame. This can + make the output of trimming operations more compatible with player + implementations that don't show the first video frame until its + presentation timestamp + ([#829](https://github.com/androidx/media/issues/829)). + * Add support for optimizing single asset mp4 trim operations. + * Add support to ensure a video frame has the first timestamp in the + output file. Fixes output files beginning with black frame on iOS based + players ([#829](https://github.com/androidx/media/issues/829)). +* Track Selection: + * Add `DefaultTrackSelector.selectImageTrack` to enable image track + selection. + * Add `TrackSelectionParameters.isPrioritizeImageOverVideoEnabled` to + determine whether to select an image track if both an image track and a + video track are available. The default value is `false` which means + selecting a video track is prioritized. +* Extractors: + * Add additional AV1C parsing to MP4 extractor to retrieve + `ColorInfo.colorSpace`, `ColorInfo.colorTransfer`, and + `ColorInfo.colorRange` values + ([#692](https://github.com/androidx/media/pull/692)). + * MP3: Use constant bitrate (CBR) seeking for files with an `Info` header + (the CBR equivalent of the `Xing` header). Previously we used the seek + table from the `Info` header, but this results in less precise seeking + than if we ignore it and assume the file is CBR. + * MPEG2-TS: Add DTS, DTS-LBR and DTS:X Profile2 support + ([#275](https://github.com/androidx/media/pull/275)). + * Extract audio types from TS descriptors and map them to role flags, + allowing users to make better-informed audio track selections + ([#973](https://github.com/androidx/media/pull/973)). +* Audio: + * Improve silence skipping algorithm with smooth volume ramp; retained + minimal silence and more natural silence durations + ([#7423](https://github.com/google/ExoPlayer/issues/7423)). + * Report the skipped silence more deterministically + ([#1035](https://github.com/androidx/media/issues/1035)). +* Video: + * Change the `MediaCodecVideoRenderer` constructor that takes a + `VideoFrameProcessor.Factory` argument and replace it with a constructor + that takes a `VideoSinkProvider` argument. Apps that want to inject a + custom `VideoFrameProcessor.Factory` can instantiate a + `CompositingVideoSinkProvider` that uses the custom + `VideoFrameProcessor.Factory` and pass the video sink provider to + `MediaCodecVideoRenderer`. +* Text: + * Fix serialization of bitmap cues to resolve `Tried to marshall a Parcel + that contained Binder objects` error when using + `DefaultExtractorsFactory.setTextTrackTranscodingEnabled` + ([#836](https://github.com/androidx/media/issues/836)). + * CEA-708: Ignore `rowLock` value. The CEA-708-E S-2023 spec states that + `rowLock` and `columnLock` should both be assumed to be true, regardless + of the values present in the stream (`columnLock` support is not + implemented, so it's effectively assumed to always be false). +* Image: + * Add support for DASH thumbnails. Grid images are cropped and individual + thumbnails are provided to `ImageOutput` close to their presentation + times. +* DRM: + * Play 'clear lead' unencrypted samples in DRM content immediately by + default, even if the keys for the later encrypted samples aren't ready + yet. This may lead to mid-playback stalls if the keys still aren't ready + when the playback position reaches the encrypted samples (but previously + playback wouldn't have started at all by this point). This behavior can + be disabled with + [`MediaItem.DrmConfiguration.Builder.setPlayClearContentWithoutKey`](https://developer.android.com/reference/androidx/media3/common/MediaItem.DrmConfiguration.Builder#setPlayClearContentWithoutKey\(boolean\)) + or + [`DefaultDrmSessionManager.Builder.setPlayClearSamplesWithoutKeys`](https://developer.android.com/reference/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.Builder#setPlayClearSamplesWithoutKeys\(boolean\)). +* IMA extension: + * Fix issue where DASH and HLS ads without the appropriate file extension + can't be played. +* Session: + * Disable double-click detection for TV apps + ([#962](https://github.com/androidx/media/issues/962)). + * Fix issue where `MediaItem.RequestMetadata` with just non-null extras is + not transmitted between media controllers and sessions. + * Add constructor to `MediaLibrarySession.Builder` that only takes a + `Context` instead of a `MediaLibraryService`. +* HLS Extension: + * Reduce `HlsMediaPeriod` to package-private visibility. This type + shouldn't be directly depended on from outside the HLS package. + * Resolve seeks to beginning of a segment more efficiently + ([#1031](https://github.com/androidx/media/pull/1031)). +* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.): + * MIDI decoder: Ignore SysEx event messages + ([#710](https://github.com/androidx/media/pull/710)). +* Test Utilities: + * Don't pause playback in `TestPlayerRunHelper.playUntilPosition`. The + test keeps the playback in a playing state, but suspends progress until + the test is able to add assertions and further actions. +* Demo app: + * Add a shortform demo module to demo the usage of `PreloadMediaSource` + with the short-form content use case. + +### 1.3.0-rc01 (2024-02-22) + +Use the 1.3.0 [stable version](#130-2024-03-06). + +### 1.3.0-beta01 (2024-02-07) + +Use the 1.3.0 [stable version](#130-2024-03-06). + +### 1.3.0-alpha01 (2024-01-15) + +Use the 1.3.0 [stable version](#130-2024-03-06). + ## 1.2 ### 1.2.1 (2024-01-09) diff --git a/api.txt b/api.txt index 32a5ca8539..4c03f14089 100644 --- a/api.txt +++ b/api.txt @@ -621,13 +621,20 @@ package androidx.media3.common { method public static String getErrorCodeName(@androidx.media3.common.PlaybackException.ErrorCode int); field public static final int CUSTOM_ERROR_CODE_BASE = 1000000; // 0xf4240 field public static final int ERROR_CODE_AUDIO_TRACK_INIT_FAILED = 5001; // 0x1389 + field public static final int ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED = 5004; // 0x138c + field public static final int ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED = 5003; // 0x138b field public static final int ERROR_CODE_AUDIO_TRACK_WRITE_FAILED = 5002; // 0x138a + field public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = -102; // 0xffffff9a + field public static final int ERROR_CODE_BAD_VALUE = -3; // 0xfffffffd field public static final int ERROR_CODE_BEHIND_LIVE_WINDOW = 1002; // 0x3ea + field public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = -104; // 0xffffff98 + field public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = -110; // 0xffffff92 field public static final int ERROR_CODE_DECODER_INIT_FAILED = 4001; // 0xfa1 field public static final int ERROR_CODE_DECODER_QUERY_FAILED = 4002; // 0xfa2 field public static final int ERROR_CODE_DECODING_FAILED = 4003; // 0xfa3 field public static final int ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES = 4004; // 0xfa4 field public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 4005; // 0xfa5 + field public static final int ERROR_CODE_DISCONNECTED = -100; // 0xffffff9c field public static final int ERROR_CODE_DRM_CONTENT_ERROR = 6003; // 0x1773 field public static final int ERROR_CODE_DRM_DEVICE_REVOKED = 6007; // 0x1777 field public static final int ERROR_CODE_DRM_DISALLOWED_OPERATION = 6005; // 0x1775 @@ -637,7 +644,9 @@ package androidx.media3.common { field public static final int ERROR_CODE_DRM_SCHEME_UNSUPPORTED = 6001; // 0x1771 field public static final int ERROR_CODE_DRM_SYSTEM_ERROR = 6006; // 0x1776 field public static final int ERROR_CODE_DRM_UNSPECIFIED = 6000; // 0x1770 + field public static final int ERROR_CODE_END_OF_PLAYLIST = -109; // 0xffffff93 field public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 1004; // 0x3ec + field public static final int ERROR_CODE_INVALID_STATE = -2; // 0xfffffffe field public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 2004; // 0x7d4 field public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 2007; // 0x7d7 field public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 2005; // 0x7d5 @@ -647,18 +656,25 @@ package androidx.media3.common { field public static final int ERROR_CODE_IO_NO_PERMISSION = 2006; // 0x7d6 field public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 2008; // 0x7d8 field public static final int ERROR_CODE_IO_UNSPECIFIED = 2000; // 0x7d0 + field public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = -106; // 0xffffff96 + field public static final int ERROR_CODE_NOT_SUPPORTED = -6; // 0xfffffffa + field public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = -105; // 0xffffff97 field public static final int ERROR_CODE_PARSING_CONTAINER_MALFORMED = 3001; // 0xbb9 field public static final int ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED = 3003; // 0xbbb field public static final int ERROR_CODE_PARSING_MANIFEST_MALFORMED = 3002; // 0xbba field public static final int ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED = 3004; // 0xbbc + field public static final int ERROR_CODE_PERMISSION_DENIED = -4; // 0xfffffffc + field public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = -103; // 0xffffff99 field public static final int ERROR_CODE_REMOTE_ERROR = 1001; // 0x3e9 + field public static final int ERROR_CODE_SETUP_REQUIRED = -108; // 0xffffff94 + field public static final int ERROR_CODE_SKIP_LIMIT_REACHED = -107; // 0xffffff95 field public static final int ERROR_CODE_TIMEOUT = 1003; // 0x3eb field public static final int ERROR_CODE_UNSPECIFIED = 1000; // 0x3e8 field @androidx.media3.common.PlaybackException.ErrorCode public final int errorCode; field public final long timestampMs; } - @IntDef(open=true, value={androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW, androidx.media3.common.PlaybackException.ERROR_CODE_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK, androidx.media3.common.PlaybackException.ERROR_CODE_IO_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, androidx.media3.common.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NO_PERMISSION, androidx.media3.common.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface PlaybackException.ErrorCode { + @IntDef(open=true, value={androidx.media3.common.PlaybackException.ERROR_CODE_INVALID_STATE, androidx.media3.common.PlaybackException.ERROR_CODE_BAD_VALUE, androidx.media3.common.PlaybackException.ERROR_CODE_PERMISSION_DENIED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_SUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DISCONNECTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED, androidx.media3.common.PlaybackException.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_CONCURRENT_STREAM_LIMIT, androidx.media3.common.PlaybackException.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_AVAILABLE_IN_REGION, androidx.media3.common.PlaybackException.ERROR_CODE_SKIP_LIMIT_REACHED, androidx.media3.common.PlaybackException.ERROR_CODE_SETUP_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_END_OF_PLAYLIST, androidx.media3.common.PlaybackException.ERROR_CODE_CONTENT_ALREADY_PLAYING, androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW, androidx.media3.common.PlaybackException.ERROR_CODE_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK, androidx.media3.common.PlaybackException.ERROR_CODE_IO_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, androidx.media3.common.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NO_PERMISSION, androidx.media3.common.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface PlaybackException.ErrorCode { } public final class PlaybackParameters { @@ -763,7 +779,7 @@ package androidx.media3.common { method @Deprecated public void setDeviceMuted(boolean); method public void setDeviceMuted(boolean, @androidx.media3.common.C.VolumeFlags int); method @Deprecated public void setDeviceVolume(@IntRange(from=0) int); - method public void setDeviceVolume(@IntRange(from=0) int, int); + method public void setDeviceVolume(@IntRange(from=0) int, @androidx.media3.common.C.VolumeFlags int); method public void setMediaItem(androidx.media3.common.MediaItem); method public void setMediaItem(androidx.media3.common.MediaItem, boolean); method public void setMediaItem(androidx.media3.common.MediaItem, long); @@ -826,6 +842,7 @@ package androidx.media3.common { field public static final int DISCONTINUITY_REASON_REMOVE = 4; // 0x4 field public static final int DISCONTINUITY_REASON_SEEK = 1; // 0x1 field public static final int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; // 0x2 + field public static final int DISCONTINUITY_REASON_SILENCE_SKIP = 6; // 0x6 field public static final int DISCONTINUITY_REASON_SKIP = 3; // 0x3 field public static final int EVENT_AUDIO_ATTRIBUTES_CHANGED = 20; // 0x14 field public static final int EVENT_AUDIO_SESSION_ID = 21; // 0x15 @@ -894,7 +911,7 @@ package androidx.media3.common { field public static final androidx.media3.common.Player.Commands EMPTY; } - @IntDef({androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT, androidx.media3.common.Player.DISCONTINUITY_REASON_SKIP, androidx.media3.common.Player.DISCONTINUITY_REASON_REMOVE, androidx.media3.common.Player.DISCONTINUITY_REASON_INTERNAL}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.DiscontinuityReason { + @IntDef({androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT, androidx.media3.common.Player.DISCONTINUITY_REASON_SKIP, androidx.media3.common.Player.DISCONTINUITY_REASON_REMOVE, androidx.media3.common.Player.DISCONTINUITY_REASON_INTERNAL, androidx.media3.common.Player.DISCONTINUITY_REASON_SILENCE_SKIP}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.DiscontinuityReason { } @IntDef({androidx.media3.common.Player.EVENT_TIMELINE_CHANGED, androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION, androidx.media3.common.Player.EVENT_TRACKS_CHANGED, androidx.media3.common.Player.EVENT_IS_LOADING_CHANGED, androidx.media3.common.Player.EVENT_PLAYBACK_STATE_CHANGED, androidx.media3.common.Player.EVENT_PLAY_WHEN_READY_CHANGED, androidx.media3.common.Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED, androidx.media3.common.Player.EVENT_REPEAT_MODE_CHANGED, androidx.media3.common.Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, androidx.media3.common.Player.EVENT_PLAYER_ERROR, androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY, androidx.media3.common.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, androidx.media3.common.Player.EVENT_AVAILABLE_COMMANDS_CHANGED, androidx.media3.common.Player.EVENT_MEDIA_METADATA_CHANGED, androidx.media3.common.Player.EVENT_PLAYLIST_METADATA_CHANGED, androidx.media3.common.Player.EVENT_SEEK_BACK_INCREMENT_CHANGED, androidx.media3.common.Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED, androidx.media3.common.Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED, androidx.media3.common.Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, androidx.media3.common.Player.EVENT_AUDIO_ATTRIBUTES_CHANGED, androidx.media3.common.Player.EVENT_AUDIO_SESSION_ID, androidx.media3.common.Player.EVENT_VOLUME_CHANGED, androidx.media3.common.Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED, androidx.media3.common.Player.EVENT_SURFACE_SIZE_CHANGED, androidx.media3.common.Player.EVENT_VIDEO_SIZE_CHANGED, androidx.media3.common.Player.EVENT_RENDERED_FIRST_FRAME, androidx.media3.common.Player.EVENT_CUES, androidx.media3.common.Player.EVENT_METADATA, androidx.media3.common.Player.EVENT_DEVICE_INFO_CHANGED, androidx.media3.common.Player.EVENT_DEVICE_VOLUME_CHANGED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.Event { @@ -1075,7 +1092,7 @@ package androidx.media3.common { method public androidx.media3.common.TrackSelectionParameters.Builder buildUpon(); method public static androidx.media3.common.TrackSelectionParameters fromBundle(android.os.Bundle); method public static androidx.media3.common.TrackSelectionParameters getDefaults(android.content.Context); - method public android.os.Bundle toBundle(); + method @CallSuper public android.os.Bundle toBundle(); field public final com.google.common.collect.ImmutableSet disabledTrackTypes; field public final boolean forceHighestSupportedBitrate; field public final boolean forceLowestBitrate; @@ -1369,7 +1386,7 @@ package androidx.media3.exoplayer.analytics { package androidx.media3.exoplayer.drm { - @RequiresApi(18) public final class FrameworkMediaDrm { + public final class FrameworkMediaDrm { method public static boolean isCryptoSchemeSupported(java.util.UUID); } @@ -1387,6 +1404,29 @@ package androidx.media3.exoplayer.ima { method public androidx.media3.exoplayer.ima.ImaAdsLoader build(); } + public final class ImaServerSideAdInsertionMediaSource implements androidx.media3.exoplayer.source.MediaSource { + } + + public static final class ImaServerSideAdInsertionMediaSource.AdsLoader { + method public androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader.State release(); + method public void setPlayer(androidx.media3.common.Player); + } + + public static final class ImaServerSideAdInsertionMediaSource.AdsLoader.Builder { + ctor public ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(android.content.Context, androidx.media3.common.AdViewProvider); + method public androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader build(); + method public androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader.Builder setAdsLoaderState(androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader.State); + } + + public static class ImaServerSideAdInsertionMediaSource.AdsLoader.State { + method public static androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader.State fromBundle(android.os.Bundle); + method public android.os.Bundle toBundle(); + } + + public static final class ImaServerSideAdInsertionMediaSource.Factory implements androidx.media3.exoplayer.source.MediaSource.Factory { + ctor public ImaServerSideAdInsertionMediaSource.Factory(androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader, androidx.media3.exoplayer.source.MediaSource.Factory); + } + } package androidx.media3.exoplayer.source { @@ -1396,6 +1436,7 @@ package androidx.media3.exoplayer.source { method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory clearLocalAdInsertionComponents(); method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory setDataSourceFactory(androidx.media3.datasource.DataSource.Factory); method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory setLocalAdInsertionComponents(androidx.media3.exoplayer.source.ads.AdsLoader.Provider, androidx.media3.common.AdViewProvider); + method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory setServerSideAdInsertionMediaSourceFactory(@Nullable androidx.media3.exoplayer.source.MediaSource.Factory); } public interface MediaSource { @@ -1484,7 +1525,7 @@ package androidx.media3.session { field @Nullable public final V value; } - @IntDef({androidx.media3.session.LibraryResult.RESULT_SUCCESS, androidx.media3.session.LibraryResult.RESULT_ERROR_UNKNOWN, androidx.media3.session.LibraryResult.RESULT_ERROR_INVALID_STATE, androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE, androidx.media3.session.LibraryResult.RESULT_ERROR_PERMISSION_DENIED, androidx.media3.session.LibraryResult.RESULT_ERROR_IO, androidx.media3.session.LibraryResult.RESULT_INFO_SKIPPED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_DISCONNECTED, androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface LibraryResult.Code { + @IntDef({androidx.media3.session.LibraryResult.RESULT_SUCCESS, androidx.media3.session.SessionError.INFO_CANCELLED, androidx.media3.session.SessionError.ERROR_UNKNOWN, androidx.media3.session.SessionError.ERROR_INVALID_STATE, androidx.media3.session.SessionError.ERROR_BAD_VALUE, androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED, androidx.media3.session.SessionError.ERROR_IO, androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED, androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionError.ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface LibraryResult.Code { } public final class MediaBrowser extends androidx.media3.session.MediaController { @@ -1760,6 +1801,7 @@ package androidx.media3.session { method public int getControllerVersion(); method public String getPackageName(); method public int getUid(); + field public static final String LEGACY_CONTROLLER_PACKAGE_NAME = "android.media.session.MediaController"; field public static final int LEGACY_CONTROLLER_VERSION = 0; // 0x0 } @@ -1808,6 +1850,7 @@ package androidx.media3.session { ctor public SessionCommands.Builder(); method public androidx.media3.session.SessionCommands.Builder add(androidx.media3.session.SessionCommand); method public androidx.media3.session.SessionCommands.Builder add(@androidx.media3.session.SessionCommand.CommandCode int); + method public androidx.media3.session.SessionCommands.Builder addSessionCommands(java.util.Collection); method public androidx.media3.session.SessionCommands build(); method public androidx.media3.session.SessionCommands.Builder remove(androidx.media3.session.SessionCommand); method public androidx.media3.session.SessionCommands.Builder remove(@androidx.media3.session.SessionCommand.CommandCode int); @@ -1837,7 +1880,7 @@ package androidx.media3.session { field @androidx.media3.session.SessionResult.Code public final int resultCode; } - @IntDef({androidx.media3.session.SessionResult.RESULT_SUCCESS, androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN, androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE, androidx.media3.session.SessionResult.RESULT_ERROR_BAD_VALUE, androidx.media3.session.SessionResult.RESULT_ERROR_PERMISSION_DENIED, androidx.media3.session.SessionResult.RESULT_ERROR_IO, androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface SessionResult.Code { + @IntDef({androidx.media3.session.SessionResult.RESULT_SUCCESS, androidx.media3.session.SessionError.INFO_CANCELLED, androidx.media3.session.SessionError.ERROR_UNKNOWN, androidx.media3.session.SessionError.ERROR_INVALID_STATE, androidx.media3.session.SessionError.ERROR_BAD_VALUE, androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED, androidx.media3.session.SessionError.ERROR_IO, androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED, androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionError.ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface SessionResult.Code { } public final class SessionToken { diff --git a/build.gradle b/build.gradle index 173e0e9b4b..ccc071a53b 100644 --- a/build.gradle +++ b/build.gradle @@ -17,9 +17,9 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.0.1' + classpath 'com.android.tools.build:gradle:8.3.2' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.4' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0' } } allprojects { diff --git a/common_library_config.gradle b/common_library_config.gradle index 115a450e2d..14e89821c7 100644 --- a/common_library_config.gradle +++ b/common_library_config.gradle @@ -24,11 +24,13 @@ android { targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + aarMetadata { + minCompileSdk = project.ext.compileSdkVersion + } multiDexEnabled true } compileOptions { - coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -43,5 +45,4 @@ android { dependencies { androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' } diff --git a/constants.gradle b/constants.gradle index 054dd37f80..905d3690bb 100644 --- a/constants.gradle +++ b/constants.gradle @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. project.ext { - releaseVersion = '1.2.1-dr10' - releaseVersionCode = 1_002_001_3_00 - minSdkVersion = 16 + releaseVersion = '1.4.1-dr1' + releaseVersionCode = 1_004_001_3_00 + minSdkVersion = 19 // See https://developer.android.com/training/cars/media/automotive-os#automotive-module automotiveMinSdkVersion = 28 appTargetSdkVersion = 34 @@ -23,17 +23,21 @@ project.ext { targetSdkVersion = 30 compileSdkVersion = 34 dexmakerVersion = '2.28.3' + // Use the same JUnit version as the Android repo: + // https://cs.android.com/android/platform/superproject/main/+/main:external/junit/METADATA junitVersion = '4.13.2' // Use the same Guava version as the Android repo: // https://cs.android.com/android/platform/superproject/main/+/main:external/guava/METADATA - guavaVersion = '31.1-android' + guavaVersion = '33.0.0-android' + kotlinxCoroutinesVersion = '1.8.1' + leakCanaryVersion = '2.10' mockitoVersion = '3.12.4' - robolectricVersion = '4.10.3' + robolectricVersion = '4.11' // Keep this in sync with Google's internal Checker Framework version. checkerframeworkVersion = '3.13.0' errorProneVersion = '2.18.0' jsr305Version = '3.0.2' - kotlinAnnotationsVersion = '1.8.20' + kotlinAnnotationsVersion = '1.9.0' // Updating this to 1.4.0+ will import Kotlin stdlib [internal ref: b/277891049]. androidxAnnotationVersion = '1.3.0' androidxAnnotationExperimentalVersion = '1.3.1' @@ -43,9 +47,8 @@ project.ext { // Updating this to 1.9.0+ will import Kotlin stdlib [internal ref: b/277891049]. androidxCoreVersion = '1.8.0' androidxExifInterfaceVersion = '1.3.6' - androidxFuturesVersion = '1.1.0' - androidxMediaVersion = '1.6.0' - androidxMedia2Version = '1.2.1' + androidxLifecycleVersion = '2.6.0' + androidxMediaVersion = '1.7.0' androidxMultidexVersion = '2.0.1' androidxRecyclerViewVersion = '1.3.0' androidxMaterialVersion = '1.8.0' @@ -54,10 +57,9 @@ project.ext { androidxTestJUnitVersion = '1.1.5' androidxTestRunnerVersion = '1.5.2' androidxTestRulesVersion = '1.5.0' - androidxTestServicesStorageVersion = '1.4.2' androidxTestTruthVersion = '1.5.0' - truthVersion = '1.1.3' - okhttpVersion = '4.11.0' + truthVersion = '1.4.0' + okhttpVersion = '4.12.0' modulePrefix = ':' if (gradle.ext.has('androidxMediaModulePrefix')) { modulePrefix += gradle.ext.androidxMediaModulePrefix diff --git a/core_settings.gradle b/core_settings.gradle index 210e6019cc..205c86c64d 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -57,6 +57,8 @@ include modulePrefix + 'lib-datasource' project(modulePrefix + 'lib-datasource').projectDir = new File(rootDir, 'libraries/datasource') include modulePrefix + 'lib-datasource-cronet' project(modulePrefix + 'lib-datasource-cronet').projectDir = new File(rootDir, 'libraries/datasource_cronet') +include modulePrefix + 'lib-datasource-httpengine' +project(modulePrefix + 'lib-datasource-httpengine').projectDir = new File(rootDir, 'libraries/datasource_httpengine') include modulePrefix + 'lib-datasource-rtmp' project(modulePrefix + 'lib-datasource-rtmp').projectDir = new File(rootDir, 'libraries/datasource_rtmp') include modulePrefix + 'lib-datasource-okhttp' @@ -70,8 +72,10 @@ include modulePrefix + 'lib-decoder-ffmpeg' project(modulePrefix + 'lib-decoder-ffmpeg').projectDir = new File(rootDir, 'libraries/decoder_ffmpeg') include modulePrefix + 'lib-decoder-flac' project(modulePrefix + 'lib-decoder-flac').projectDir = new File(rootDir, 'libraries/decoder_flac') -include modulePrefix + 'lib-decoder-midi' -project(modulePrefix + 'lib-decoder-midi').projectDir = new File(rootDir, 'libraries/decoder_midi') +if (gradle.ext.has('androidxMediaEnableMidiModule') && gradle.ext.androidxMediaEnableMidiModule) { + include modulePrefix + 'lib-decoder-midi' + project(modulePrefix + 'lib-decoder-midi').projectDir = new File(rootDir, 'libraries/decoder_midi') +} include modulePrefix + 'lib-decoder-opus' project(modulePrefix + 'lib-decoder-opus').projectDir = new File(rootDir, 'libraries/decoder_opus') include modulePrefix + 'lib-decoder-vp9' @@ -98,7 +102,3 @@ 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/cast/build.gradle b/demos/cast/build.gradle index 7f41b8e0be..de067a5708 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'com.android.application' android { namespace 'androidx.media3.demo.cast' - compileSdkVersion project.ext.compileSdkVersion + compileSdk project.ext.compileSdkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/demos/cast/src/main/java/androidx/media3/demo/cast/MainActivity.java b/demos/cast/src/main/java/androidx/media3/demo/cast/MainActivity.java index aa9bd9f08e..ae49a6f45a 100644 --- a/demos/cast/src/main/java/androidx/media3/demo/cast/MainActivity.java +++ b/demos/cast/src/main/java/androidx/media3/demo/cast/MainActivity.java @@ -44,6 +44,7 @@ import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.dynamite.DynamiteModule; +import com.google.common.util.concurrent.MoreExecutors; /** * An activity that plays video using {@link ExoPlayer} and supports casting using ExoPlayer's Cast @@ -65,7 +66,7 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Getting the cast context later than onStart can cause device discovery not to take place. try { - castContext = CastContext.getSharedInstance(this); + castContext = CastContext.getSharedInstance(this, MoreExecutors.directExecutor()).getResult(); } catch (RuntimeException e) { Throwable cause = e.getCause(); while (cause != null) { diff --git a/demos/compose/README.md b/demos/compose/README.md new file mode 100644 index 0000000000..a70fc11022 --- /dev/null +++ b/demos/compose/README.md @@ -0,0 +1,14 @@ +# ExoPlayer demo with Compose integration + +This is an experimental ExoPlayer demo app that is built fully using Compose +features. This should be taken as Work-In-Progress, rather than experimental API +for testing out application development with the media3 and Jetpack Compose +libraries. Please await further announcement via Release Notes for when the +implementation is fully integrated into the library. + +For an intermediate solution, use Jetpack Compose Interop with AndroidView and +PlayerView. However, note that it provides limited functionality and some +features may not be supported. + +See the [demos README](../README.md) for instructions on how to build and run +this demo. diff --git a/demos/compose/build.gradle b/demos/compose/build.gradle new file mode 100644 index 0000000000..9726565c87 --- /dev/null +++ b/demos/compose/build.gradle @@ -0,0 +1,76 @@ +// Copyright 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// 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. +apply from: '../../constants.gradle' +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + namespace 'androidx.media3.demo.compose' + + compileSdk project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion 21 + targetSdkVersion project.ext.appTargetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + signingConfig signingConfigs.debug + } + debug { + jniDebuggable = true + } + } + + lintOptions { + // The demo app isn't indexed, and doesn't have translations. + disable 'GoogleAppIndexingWarning','MissingTranslation' + } + buildFeatures { + viewBinding true + compose true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.0" + } +} + +dependencies { + def composeBom = platform('androidx.compose:compose-bom:2024.05.00') + implementation composeBom + + implementation 'androidx.activity:activity-compose:1.9.0' + implementation 'androidx.compose.foundation:foundation-android:1.6.7' + implementation 'androidx.compose.material3:material3-android:1.2.1' + implementation 'com.google.android.material:material:' + androidxMaterialVersion + + implementation project(modulePrefix + 'lib-exoplayer') + + // For detecting and debugging leaks only. LeakCanary is not needed for demo app to work. + debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion +} diff --git a/demos/compose/src/main/AndroidManifest.xml b/demos/compose/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a348407347 --- /dev/null +++ b/demos/compose/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt b/demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt new file mode 100644 index 0000000000..07dc6e1212 --- /dev/null +++ b/demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.demo.compose + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Surface +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.demo.compose.data.videos +import androidx.media3.exoplayer.ExoPlayer + +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + Surface { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val context = LocalContext.current + val exoPlayer = remember { + ExoPlayer.Builder(context).build().apply { + setMediaItem(MediaItem.fromUri(videos[0])) + prepare() + playWhenReady = true + repeatMode = Player.REPEAT_MODE_ONE + } + } + PlayerSurface( + player = exoPlayer, + surfaceType = SURFACE_TYPE_SURFACE_VIEW, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } + } + } + } +} diff --git a/demos/compose/src/main/java/androidx/media3/demo/compose/PlayerSurface.kt b/demos/compose/src/main/java/androidx/media3/demo/compose/PlayerSurface.kt new file mode 100644 index 0000000000..d26cc74733 --- /dev/null +++ b/demos/compose/src/main/java/androidx/media3/demo/compose/PlayerSurface.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.demo.compose + +import android.view.Surface +import android.view.SurfaceView +import android.view.TextureView +import androidx.annotation.IntDef +import androidx.compose.foundation.AndroidEmbeddedExternalSurface +import androidx.compose.foundation.AndroidExternalSurface +import androidx.compose.foundation.AndroidExternalSurfaceScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.media3.common.Player + +/** + * Provides a dedicated drawing [Surface] for media playbacks using a [Player]. + * + * The player's video output is displayed with either a [SurfaceView]/[AndroidExternalSurface] or a + * [TextureView]/[AndroidEmbeddedExternalSurface]. + * + * [Player] takes care of attaching the rendered output to the [Surface] and clearing it, when it is + * destroyed. + * + * See + * [Choosing a surface type](https://developer.android.com/media/media3/ui/playerview#surfacetype) + * for more information. + */ +@Composable +fun PlayerSurface(player: Player, surfaceType: @SurfaceType Int, modifier: Modifier = Modifier) { + val onSurfaceCreated: (Surface) -> Unit = { surface -> player.setVideoSurface(surface) } + val onSurfaceDestroyed: () -> Unit = { player.setVideoSurface(null) } + val onSurfaceInitialized: AndroidExternalSurfaceScope.() -> Unit = { + onSurface { surface, _, _ -> + onSurfaceCreated(surface) + surface.onDestroyed { onSurfaceDestroyed() } + } + } + + when (surfaceType) { + SURFACE_TYPE_SURFACE_VIEW -> + AndroidExternalSurface(modifier = modifier, onInit = onSurfaceInitialized) + SURFACE_TYPE_TEXTURE_VIEW -> + AndroidEmbeddedExternalSurface(modifier = modifier, onInit = onSurfaceInitialized) + else -> throw IllegalArgumentException("Unrecognized surface type: $surfaceType") + } +} + +/** + * The type of surface view used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW] or + * [SURFACE_TYPE_TEXTURE_VIEW]. + */ +@MustBeDocumented +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.TYPE_PARAMETER) +@IntDef(SURFACE_TYPE_SURFACE_VIEW, SURFACE_TYPE_TEXTURE_VIEW) +annotation class SurfaceType + +/** Surface type equivalent to [SurfaceView] . */ +const val SURFACE_TYPE_SURFACE_VIEW = 1 +/** Surface type equivalent to [TextureView]. */ +const val SURFACE_TYPE_TEXTURE_VIEW = 2 diff --git a/demos/compose/src/main/java/androidx/media3/demo/compose/data/videos.kt b/demos/compose/src/main/java/androidx/media3/demo/compose/data/videos.kt new file mode 100644 index 0000000000..0da7be9aba --- /dev/null +++ b/demos/compose/src/main/java/androidx/media3/demo/compose/data/videos.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.demo.compose.data + +val videos = + listOf( + "https://html5demos.com/assets/dizzy.mp4", + "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac", + "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm", + ) diff --git a/demos/compose/src/main/res/drawable/divider.xml b/demos/compose/src/main/res/drawable/divider.xml new file mode 100644 index 0000000000..1602867ace --- /dev/null +++ b/demos/compose/src/main/res/drawable/divider.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/demos/compose/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/compose/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..adaa93220e Binary files /dev/null and b/demos/compose/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/compose/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/compose/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..9b6f7d5e80 Binary files /dev/null and b/demos/compose/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/compose/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/compose/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..2101026c9f Binary files /dev/null and b/demos/compose/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/compose/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/compose/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..223ec8bd11 Binary files /dev/null and b/demos/compose/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..698ed68c42 Binary files /dev/null and b/demos/compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/compose/src/main/res/values-night/themes.xml b/demos/compose/src/main/res/values-night/themes.xml new file mode 100644 index 0000000000..454bf8cb30 --- /dev/null +++ b/demos/compose/src/main/res/values-night/themes.xml @@ -0,0 +1,33 @@ + + + + + + diff --git a/demos/compose/src/main/res/values/colors.xml b/demos/compose/src/main/res/values/colors.xml new file mode 100644 index 0000000000..dac0cb81c7 --- /dev/null +++ b/demos/compose/src/main/res/values/colors.xml @@ -0,0 +1,30 @@ + + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + #FF999999 + #292929 + #1c1c1c + #363434 + #635E5E + #646464 + diff --git a/demos/compose/src/main/res/values/strings.xml b/demos/compose/src/main/res/values/strings.xml new file mode 100644 index 0000000000..a11306db98 --- /dev/null +++ b/demos/compose/src/main/res/values/strings.xml @@ -0,0 +1,26 @@ + + + + Media3 Compose Demo + Current playlist + Click to view your play list + Added %1$s to playlist + Shuffle + Play + Waiting for playlist to load… + + "Without notification access the app can't warn about failed background operations" + diff --git a/demos/compose/src/main/res/values/themes.xml b/demos/compose/src/main/res/values/themes.xml new file mode 100644 index 0000000000..0918919117 --- /dev/null +++ b/demos/compose/src/main/res/values/themes.xml @@ -0,0 +1,33 @@ + + + + + + diff --git a/demos/composition/README.md b/demos/composition/README.md new file mode 100644 index 0000000000..9162757673 --- /dev/null +++ b/demos/composition/README.md @@ -0,0 +1,8 @@ +# Composition demo + +This app is an **EXPERIMENTAL** demo app created to explore the potential of `Composition` and `CompositionPlayer` APIs. It may exhibit limited features, occasional bugs, or unexpected behaviors. + +**Attention**: `CompositionPlayer` APIs should be taken as work in progress, rather than experimental API. Please await further announcement via [Release Notes](https://github.com/androidx/media/releases) for when the APIs are fully integrated. + +See the [demos README](../README.md) for instructions on how to build and run +this demo. diff --git a/demos/composition/build.gradle b/demos/composition/build.gradle new file mode 100644 index 0000000000..e26187cbd2 --- /dev/null +++ b/demos/composition/build.gradle @@ -0,0 +1,63 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +apply from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + namespace 'androidx.media3.demo.composition' + + compileSdk project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion 21 + targetSdkVersion project.ext.appTargetSdkVersion + multiDexEnabled true + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.txt' + signingConfig signingConfigs.debug + } + } + + lintOptions { + // This demo app isn't indexed and doesn't have translations. + disable 'GoogleAppIndexingWarning','MissingTranslation' + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:' + androidxMaterialVersion + implementation project(modulePrefix + 'lib-effect') + implementation project(modulePrefix + 'lib-exoplayer') + implementation project(modulePrefix + 'lib-exoplayer-dash') + implementation project(modulePrefix + 'lib-transformer') + implementation project(modulePrefix + 'lib-ui') + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.multidex:multidex:' + androidxMultidexVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion +} diff --git a/demos/composition/proguard-rules.txt b/demos/composition/proguard-rules.txt new file mode 100644 index 0000000000..cd85b36a6c --- /dev/null +++ b/demos/composition/proguard-rules.txt @@ -0,0 +1 @@ +# Proguard rules specific to the composition demo app. diff --git a/demos/composition/src/main/AndroidManifest.xml b/demos/composition/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a521883c12 --- /dev/null +++ b/demos/composition/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/composition/src/main/java/androidx/media3/demo/composition/AssetItemAdapter.java b/demos/composition/src/main/java/androidx/media3/demo/composition/AssetItemAdapter.java new file mode 100644 index 0000000000..620fdf6903 --- /dev/null +++ b/demos/composition/src/main/java/androidx/media3/demo/composition/AssetItemAdapter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.demo.composition; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import java.util.List; + +/** A {@link RecyclerView.Adapter} that displays assets in a sequence in a {@link RecyclerView}. */ +public final class AssetItemAdapter extends RecyclerView.Adapter { + private static final String TAG = "AssetItemAdapter"; + + private final List data; + + /** + * Creates a new instance + * + * @param data A list of items to populate RecyclerView with. + */ + public AssetItemAdapter(List data) { + this.data = data; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.preset_item, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + holder.getTextView().setText(data.get(position)); + } + + @Override + public int getItemCount() { + return data.size(); + } + + /** A {@link RecyclerView.ViewHolder} used to build {@link AssetItemAdapter}. */ + public static final class ViewHolder extends RecyclerView.ViewHolder { + private final TextView textView; + + private ViewHolder(View view) { + super(view); + textView = view.findViewById(R.id.preset_name_text); + } + + private TextView getTextView() { + return textView; + } + } +} diff --git a/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.java b/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.java new file mode 100644 index 0000000000..36a4ade823 --- /dev/null +++ b/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.java @@ -0,0 +1,326 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.demo.composition; + +import android.app.Activity; +import android.content.DialogInterface; +import android.os.Bundle; +import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.AppCompatButton; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.media3.common.Effect; +import androidx.media3.common.MediaItem; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.Player; +import androidx.media3.common.audio.SonicAudioProcessor; +import androidx.media3.common.util.Clock; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.Util; +import androidx.media3.effect.RgbFilter; +import androidx.media3.transformer.Composition; +import androidx.media3.transformer.CompositionPlayer; +import androidx.media3.transformer.EditedMediaItem; +import androidx.media3.transformer.EditedMediaItemSequence; +import androidx.media3.transformer.Effects; +import androidx.media3.transformer.ExportException; +import androidx.media3.transformer.ExportResult; +import androidx.media3.transformer.JsonUtil; +import androidx.media3.transformer.Transformer; +import androidx.media3.ui.PlayerView; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.google.common.base.Stopwatch; +import com.google.common.base.Ticker; +import com.google.common.collect.ImmutableList; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * An {@link Activity} that previews compositions, using {@link + * androidx.media3.transformer.CompositionPlayer}. + */ +public final class CompositionPreviewActivity extends AppCompatActivity { + private static final String TAG = "CompPreviewActivity"; + + private ArrayList sequenceAssetTitles; + private boolean[] selectedMediaItems; + private String[] presetDescriptions; + private AssetItemAdapter assetItemAdapter; + @Nullable private CompositionPlayer compositionPlayer; + @Nullable private Transformer transformer; + @Nullable private File outputFile; + private PlayerView playerView; + private AppCompatButton exportButton; + private AppCompatTextView exportInformationTextView; + private Stopwatch exportStopwatch; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.composition_preview_activity); + playerView = findViewById(R.id.composition_player_view); + + findViewById(R.id.preview_button).setOnClickListener(view -> previewComposition()); + findViewById(R.id.edit_sequence_button).setOnClickListener(view -> selectPreset()); + RecyclerView presetList = findViewById(R.id.composition_preset_list); + presetList.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); + LinearLayoutManager layoutManager = + new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, /* reverseLayout= */ false); + presetList.setLayoutManager(layoutManager); + + exportInformationTextView = findViewById(R.id.export_information_text); + exportButton = findViewById(R.id.composition_export_button); + exportButton.setOnClickListener(view -> exportComposition()); + + presetDescriptions = getResources().getStringArray(R.array.preset_descriptions); + // Select two media items by default. + selectedMediaItems = new boolean[presetDescriptions.length]; + selectedMediaItems[0] = true; + selectedMediaItems[2] = true; + sequenceAssetTitles = new ArrayList<>(); + for (int i = 0; i < selectedMediaItems.length; i++) { + if (selectedMediaItems[i]) { + sequenceAssetTitles.add(presetDescriptions[i]); + } + } + assetItemAdapter = new AssetItemAdapter(sequenceAssetTitles); + presetList.setAdapter(assetItemAdapter); + + exportStopwatch = + Stopwatch.createUnstarted( + new Ticker() { + @Override + public long read() { + return android.os.SystemClock.elapsedRealtimeNanos(); + } + }); + } + + @Override + protected void onStart() { + super.onStart(); + playerView.onResume(); + } + + @Override + protected void onStop() { + super.onStop(); + playerView.onPause(); + releasePlayer(); + cancelExport(); + exportStopwatch.reset(); + } + + private Composition prepareComposition() { + String[] presetUris = getResources().getStringArray(/* id= */ R.array.preset_uris); + int[] presetDurationsUs = getResources().getIntArray(/* id= */ R.array.preset_durations); + List mediaItems = new ArrayList<>(); + ImmutableList effects = + ImmutableList.of( + MatrixTransformationFactory.createDizzyCropEffect(), RgbFilter.createGrayscaleFilter()); + for (int i = 0; i < selectedMediaItems.length; i++) { + if (selectedMediaItems[i]) { + SonicAudioProcessor pitchChanger = new SonicAudioProcessor(); + pitchChanger.setPitch(mediaItems.size() % 2 == 0 ? 2f : 0.2f); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(presetUris[i]) + .setImageDurationMs(Util.usToMs(presetDurationsUs[i])) // Ignored for audio/video + .build(); + EditedMediaItem.Builder itemBuilder = + new EditedMediaItem.Builder(mediaItem) + .setEffects( + new Effects( + /* audioProcessors= */ ImmutableList.of(pitchChanger), + /* videoEffects= */ effects)) + .setDurationUs(presetDurationsUs[i]); + mediaItems.add(itemBuilder.build()); + } + } + EditedMediaItemSequence videoSequence = new EditedMediaItemSequence(mediaItems); + SonicAudioProcessor sampleRateChanger = new SonicAudioProcessor(); + sampleRateChanger.setOutputSampleRateHz(8_000); + return new Composition.Builder(/* sequences= */ ImmutableList.of(videoSequence)) + .setEffects( + new Effects( + /* audioProcessors= */ ImmutableList.of(sampleRateChanger), + /* videoEffects= */ ImmutableList.of())) + .build(); + } + + private void previewComposition() { + releasePlayer(); + Composition composition = prepareComposition(); + playerView.setPlayer(null); + + CompositionPlayer player = new CompositionPlayer.Builder(getApplicationContext()).build(); + this.compositionPlayer = player; + playerView.setPlayer(compositionPlayer); + playerView.setControllerAutoShow(false); + player.addListener( + new Player.Listener() { + @Override + public void onPlayerError(PlaybackException error) { + Toast.makeText(getApplicationContext(), "Preview error: " + error, Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "Preview error", error); + } + }); + player.setComposition(composition); + player.prepare(); + player.play(); + } + + private void selectPreset() { + new AlertDialog.Builder(/* context= */ this) + .setTitle(R.string.select_preset_title) + .setMultiChoiceItems(presetDescriptions, selectedMediaItems, this::selectPresetInDialog) + .setPositiveButton(android.R.string.ok, /* listener= */ null) + .setCancelable(false) + .create() + .show(); + } + + private void selectPresetInDialog(DialogInterface dialog, int which, boolean isChecked) { + selectedMediaItems[which] = isChecked; + // The items will be added to a the sequence in the order they were selected. + if (isChecked) { + sequenceAssetTitles.add(presetDescriptions[which]); + assetItemAdapter.notifyItemInserted(sequenceAssetTitles.size() - 1); + } else { + int index = sequenceAssetTitles.indexOf(presetDescriptions[which]); + sequenceAssetTitles.remove(presetDescriptions[which]); + assetItemAdapter.notifyItemRemoved(index); + } + } + + private void exportComposition() { + // Cancel and clean up files from any ongoing export. + cancelExport(); + + Composition composition = prepareComposition(); + + try { + outputFile = + createExternalCacheFile( + "composition-preview-" + Clock.DEFAULT.elapsedRealtime() + ".mp4"); + } catch (IOException e) { + Toast.makeText( + getApplicationContext(), + "Aborting export! Unable to create output file: " + e, + Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "Aborting export! Unable to create output file: ", e); + return; + } + String filePath = outputFile.getAbsolutePath(); + + transformer = + new Transformer.Builder(/* context= */ this) + .addListener( + new Transformer.Listener() { + @Override + public void onCompleted(Composition composition, ExportResult exportResult) { + exportStopwatch.stop(); + long elapsedTimeMs = exportStopwatch.elapsed(TimeUnit.MILLISECONDS); + String details = + getString(R.string.export_completed, elapsedTimeMs / 1000.f, filePath); + Log.i(TAG, details); + exportInformationTextView.setText(details); + + try { + JSONObject resultJson = + JsonUtil.exportResultAsJsonObject(exportResult) + .put("elapsedTimeMs", elapsedTimeMs) + .put("device", JsonUtil.getDeviceDetailsAsJsonObject()); + for (String line : Util.split(resultJson.toString(2), "\n")) { + Log.i(TAG, line); + } + } catch (JSONException e) { + Log.w(TAG, "Unable to convert exportResult to JSON", e); + } + } + + @Override + public void onError( + Composition composition, + ExportResult exportResult, + ExportException exportException) { + exportStopwatch.stop(); + Toast.makeText( + getApplicationContext(), + "Export error: " + exportException, + Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "Export error", exportException); + exportInformationTextView.setText(R.string.export_error); + } + }) + .build(); + + exportInformationTextView.setText(R.string.export_started); + exportStopwatch.reset(); + exportStopwatch.start(); + transformer.start(composition, filePath); + Log.i(TAG, "Export started"); + } + + private void releasePlayer() { + if (compositionPlayer != null) { + compositionPlayer.release(); + compositionPlayer = null; + } + } + + /** Cancels any ongoing export operation, and deletes output file contents. */ + private void cancelExport() { + if (transformer != null) { + transformer.cancel(); + transformer = null; + } + if (outputFile != null) { + outputFile.delete(); + outputFile = null; + } + exportInformationTextView.setText(""); + } + + /** + * Creates a {@link File} of the {@code fileName} in the application cache directory. + * + *

If a file of that name already exists, it is overwritten. + */ + // TODO: b/320636291 - Refactor duplicate createExternalCacheFile functions. + private File createExternalCacheFile(String fileName) throws IOException { + File file = new File(getExternalCacheDir(), fileName); + if (file.exists() && !file.delete()) { + throw new IOException("Could not delete file: " + file.getAbsolutePath()); + } + if (!file.createNewFile()) { + throw new IOException("Could not create file: " + file.getAbsolutePath()); + } + return file; + } +} diff --git a/demos/composition/src/main/java/androidx/media3/demo/composition/MatrixTransformationFactory.java b/demos/composition/src/main/java/androidx/media3/demo/composition/MatrixTransformationFactory.java new file mode 100644 index 0000000000..85cc61d19f --- /dev/null +++ b/demos/composition/src/main/java/androidx/media3/demo/composition/MatrixTransformationFactory.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.demo.composition; + +import android.graphics.Matrix; +import androidx.media3.common.C; +import androidx.media3.common.util.Util; +import androidx.media3.effect.GlMatrixTransformation; +import androidx.media3.effect.MatrixTransformation; + +/** + * Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link + * MatrixTransformation MatrixTransformations} that create video effects by applying transformation + * matrices to the individual video frames. + */ +/* package */ final class MatrixTransformationFactory { + /** + * Returns a {@link MatrixTransformation} that rescales the frames over the first {@link + * #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases + * linearly in size from a single point to filling the full output frame. + */ + public static MatrixTransformation createZoomInTransition() { + return MatrixTransformationFactory::calculateZoomInTransitionMatrix; + } + + /** + * Returns a {@link MatrixTransformation} that crops frames to a rectangle that moves on an + * ellipse. + */ + public static MatrixTransformation createDizzyCropEffect() { + return MatrixTransformationFactory::calculateDizzyCropMatrix; + } + + /** + * Returns a {@link GlMatrixTransformation} that rotates a frame in 3D around the y-axis and + * applies perspective projection to 2D. + */ + public static GlMatrixTransformation createSpin3dEffect() { + return MatrixTransformationFactory::calculate3dSpinMatrix; + } + + private static final float ZOOM_DURATION_SECONDS = 2f; + private static final float DIZZY_CROP_ROTATION_PERIOD_US = 5_000_000f; + + private static Matrix calculateZoomInTransitionMatrix(long presentationTimeUs) { + Matrix transformationMatrix = new Matrix(); + float scale = Math.min(1, presentationTimeUs / (C.MICROS_PER_SECOND * ZOOM_DURATION_SECONDS)); + transformationMatrix.postScale(/* sx= */ scale, /* sy= */ scale); + return transformationMatrix; + } + + private static android.graphics.Matrix calculateDizzyCropMatrix(long presentationTimeUs) { + double theta = presentationTimeUs * 2 * Math.PI / DIZZY_CROP_ROTATION_PERIOD_US; + float centerX = 0.5f * (float) Math.cos(theta); + float centerY = 0.5f * (float) Math.sin(theta); + android.graphics.Matrix transformationMatrix = new android.graphics.Matrix(); + transformationMatrix.postTranslate(/* dx= */ centerX, /* dy= */ centerY); + transformationMatrix.postScale(/* sx= */ 2f, /* sy= */ 2f); + return transformationMatrix; + } + + private static float[] calculate3dSpinMatrix(long presentationTimeUs) { + float[] transformationMatrix = new float[16]; + android.opengl.Matrix.frustumM( + transformationMatrix, + /* offset= */ 0, + /* left= */ -1f, + /* right= */ 1f, + /* bottom= */ -1f, + /* top= */ 1f, + /* near= */ 3f, + /* far= */ 5f); + android.opengl.Matrix.translateM( + transformationMatrix, /* mOffset= */ 0, /* x= */ 0f, /* y= */ 0f, /* z= */ -4f); + float theta = Util.usToMs(presentationTimeUs) / 10f; + android.opengl.Matrix.rotateM( + transformationMatrix, /* mOffset= */ 0, theta, /* x= */ 0f, /* y= */ 1f, /* z= */ 0f); + return transformationMatrix; + } +} diff --git a/demos/composition/src/main/java/androidx/media3/demo/composition/package-info.java b/demos/composition/src/main/java/androidx/media3/demo/composition/package-info.java new file mode 100644 index 0000000000..068f941e6b --- /dev/null +++ b/demos/composition/src/main/java/androidx/media3/demo/composition/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +@OptIn(markerClass = UnstableApi.class) +package androidx.media3.demo.composition; + +import androidx.annotation.OptIn; +import androidx.media3.common.util.NonNullApi; +import androidx.media3.common.util.UnstableApi; diff --git a/demos/composition/src/main/res/layout/composition_preview_activity.xml b/demos/composition/src/main/res/layout/composition_preview_activity.xml new file mode 100644 index 0000000000..83819ec920 --- /dev/null +++ b/demos/composition/src/main/res/layout/composition_preview_activity.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/composition/src/main/res/layout/preset_item.xml b/demos/composition/src/main/res/layout/preset_item.xml new file mode 100644 index 0000000000..41933efc0c --- /dev/null +++ b/demos/composition/src/main/res/layout/preset_item.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/demos/composition/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/composition/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..adaa93220e Binary files /dev/null and b/demos/composition/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/composition/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/composition/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..9b6f7d5e80 Binary files /dev/null and b/demos/composition/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/composition/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/composition/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..2101026c9f Binary files /dev/null and b/demos/composition/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/composition/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/composition/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..223ec8bd11 Binary files /dev/null and b/demos/composition/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/composition/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/composition/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..698ed68c42 Binary files /dev/null and b/demos/composition/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/composition/src/main/res/values-night/themes.xml b/demos/composition/src/main/res/values-night/themes.xml new file mode 100644 index 0000000000..5194fe967e --- /dev/null +++ b/demos/composition/src/main/res/values-night/themes.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/demos/composition/src/main/res/values/arrays.xml b/demos/composition/src/main/res/values/arrays.xml new file mode 100644 index 0000000000..780eeb5635 --- /dev/null +++ b/demos/composition/src/main/res/values/arrays.xml @@ -0,0 +1,77 @@ + + + + + 720p H264 video and AAC audio + 1080p H265 video and AAC audio + 360p H264 video and AAC audio + 360p VP8 video and Vorbis audio + 4K H264 video and AAC audio (portrait, no B-frames) + 8k H265 video and AAC audio + Short 1080p H265 video and AAC audio + Long 180p H264 video and AAC audio + H264 video and AAC audio (portrait, H > W, 0°) + H264 video and AAC audio (portrait, H < W, 90°) + SEF slow motion with 240 fps + 480p DASH (non-square pixels) + HDR (HDR10) H265 limited range video (encoding may fail) + HDR (HLG) H265 limited range video (encoding may fail) + 720p H264 video with no audio + London JPG image (plays for 5 secs at 30 fps) + Tokyo JPG image (portrait, plays for 5 secs at 30 fps) + Pixel 7 shorter audio track + + + https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4 + https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4 + https://html5demos.com/assets/dizzy.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/1920w_1080h_4s.mp4 + https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.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/gen/screens/dash-vod-single-segment/manifest-baseline.mpd + https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-s21-hdr-hdr10.mp4 + https://storage.googleapis.com/exoplayer-test-media-1/mp4/Pixel7Pro_HLG_1080P.mp4 + https://storage.googleapis.com/exoplayer-test-media-1/mp4/sample_video_track_only.mp4 + https://storage.googleapis.com/exoplayer-test-media-1/jpg/london.jpg + https://storage.googleapis.com/exoplayer-test-media-1/jpg/tokyo.jpg + https://storage.googleapis.com/exoplayer-temp/audio-blip/metronome_selfie_pixel.mp4 + + + 10024000 + 23823000 + 25000000 + 25000000 + 3745000 + 4421000 + 3923000 + 596459000 + 3687000 + 2235000 + 47987000 + 128270000 + 4236000 + 5167000 + 1001000 + 5000000 + 5000000 + 2170000 + + diff --git a/demos/composition/src/main/res/values/colors.xml b/demos/composition/src/main/res/values/colors.xml new file mode 100644 index 0000000000..91d1b1023f --- /dev/null +++ b/demos/composition/src/main/res/values/colors.xml @@ -0,0 +1,24 @@ + + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + diff --git a/demos/composition/src/main/res/values/strings.xml b/demos/composition/src/main/res/values/strings.xml new file mode 100644 index 0000000000..b31465ffc0 --- /dev/null +++ b/demos/composition/src/main/res/values/strings.xml @@ -0,0 +1,27 @@ + + + + Composition Demo + Edit + Preview + Single sequence preview + Single sequence items: + Choose preset input + Export + Export completed in %.3f seconds.\nOutput: %s + Export error + Export started + diff --git a/demos/composition/src/main/res/values/themes.xml b/demos/composition/src/main/res/values/themes.xml new file mode 100644 index 0000000000..29ccfdfc53 --- /dev/null +++ b/demos/composition/src/main/res/values/themes.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/demos/gl/build.gradle b/demos/gl/build.gradle index d66c72cf4e..5a19c6e796 100644 --- a/demos/gl/build.gradle +++ b/demos/gl/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'com.android.application' android { namespace 'androidx.media3.demo.gl' - compileSdkVersion project.ext.compileSdkVersion + compileSdk project.ext.compileSdkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 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 793e1fd493..89743d700d 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 @@ -63,7 +63,7 @@ public BitmapOverlayVideoProcessor(Context context) { paint = new Paint(); paint.setTextSize(64); paint.setAntiAlias(true); - paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF); + paint.setColor(Color.WHITE); textures = new int[1]; overlayBitmap = Bitmap.createBitmap(OVERLAY_WIDTH, OVERLAY_HEIGHT, Bitmap.Config.ARGB_8888); overlayCanvas = new Canvas(overlayBitmap); 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 c9e7ccd11c..bd6ba4b1d8 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 @@ -143,7 +143,7 @@ private void initializePlayer() { ? Assertions.checkNotNull(intent.getData()) : Uri.parse(DEFAULT_MEDIA_URI); DrmSessionManager drmSessionManager; - if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) { + if (intent.hasExtra(DRM_SCHEME_EXTRA)) { 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)); diff --git a/demos/main/build.gradle b/demos/main/build.gradle index e197b99d40..9250df865f 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -19,7 +19,7 @@ apply plugin: 'kotlin-android' android { namespace 'androidx.media3.demo.main' - compileSdkVersion project.ext.compileSdkVersion + compileSdk project.ext.compileSdkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -55,7 +55,9 @@ android { disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities' } - flavorDimensions "decoderExtensions" + flavorDimensions = ["decoderExtensions"] + + buildFeatures.buildConfig true productFlavors { noDecoderExtensions { diff --git a/demos/main/libs/doris-playback-service-3.9.2.aar b/demos/main/libs/doris-playback-service-3.9.2.aar deleted file mode 100644 index 82cef71692..0000000000 Binary files a/demos/main/libs/doris-playback-service-3.9.2.aar and /dev/null differ diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 8c373f6dcc..9dff590eab 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -217,37 +217,6 @@ } ] }, - { - "name": "Dice Test", - "samples": [ - { - "name": "webvtt 3.0, sandbox", - "uri": "https://dummy.imggaming.com/video?id=85158&live=&dash=&realm=dce.sandbox&usr=&pwd=", - "subtitle_uri": "https://dve-subtitles.imggaming.com/85158/127290/vtt/subtitle-hu-HU-199613-1697467810981.vtt", - "subtitle_mime_type": "text/vtt", - "subtitle_language": "hu" - }, - { - "name": "WWE live hls clear, sandbox", - "uri": "https://dummy.imggaming.com/video?id=111449&live=true&dash=&realm=dce.sandbox&usr=&pwd=" - }, - { - "name": "WWE live hls clear, wwe", - "uri": "https://dummy.imggaming.com/video?id=111449&live=true&dash=&realm=dce.wwe&usr=&pwd=" - }, - { - "name": "WWE vod hls clear, wwe", - "uri": "https://dummy.imggaming.com/video?id=530182&live=&dash=&realm=dce.wwe&usr=&pwd=", - "subtitle_uri": "https://dve-subtitles.imggaming.com/530182/741299/vtt/subtitle-en-US-199647-1697490152197.vtt", - "subtitle_mime_type": "text/vtt", - "subtitle_language": "en" - }, - { - "name": "450830 vod dash DRM, adjara", - "uri": "https://dummy.imggaming.com/video?id=450830&live=&dash=true&realm=dce.adjara&usr=&pwd=" - } - ] - }, { "name": "Clear DASH", "samples": [ @@ -861,6 +830,27 @@ "clip_start_position_ms": 10000 } ] + }, + { + "name": "Image -> Video -> Image -> Image", + "playlist": [ + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/tokyo.jpg", + "image_duration_ms": 2000 + }, + { + "uri": "https://html5demos.com/assets/dizzy.mp4", + "clip_end_position_ms": 2000 + }, + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/london.jpg", + "image_duration_ms": 2000 + }, + { + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/tokyo.jpg", + "image_duration_ms": 2000 + } + ] } ] }, @@ -930,6 +920,13 @@ "subtitle_mime_type": "text/vtt", "subtitle_language": "en" }, + { + "name": "WebVTT from hidive", + "uri": "https://html5demos.com/assets/dizzy.mp4", + "subtitle_uri": "https://dve-subtitles.imggaming.com/560967/776847/vtt/subtitle-en-US-245273-1712171888041.vtt", + "subtitle_mime_type": "text/vtt", + "subtitle_language": "en" + }, { "name": "WebVTT Japanese features", "uri": "https://html5demos.com/assets/dizzy.mp4", @@ -966,7 +963,7 @@ ] }, { - "name": "Misc", + "name": "Progressive", "samples": [ { "name": "Dizzy (MP4)", @@ -1021,5 +1018,39 @@ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4" } ] + }, + { + "name": "Images", + "samples": [ + { + "name": "JPEG (wide)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/london.jpg", + "image_duration_ms": 2000 + }, + { + "name": "JPEG (tall)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/tokyo.jpg", + "image_duration_ms": 2000 + }, + { + "name": "PNG", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/png/media3test.png", + "image_duration_ms": 2000 + }, + { + "name": "JPEG motion photo (still)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/london_motion_photo.jpg", + "image_duration_ms": 2000 + }, + { + "name": "JPEG motion photo (motion)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/london_motion_photo.jpg" + }, + { + "name": "JPEG (Ultra HDR)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/jpg/ultra_hdr.jpg", + "image_duration_ms": 2000 + } + ] } ] 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 c67f59b94c..53794ab9ff 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,15 @@ package androidx.media3.demo.main; import android.content.Context; +import android.net.http.HttpEngine; +import android.os.Build; 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.HttpEngineDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.NoOpCacheEvictor; @@ -51,25 +54,36 @@ public final class DemoUtil { public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"; /** - * Whether the demo application uses Cronet for networking. Note that Cronet does not provide - * automatic support for cookies (https://github.com/google/ExoPlayer/issues/5975). + * Whether the demo application uses Cronet for networking when {@link HttpEngine} is not + * supported. Note that Cronet does not provide automatic support for cookies + * (https://github.com/google/ExoPlayer/issues/5975). * - *

If set to false, the platform's default network stack is used with a {@link CookieManager} - * configured in {@link #getHttpDataSourceFactory}. + *

If set to false, the {@link DefaultHttpDataSource} is used with a {@link CookieManager} + * configured in {@link #getHttpDataSourceFactory} when {@link HttpEngine} is not supported. */ - private static final boolean USE_CRONET_FOR_NETWORKING = true; - private static final boolean USE_OKHTTP_FOR_NETWORKING = true; + private static final boolean ALLOW_CRONET_FOR_NETWORKING = true; + private static final boolean ALLOW_OKHTTP_FOR_NETWORKING = true; private static final String TAG = "DemoUtil"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; private static DataSource.@MonotonicNonNull Factory dataSourceFactory; private static DataSource.@MonotonicNonNull Factory httpDataSourceFactory; + + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static @MonotonicNonNull DatabaseProvider databaseProvider; + private static @MonotonicNonNull File downloadDirectory; + + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static @MonotonicNonNull Cache downloadCache; + + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static @MonotonicNonNull DownloadManager downloadManager; + private static @MonotonicNonNull DownloadTracker downloadTracker; + + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static @MonotonicNonNull DownloadNotificationHelper downloadNotificationHelper; /** Returns whether extension renderers should be used. */ @@ -91,28 +105,37 @@ public static RenderersFactory buildRenderersFactory( .setExtensionRendererMode(extensionRendererMode); } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) { - if (httpDataSourceFactory == null) { - if (USE_OKHTTP_FOR_NETWORKING) { - OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder() - .addNetworkInterceptor(new StethoInterceptor()); - httpDataSourceFactory = new OkHttpDataSource.Factory((Call.Factory) okHttpBuilder.build()); - } else if (USE_CRONET_FOR_NETWORKING) { - context = context.getApplicationContext(); - @Nullable CronetEngine cronetEngine = CronetUtil.buildCronetEngine(context); - if (cronetEngine != null) { - httpDataSourceFactory = - new CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor()); - } - } - if (httpDataSourceFactory == null) { - // We don't want to use Cronet, or we failed to instantiate a CronetEngine. - CookieManager cookieManager = new CookieManager(); - cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); - CookieHandler.setDefault(cookieManager); - httpDataSourceFactory = new DefaultHttpDataSource.Factory(); + if (httpDataSourceFactory != null) { + return httpDataSourceFactory; + } + context = context.getApplicationContext(); + if (Build.VERSION.SDK_INT >= 34) { + HttpEngine httpEngine = new HttpEngine.Builder(context).build(); + httpDataSourceFactory = + new HttpEngineDataSource.Factory(httpEngine, Executors.newSingleThreadExecutor()); + return httpDataSourceFactory; + } + if (ALLOW_OKHTTP_FOR_NETWORKING) { + OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder() + .addNetworkInterceptor(new StethoInterceptor()); + httpDataSourceFactory = new OkHttpDataSource.Factory((Call.Factory) okHttpBuilder.build()); + return httpDataSourceFactory; + } else if (ALLOW_CRONET_FOR_NETWORKING) { + @Nullable CronetEngine cronetEngine = CronetUtil.buildCronetEngine(context); + if (cronetEngine != null) { + httpDataSourceFactory = + new CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor()); + return httpDataSourceFactory; } } + // The device doesn't support HttpEngine or we don't want to allow Cronet, or we failed to + // instantiate a CronetEngine. + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); + CookieHandler.setDefault(cookieManager); + httpDataSourceFactory = new DefaultHttpDataSource.Factory(); return httpDataSourceFactory; } @@ -137,6 +160,7 @@ public static synchronized DownloadNotificationHelper getDownloadNotificationHel return downloadNotificationHelper; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public static synchronized DownloadManager getDownloadManager(Context context) { ensureDownloadManagerInitialized(context); return downloadManager; 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 af32eb7065..2a322a6a47 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 @@ -16,15 +16,16 @@ package androidx.media3.demo.main; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import android.content.Context; import android.content.DialogInterface; import android.net.Uri; -import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; 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.C; import androidx.media3.common.DrmInitData; @@ -54,6 +55,9 @@ import java.util.HashMap; import java.util.UUID; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; /** Tracks media that has been downloaded. */ @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) @@ -182,7 +186,7 @@ public void release() { trackSelectionDialog.dismiss(); } if (widevineOfflineLicenseFetchTask != null) { - widevineOfflineLicenseFetchTask.cancel(false); + widevineOfflineLicenseFetchTask.cancel(); } } @@ -197,12 +201,7 @@ public void onPrepared(DownloadHelper helper) { } // The content is DRM protected. We need to acquire an offline license. - if (Util.SDK_INT < 18) { - Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG) - .show(); - Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18"); - return; - } + // TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest. if (!hasNonNullWidevineSchemaData(format.drmInitData)) { Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG) @@ -357,15 +356,16 @@ private DownloadRequest buildDownloadRequest() { } /** Downloads a Widevine offline license in a background thread. */ - @RequiresApi(18) - private static final class WidevineOfflineLicenseFetchTask extends AsyncTask { + private static final class WidevineOfflineLicenseFetchTask { private final Format format; private final MediaItem.DrmConfiguration drmConfiguration; private final DataSource.Factory dataSourceFactory; private final StartDownloadDialogHelper dialogHelper; private final DownloadHelper downloadHelper; + private final ExecutorService executorService; + @Nullable Future future; @Nullable private byte[] keySetId; @Nullable private DrmSession.DrmSessionException drmSessionException; @@ -375,6 +375,8 @@ public WidevineOfflineLicenseFetchTask( DataSource.Factory dataSourceFactory, StartDownloadDialogHelper dialogHelper, DownloadHelper downloadHelper) { + checkState(drmConfiguration.scheme.equals(C.WIDEVINE_UUID)); + this.executorService = Executors.newSingleThreadExecutor(); this.format = format; this.drmConfiguration = drmConfiguration; this.dataSourceFactory = dataSourceFactory; @@ -382,32 +384,41 @@ public WidevineOfflineLicenseFetchTask( this.downloadHelper = downloadHelper; } - @Override - protected Void doInBackground(Void... voids) { - OfflineLicenseHelper offlineLicenseHelper = - OfflineLicenseHelper.newWidevineInstance( - drmConfiguration.licenseUri.toString(), - drmConfiguration.forceDefaultLicenseUri, - dataSourceFactory, - drmConfiguration.licenseRequestHeaders, - new DrmSessionEventListener.EventDispatcher()); - try { - keySetId = offlineLicenseHelper.downloadLicense(format); - } catch (DrmSession.DrmSessionException e) { - drmSessionException = e; - } finally { - offlineLicenseHelper.release(); + public void cancel() { + if (future != null) { + future.cancel(/* mayInterruptIfRunning= */ false); } - return null; } - @Override - protected void onPostExecute(Void aVoid) { - if (drmSessionException != null) { - dialogHelper.onOfflineLicenseFetchedError(drmSessionException); - } else { - dialogHelper.onOfflineLicenseFetched(downloadHelper, checkNotNull(keySetId)); - } + public void execute() { + future = + executorService.submit( + () -> { + OfflineLicenseHelper offlineLicenseHelper = + OfflineLicenseHelper.newWidevineInstance( + drmConfiguration.licenseUri.toString(), + drmConfiguration.forceDefaultLicenseUri, + dataSourceFactory, + drmConfiguration.licenseRequestHeaders, + new DrmSessionEventListener.EventDispatcher()); + try { + keySetId = offlineLicenseHelper.downloadLicense(format); + } catch (DrmSession.DrmSessionException e) { + drmSessionException = e; + } finally { + offlineLicenseHelper.release(); + new Handler(Looper.getMainLooper()) + .post( + () -> { + if (drmSessionException != null) { + dialogHelper.onOfflineLicenseFetchedError(drmSessionException); + } else { + dialogHelper.onOfflineLicenseFetched( + downloadHelper, checkNotNull(keySetId)); + } + }); + } + }); } } } diff --git a/demos/main/src/main/java/androidx/media3/demo/main/upstream/FetchUtil.java b/demos/main/src/main/java/androidx/media3/demo/main/FetchUtil.java similarity index 83% rename from demos/main/src/main/java/androidx/media3/demo/main/upstream/FetchUtil.java rename to demos/main/src/main/java/androidx/media3/demo/main/FetchUtil.java index 66625eb0a2..0d8d534799 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/upstream/FetchUtil.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/FetchUtil.java @@ -1,7 +1,7 @@ -package androidx.media3.demo.main.upstream; +package androidx.media3.demo.main; -import android.text.TextUtils; -import com.diceplatform.doris.sdk.playback.internal.HttpService; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParser; import io.reactivex.rxjava3.core.Observable; import java.io.IOException; import java.util.Map; @@ -17,7 +17,10 @@ public final class FetchUtil { - private static final long DEFAULT_HTTP_TIMEOUT_MS = 10_000; + public static final String CONTENT_TYPE_JSON = "application/json"; + public static final MediaType MEDIA_TYPE_JSON = MediaType.get(CONTENT_TYPE_JSON); + public static final long DEFAULT_HTTP_TIMEOUT_MS = 10_000; + private static final OkHttpClient client = client(); private FetchUtil() { @@ -62,13 +65,8 @@ public static RequestData from(String url, Map headers, String b return from(url, headers, body, null); } - public static RequestData from(String url, Map headers, String body, - MediaType bodyType) { - if (TextUtils.isEmpty(body)) { - return new RequestData(url, headers, null, null); - } - return new RequestData(url, headers, body, - bodyType == null ? HttpService.MEDIA_TYPE_JSON : bodyType); + public static RequestData from(String url, Map headers, String body, MediaType bodyType) { + return new RequestData(url, headers, body, bodyType == null ? MEDIA_TYPE_JSON : bodyType); } } @@ -77,7 +75,6 @@ public static OkHttpClient client() { .connectTimeout(DEFAULT_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS) .readTimeout(DEFAULT_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS) .writeTimeout(DEFAULT_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS) - // .addInterceptor(new HttpService.ResponseBodyInterceptor()) .build(); } @@ -126,4 +123,12 @@ public void onResponse(@NotNull Call call, @NotNull Response response) { }); }); } + + public static String generateJson(Object obj) { + return new GsonBuilder().setPrettyPrinting().create().toJson(obj); + } + + public static String prettyJson(String json) { + return generateJson(JsonParser.parseString(json)); + } } \ No newline at end of file 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 5369029b91..fffd60c887 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 @@ -22,12 +22,15 @@ import android.content.Intent; import android.net.Uri; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.media3.common.C; 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.Player; import androidx.media3.common.endeavor.WebUtil; +import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; import java.util.ArrayList; @@ -54,6 +57,7 @@ public class IntentUtil { public static final String MIME_TYPE_EXTRA = "mime_type"; public static final String CLIP_START_POSITION_MS_EXTRA = "clip_start_position_ms"; public static final String CLIP_END_POSITION_MS_EXTRA = "clip_end_position_ms"; + public static final String IMAGE_DURATION_MS = "image_duration_ms"; public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; @@ -67,6 +71,21 @@ public class IntentUtil { public static final String SUBTITLE_URI_EXTRA = "subtitle_uri"; public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type"; public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language"; + public static final String REPEAT_MODE_EXTRA = "repeat_mode"; + + public static @Player.RepeatMode int parseRepeatModeExtra(String repeatMode) { + switch (repeatMode) { + case "OFF": + return Player.REPEAT_MODE_OFF; + case "ONE": + return Player.REPEAT_MODE_ONE; + case "ALL": + return Player.REPEAT_MODE_ALL; + default: + throw new IllegalArgumentException( + "Argument " + repeatMode + " does not match any of the repeat modes: OFF|ONE|ALL"); + } + } /** Creates a list of {@link MediaItem media items} from an {@link Intent}. */ public static List createMediaItemsFromIntent(Intent intent) { @@ -115,6 +134,7 @@ public static void addToIntent(List mediaItems, Intent intent) { } } + @OptIn(markerClass = UnstableApi.class) // Setting image duration. private static MediaItem createMediaItemFromIntent( Uri uri, Intent intent, String extrasKeySuffix) { @Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix); @@ -123,6 +143,7 @@ private static MediaItem createMediaItemFromIntent( @Nullable SubtitleConfiguration subtitleConfiguration = createSubtitleConfiguration(intent, extrasKeySuffix); + long imageDurationMs = intent.getLongExtra(IMAGE_DURATION_MS + extrasKeySuffix, C.TIME_UNSET); MediaItem.Builder builder = new MediaItem.Builder() .setUri(uri) @@ -135,7 +156,8 @@ private static MediaItem createMediaItemFromIntent( .setEndPositionMs( intent.getLongExtra( CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE)) - .build()); + .build()) + .setImageDurationMs(imageDurationMs); if (adTagUri != null) { builder.setAdsConfiguration( new MediaItem.AdsConfiguration.Builder(Uri.parse(adTagUri)).build()); @@ -200,6 +222,7 @@ private static MediaItem.Builder populateDrmPropertiesFromIntent( return builder; } + @OptIn(markerClass = UnstableApi.class) // Accessing image duration. private static void addLocalConfigurationToIntent( MediaItem.LocalConfiguration localConfiguration, Intent intent, String extrasKeySuffix) { intent @@ -220,6 +243,9 @@ private static void addLocalConfigurationToIntent( intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, subtitleConfiguration.mimeType); intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, subtitleConfiguration.language); } + if (localConfiguration.imageDurationMs != C.TIME_UNSET) { + intent.putExtra(IMAGE_DURATION_MS + extrasKeySuffix, localConfiguration.imageDurationMs); + } } private static void addDrmConfigurationToIntent( 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 857efd4ddf..6be37323bd 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 @@ -93,11 +93,8 @@ public class PlayerActivity extends AppCompatActivity @Nullable private AdsLoader clientSideAdsLoader; - @OptIn(markerClass = UnstableApi.class) - @Nullable - private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader; + @Nullable private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader; - @OptIn(markerClass = UnstableApi.class) private ImaServerSideAdInsertionMediaSource.AdsLoader.@MonotonicNonNull State serverSideAdsLoaderState; @@ -262,8 +259,8 @@ protected void setContentView() { * @return Whether initialization was successful. */ protected boolean initializePlayer() { + Intent intent = getIntent(); if (player == null) { - Intent intent = getIntent(); mediaItems = createMediaItems(intent); if (mediaItems.isEmpty()) { @@ -293,11 +290,15 @@ protected boolean initializePlayer() { } player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition); player.prepare(); + String repeatModeExtra = intent.getStringExtra(IntentUtil.REPEAT_MODE_EXTRA); + if (repeatModeExtra != null) { + player.setRepeatMode(IntentUtil.parseRepeatModeExtra(repeatModeExtra)); + } updateButtonVisibility(); return true; } - @OptIn(markerClass = UnstableApi.class) // SSAI configuration + @OptIn(markerClass = UnstableApi.class) // DRM configuration private MediaSource.Factory createMediaSourceFactory() { DefaultDrmSessionManagerProvider drmSessionManagerProvider = new DefaultDrmSessionManagerProvider(); @@ -330,7 +331,6 @@ private void setRenderersFactory( playerBuilder.setRenderersFactory(renderersFactory); } - @OptIn(markerClass = UnstableApi.class) private void configurePlayerWithServerSideAdsLoader() { serverSideAdsLoader.setPlayer(player); } @@ -361,11 +361,7 @@ private List createMediaItems(Intent intent) { MediaItem.DrmConfiguration drmConfiguration = mediaItem.localConfiguration.drmConfiguration; if (drmConfiguration != null) { - if (Build.VERSION.SDK_INT < 18) { - showToast(R.string.error_drm_unsupported_before_api_18); - finish(); - return Collections.emptyList(); - } else if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmConfiguration.scheme)) { + if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmConfiguration.scheme)) { showToast(R.string.error_drm_unsupported_scheme); finish(); return Collections.emptyList(); @@ -403,7 +399,6 @@ protected void releasePlayer() { } } - @OptIn(markerClass = UnstableApi.class) private void releaseServerSideAdsLoader() { serverSideAdsLoaderState = serverSideAdsLoader.release(); serverSideAdsLoader = null; @@ -417,20 +412,17 @@ private void releaseClientSideAdsLoader() { } } - @OptIn(markerClass = UnstableApi.class) private void saveServerSideAdsLoaderState(Bundle outState) { if (serverSideAdsLoaderState != null) { outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle()); } } - @OptIn(markerClass = UnstableApi.class) private void restoreServerSideAdsLoaderState(Bundle savedInstanceState) { Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE); if (adsLoaderStateBundle != null) { serverSideAdsLoaderState = - ImaServerSideAdInsertionMediaSource.AdsLoader.State.CREATOR.fromBundle( - adsLoaderStateBundle); + ImaServerSideAdInsertionMediaSource.AdsLoader.State.fromBundle(adsLoaderStateBundle); } } @@ -514,7 +506,7 @@ public void onTracksChanged(Tracks tracks) { private class PlayerErrorMessageProvider implements ErrorMessageProvider { - @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) + @OptIn(markerClass = UnstableApi.class) // Using decoder exceptions @Override public Pair getErrorMessage(PlaybackException e) { String errorString = getString(R.string.error_generic); @@ -555,7 +547,7 @@ private static List createMediaItems(Intent intent, DownloadTracker d return mediaItems; } - @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) + @OptIn(markerClass = UnstableApi.class) // Using Download API private static MediaItem maybeSetDownloadProperties( MediaItem item, @Nullable DownloadRequest downloadRequest) { if (downloadRequest == null) { 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 da5c20289d..2c32835811 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 @@ -26,9 +26,10 @@ import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.net.Uri; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.text.TextUtils; import android.util.JsonReader; import android.view.Menu; @@ -48,17 +49,17 @@ import androidx.annotation.OptIn; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; +import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem.ClippingConfiguration; import androidx.media3.common.MediaMetadata; import androidx.media3.common.util.Log; +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.DataSourceUtil; import androidx.media3.datasource.DataSpec; -import androidx.media3.demo.main.upstream.FetchUtil; -import androidx.media3.demo.main.upstream.PlaybackProvider; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.offline.DownloadService; import com.facebook.stetho.Stetho; @@ -71,6 +72,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -78,6 +80,8 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** An activity for selecting from a list of media samples. */ public class SampleChooserActivity extends AppCompatActivity @@ -124,6 +128,7 @@ public void onCreate(Bundle savedInstanceState) { } } } catch (IOException e) { + Log.e(TAG, "One or more sample lists failed to load", e); Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) .show(); } @@ -209,7 +214,7 @@ private void loadSample() { } private void loadOfficeSample() { - String url = "http://172.16.0.108:8899/exoplayer/media.exolist.json"; + String url = "http://172.16.0.108:8899/exoplayer/media.exolist.fake.json"; FetchUtil.fetch(url) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -264,31 +269,6 @@ public boolean onChildClick( intent.putExtra( IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, isNonNullAndChecked(preferExtensionDecodersMenuItem)); - if (playlistHolder.mediaItems.size() == 1) { - Uri originUri = playlistHolder.mediaItems.get(0).localConfiguration.uri; - PlaybackProvider.getInstance().getStream(originUri) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - List mediaItems = playlistHolder.mediaItems; - if (result != null && result.localConfiguration != null) { - Log.i(TAG, "fetch video from backend - " + originUri.getQuery() + " >> " + result.localConfiguration.uri); - if (playlistHolder.mediaItems.get(0).localConfiguration.subtitleConfigurations != null) { - result = result.buildUpon() - .setSubtitleConfigurations(playlistHolder.mediaItems.get(0).localConfiguration.subtitleConfigurations) - .build(); - } - mediaItems = Collections.singletonList(result); - } - IntentUtil.addToIntent(mediaItems, intent); - startActivity(intent); - }, - error -> { - Log.e(TAG, "fail to fetch video from backend - " + originUri.getQuery(), error); - IntentUtil.addToIntent(playlistHolder.mediaItems, intent); - startActivity(intent); - }); - return true; - } IntentUtil.addToIntent(playlistHolder.mediaItems, intent); startActivity(intent); return true; @@ -311,6 +291,7 @@ && checkSelfPermission(Api33.getPostNotificationPermissionString()) } } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private void toggleDownload(MediaItem mediaItem) { RenderersFactory renderersFactory = DemoUtil.buildRenderersFactory( @@ -327,6 +308,10 @@ private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) { if (localConfiguration.adsConfiguration != null) { return R.string.download_ads_unsupported; } + @Nullable MediaItem.DrmConfiguration drmConfiguration = localConfiguration.drmConfiguration; + if (drmConfiguration != null && !drmConfiguration.scheme.equals(C.WIDEVINE_UUID)) { + return R.string.download_only_widevine_drm_supported; + } String scheme = localConfiguration.uri.getScheme(); if (!("http".equals(scheme) || "https".equals(scheme))) { return R.string.download_scheme_unsupported; @@ -339,34 +324,43 @@ private static boolean isNonNullAndChecked(@Nullable MenuItem menuItem) { return menuItem != null && menuItem.isChecked(); } - private final class SampleListLoader extends AsyncTask> { + private final class SampleListLoader { + + private final ExecutorService executorService; private boolean sawError; - @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) - @Override - protected List doInBackground(String... uris) { - List result = new ArrayList<>(); - Context context = getApplicationContext(); - DataSource dataSource = DemoUtil.getDataSourceFactory(context).createDataSource(); - for (String uri : uris) { - DataSpec dataSpec = new DataSpec(Uri.parse(uri)); - InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); - try { - readPlaylistGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result); - } catch (Exception e) { - Log.e(TAG, "Error loading sample list: " + uri, e); - sawError = true; - } finally { - DataSourceUtil.closeQuietly(dataSource); - } - } - return result; + public SampleListLoader() { + executorService = Executors.newSingleThreadExecutor(); } - @Override - protected void onPostExecute(List result) { - onPlaylistGroups(result, sawError); + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) + public void execute(String... uris) { + executorService.execute( + () -> { + List result = new ArrayList<>(); + Context context = getApplicationContext(); + DataSource dataSource = DemoUtil.getDataSourceFactory(context).createDataSource(); + for (String uri : uris) { + DataSpec dataSpec = new DataSpec(Uri.parse(uri)); + InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); + try { + readPlaylistGroups( + new JsonReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)), + result); + } catch (Exception e) { + Log.e(TAG, "Error loading sample list: " + uri, e); + sawError = true; + } finally { + DataSourceUtil.closeQuietly(dataSource); + } + } + new Handler(Looper.getMainLooper()) + .post( + () -> { + onPlaylistGroups(result, sawError); + }); + }); } private void readPlaylistGroups(JsonReader reader, List groups) @@ -410,6 +404,7 @@ private void readPlaylistGroup(JsonReader reader, List groups) group.playlists.addAll(playlistHolders); } + @OptIn(markerClass = UnstableApi.class) // Setting image duration. private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) throws IOException { Uri uri = null; String extension = null; @@ -447,6 +442,9 @@ private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) thro case "clip_end_position_ms": clippingConfiguration.setEndPositionMs(reader.nextLong()); break; + case "image_duration_ms": + mediaItem.setImageDurationMs(reader.nextLong()); + break; case "ad_tag_uri": mediaItem.setAdsConfiguration( new MediaItem.AdsConfiguration.Builder(Uri.parse(reader.nextString())).build()); 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 e82b600354..f7efa935ec 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 @@ -67,7 +67,8 @@ public interface TrackSelectionListener { } public static final ImmutableList SUPPORTED_TRACK_TYPES = - ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT); + ImmutableList.of( + C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT, C.TRACK_TYPE_IMAGE); private final SparseArray tabFragments; private final ArrayList tabTrackTypes; @@ -266,11 +267,13 @@ public View onCreateView( 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); + return resources.getString(R.string.track_selection_title_video); case C.TRACK_TYPE_AUDIO: - return resources.getString(R.string.exo_track_selection_title_audio); + return resources.getString(R.string.track_selection_title_audio); case C.TRACK_TYPE_TEXT: - return resources.getString(R.string.exo_track_selection_title_text); + return resources.getString(R.string.track_selection_title_text); + case C.TRACK_TYPE_IMAGE: + return resources.getString(R.string.track_selection_title_image); default: throw new IllegalArgumentException(); } diff --git a/demos/main/src/main/java/androidx/media3/demo/main/upstream/PlaybackProvider.java b/demos/main/src/main/java/androidx/media3/demo/main/upstream/PlaybackProvider.java deleted file mode 100644 index cd02285e21..0000000000 --- a/demos/main/src/main/java/androidx/media3/demo/main/upstream/PlaybackProvider.java +++ /dev/null @@ -1,118 +0,0 @@ -package androidx.media3.demo.main.upstream; - -import android.net.Uri; -import android.text.TextUtils; -import android.util.Pair; -import androidx.annotation.NonNull; -import androidx.media3.common.C; -import androidx.media3.common.MediaItem; -import androidx.media3.demo.main.upstream.setting.Media; -import androidx.media3.demo.main.upstream.setting.Realm; -import com.diceplatform.doris.sdk.playback.PlaybackService; -import com.diceplatform.doris.sdk.playback.PlaybackServiceImpl; -import com.diceplatform.doris.sdk.playback.PlaybackServicePolicy; -import com.diceplatform.doris.sdk.playback.internal.HttpResponse.DrmInfo; -import com.diceplatform.doris.sdk.playback.internal.HttpResponse.LiveStreamInfo; -import com.diceplatform.doris.sdk.playback.internal.HttpResponse.PlaybackData; -import com.diceplatform.doris.sdk.playback.internal.HttpResponse.VodStreamInfo; -import io.reactivex.rxjava3.core.Observable; -import java.util.HashMap; - -public class PlaybackProvider { - - private static class SingletonHolder { - - private static final PlaybackProvider instance = new PlaybackProvider(); - } - - public static PlaybackProvider getInstance() { - return SingletonHolder.instance; - } - - private final PlaybackService playbackService; - - private PlaybackProvider() { - playbackService = new PlaybackServiceImpl(); - } - - // Asynchronous methods. - - public Observable getStream(@NonNull Uri uri) { - Media media = buildRequestMedia(uri); - if (media == null) { - return Observable.just(buildResponseMedia(null)); - } - boolean useDash = "true".equals(uri.getQueryParameter("dash")); - if (media.isLive()) { - return playbackService.fetchRealmConfig(media.getRealm().getName(), media.getRealm().getEnv()) - .flatMap(realm -> playbackService.login(media.getRealm().getUsername(), media.getRealm().getPassword(), realm) - .flatMap(credential -> playbackService.fetchLiveDetail(media.getId(), null, credential, realm) - .map(detail -> buildResponseMedia(detail == null ? null : getLiveStream(useDash, detail.stream))))); - } - return playbackService.fetchRealmConfig(media.getRealm().getName(), media.getRealm().getEnv()) - .flatMap(realm -> playbackService.login(media.getRealm().getUsername(), media.getRealm().getPassword(), realm) - .flatMap(credential -> playbackService.fetchVodDetail(media.getId(), null, credential, realm) - .map(detail -> buildResponseMedia(detail == null ? null : getVodStream(useDash, detail.stream))))); - } - - // Synchronous methods. - - public MediaItem getStreamSync(@NonNull Uri uri) { - return getStream(uri).blockingFirst(); - } - - // Internal methods. - - private Media buildRequestMedia(Uri uri) { - if (uri == null || !Media.GAME_DOMAIN.equals(uri.getHost())) { - return null; - } - Media.Builder builder = new Media.Builder(); - int id = Integer.parseInt(uri.getQueryParameter("id")); - String realm = uri.getQueryParameter("realm"); - String usr = uri.getQueryParameter("usr"); - String pwd = uri.getQueryParameter("pwd"); - boolean live = "true".equals(uri.getQueryParameter("live")); - if (id > 0 && !TextUtils.isEmpty(realm) && !TextUtils.isEmpty(usr) && !TextUtils.isEmpty(pwd)) { - builder.setId(id).setLive(live).setRealm(new Realm(realm).setUsername(usr).setPassword(pwd)); - } - return builder.build(); - } - - private MediaItem buildResponseMedia(PlaybackData result) { - MediaItem.Builder builder = new MediaItem.Builder(); - if (!PlaybackData.isEmpty(result)) { - builder.setUri(result.url); - if (!DrmInfo.isEmpty(result.drm)) { - HashMap requestHeaders = new HashMap<>(); - requestHeaders.put("Authorization", "Bearer " + result.drm.jwtToken); - MediaItem.DrmConfiguration drmConfiguration = new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID) - .setLicenseUri(result.drm.url) - .setLicenseRequestHeaders(requestHeaders) - .build(); - builder.setDrmConfiguration(drmConfiguration); - } - } - return builder.build(); - } - - private PlaybackData getVodStream(boolean useDash, VodStreamInfo result) { - VodStreamInfo vod = VodStreamInfo.isEmpty(result) ? null : new VodStreamInfo( - useDash ? null : result.hls, - useDash ? null : result.hlsWidevine, - useDash ? result.dash : null, - result.annotations, result.watermark, result.skipMarkers); - Pair playback = PlaybackServicePolicy.getPreferredPlayback(vod); - return playback == null ? null : playback.second; - } - - private PlaybackData getLiveStream(boolean useDash, LiveStreamInfo result) { - LiveStreamInfo live = LiveStreamInfo.isEmpty(result) ? null : new LiveStreamInfo( - result.eventId, - useDash ? null : result.hls, - useDash ? null : result.hlsWidevine, - useDash ? result.dash : null); - Pair playback = PlaybackServicePolicy.getPreferredPlayback(live); - return playback == null ? null : playback.second; - } -} \ No newline at end of file diff --git a/demos/main/src/main/java/androidx/media3/demo/main/upstream/setting/Media.java b/demos/main/src/main/java/androidx/media3/demo/main/upstream/setting/Media.java deleted file mode 100644 index 021bc21260..0000000000 --- a/demos/main/src/main/java/androidx/media3/demo/main/upstream/setting/Media.java +++ /dev/null @@ -1,75 +0,0 @@ -package androidx.media3.demo.main.upstream.setting; - -import androidx.annotation.NonNull; - -public class Media { - - public static final String GAME_DOMAIN = "dummy.imggaming.com"; - - public static final class Builder { - - private int id; - private boolean isLive; - @NonNull private Realm realm = Realm.DCE_SANDBOX; - - public Builder() { - } - - private Builder(Media media) { - id = media.getId(); - isLive = media.isLive(); - realm = media.getRealm(); - } - - public Builder setId(int id) { - this.id = id; - return this; - } - - public Builder setLive(boolean live) { - isLive = live; - return this; - } - - public Builder setRealm(@NonNull Realm realm) { - this.realm = realm; - return this; - } - - public Media build() { - return new Media( - id, - isLive, - realm); - } - } - - private final int id; - private final boolean isLive; - @NonNull private final Realm realm; - - public Media( - int id, - boolean isLive, - Realm realm) { - this.id = id; - this.isLive = isLive; - this.realm = realm; - } - - public Builder buildUpon() { - return new Builder(this); - } - - public int getId() { - return id; - } - - public boolean isLive() { - return isLive; - } - - public Realm getRealm() { - return realm; - } -} \ No newline at end of file diff --git a/demos/main/src/main/java/androidx/media3/demo/main/upstream/setting/Realm.java b/demos/main/src/main/java/androidx/media3/demo/main/upstream/setting/Realm.java deleted file mode 100644 index ea3c612167..0000000000 --- a/demos/main/src/main/java/androidx/media3/demo/main/upstream/setting/Realm.java +++ /dev/null @@ -1,49 +0,0 @@ -package androidx.media3.demo.main.upstream.setting; - -import android.text.TextUtils; - -public class Realm { - - public static final Realm DCE_SANDBOX = new Realm("dce.sandbox"); - - private final transient String name; - private final transient String env; - - private String username = ""; - private String password = ""; - - public Realm(String name) { - this(name, null); - } - - public Realm(String name, String env) { - this.name = name; - this.env = TextUtils.isEmpty(env) ? "production" : env; - } - - public String getName() { - return name; - } - - public String getEnv() { - return env; - } - - public String getUsername() { - return username; - } - - public Realm setUsername(String username) { - this.username = username; - return this; - } - - public String getPassword() { - return password; - } - - public Realm setPassword(String password) { - this.password = password; - return this; - } -} \ No newline at end of file diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index b8f6e7c320..d416020034 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -25,8 +25,6 @@ Playback failed - DRM content not supported on API levels below 18 - This device does not support the required DRM scheme This device does not provide a decoder for %1$s @@ -59,6 +57,16 @@ IMA does not support offline ads + This demo app only supports downloading unencrypted or Widevine DRM content + Prefer extension decoders + Video + + Audio + + Text + + Image + diff --git a/demos/session/build.gradle b/demos/session/build.gradle index c279b96f30..bd2c1dd7eb 100644 --- a/demos/session/build.gradle +++ b/demos/session/build.gradle @@ -18,7 +18,7 @@ apply plugin: 'kotlin-android' android { namespace 'androidx.media3.demo.session' - compileSdkVersion project.ext.compileSdkVersion + compileSdk project.ext.compileSdkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -60,11 +60,14 @@ android { dependencies { // For detecting and debugging leaks only. LeakCanary is not needed for demo app to work. - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion implementation 'androidx.core:core-ktx:' + androidxCoreVersion + implementation 'androidx.lifecycle:lifecycle-common:' + androidxLifecycleVersion + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:' + androidxLifecycleVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.multidex:multidex:' + androidxMultidexVersion implementation 'com.google.android.material:material:' + androidxMaterialVersion + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-guava:' + kotlinxCoroutinesVersion implementation project(modulePrefix + 'lib-ui') implementation project(modulePrefix + 'lib-session') implementation project(modulePrefix + 'demo-session-service') diff --git a/demos/session/src/main/AndroidManifest.xml b/demos/session/src/main/AndroidManifest.xml index 550b0698f0..d902b7f414 100644 --- a/demos/session/src/main/AndroidManifest.xml +++ b/demos/session/src/main/AndroidManifest.xml @@ -59,7 +59,7 @@ android:foregroundServiceType="mediaPlayback" android:exported="true"> - + diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt index a87e177b58..134405ed9b 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt @@ -18,6 +18,7 @@ package androidx.media3.demo.session import android.content.ComponentName import android.content.Context import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -28,6 +29,9 @@ import android.widget.TextView import androidx.annotation.OptIn import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.media3.common.C.TRACK_TYPE_TEXT import androidx.media3.common.MediaItem import androidx.media3.common.Player @@ -40,13 +44,15 @@ import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import androidx.media3.ui.PlayerView import com.google.common.util.concurrent.ListenableFuture -import com.google.common.util.concurrent.MoreExecutors +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.guava.await +import kotlinx.coroutines.launch + +private const val TAG = "PlayerActivity" class PlayerActivity : AppCompatActivity() { private lateinit var controllerFuture: ListenableFuture - private val controller: MediaController? - get() = - if (controllerFuture.isDone && !controllerFuture.isCancelled) controllerFuture.get() else null + private lateinit var controller: MediaController private lateinit var playerView: PlayerView private lateinit var mediaItemListView: ListView @@ -54,8 +60,21 @@ class PlayerActivity : AppCompatActivity() { private val mediaItemList: MutableList = mutableListOf() private var lastMediaItemId: String? = null + @OptIn(UnstableApi::class) // PlayerView.hideController override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + try { + initializeController() + awaitCancellation() + } finally { + playerView.player = null + releaseController() + } + } + } + setContentView(R.layout.activity_player) playerView = findViewById(R.id.player_view) @@ -64,10 +83,8 @@ class PlayerActivity : AppCompatActivity() { mediaItemListView.adapter = mediaItemListAdapter mediaItemListView.setOnItemClickListener { _, _, position, _ -> run { - val controller = this.controller ?: return@run if (controller.currentMediaItemIndex == position) { controller.playWhenReady = !controller.playWhenReady - @OptIn(UnstableApi::class) // PlayerView.hideController if (controller.playWhenReady) { playerView.hideController() } @@ -79,26 +96,15 @@ class PlayerActivity : AppCompatActivity() { } } - override fun onStart() { - super.onStart() - initializeController() - } - - override fun onStop() { - super.onStop() - playerView.player = null - releaseController() - } - - private fun initializeController() { + private suspend fun initializeController() { controllerFuture = MediaController.Builder( this, - SessionToken(this, ComponentName(this, PlaybackService::class.java)) + SessionToken(this, ComponentName(this, PlaybackService::class.java)), ) .buildAsync() updateMediaMetadataUI() - controllerFuture.addListener({ setController() }, MoreExecutors.directExecutor()) + setController() } private fun releaseController() { @@ -106,9 +112,13 @@ class PlayerActivity : AppCompatActivity() { } @OptIn(UnstableApi::class) // PlayerView.setShowSubtitleButton - private fun setController() { - val controller = this.controller ?: return - + private suspend fun setController() { + try { + controller = controllerFuture.await() + } catch (t: Throwable) { + Log.w(TAG, "Failed to connect to MediaController", t) + return + } playerView.player = controller updateCurrentPlaylistUI() @@ -137,8 +147,7 @@ class PlayerActivity : AppCompatActivity() { } private fun updateMediaMetadataUI() { - val controller = this.controller - if (controller == null || controller.mediaItemCount == 0) { + if (!::controller.isInitialized || controller.mediaItemCount == 0) { findViewById(R.id.media_title).text = getString(R.string.waiting_for_metadata) findViewById(R.id.media_artist).text = "" return @@ -152,7 +161,9 @@ class PlayerActivity : AppCompatActivity() { } private fun updateCurrentPlaylistUI() { - val controller = this.controller ?: return + if (!::controller.isInitialized) { + return + } mediaItemList.clear() for (i in 0 until controller.mediaItemCount) { mediaItemList.add(controller.getMediaItemAt(i)) @@ -163,7 +174,7 @@ class PlayerActivity : AppCompatActivity() { private inner class MediaItemListAdapter( context: Context, viewID: Int, - mediaItemList: List + mediaItemList: List, ) : ArrayAdapter(context, viewID, mediaItemList) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val mediaItem = getItem(position)!! @@ -173,7 +184,7 @@ class PlayerActivity : AppCompatActivity() { returnConvertView.findViewById(R.id.media_item).text = mediaItem.mediaMetadata.title val deleteButton = returnConvertView.findViewById