diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 80ac3bada..7f7b36974 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -226,6 +226,7 @@ androidx-media3-common = { module = "androidx.media3:media3-common", version.ref
androidx-media3-effect = { module = "androidx.media3:media3-effect", version.ref = "media3" }
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3Session" }
+androidx-media3-transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" }
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Ui" }
androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "media3Ui" }
androidx-media3-ui-compose-material3 = { module = "androidx.media3:media3-ui-compose-material3", version.ref = "media3Ui" }
diff --git a/media/build.gradle.kts b/media/build.gradle.kts
index 7d3545b0d..47900dc9b 100644
--- a/media/build.gradle.kts
+++ b/media/build.gradle.kts
@@ -26,5 +26,11 @@ android {
}
dependencies {
+ implementation(libs.androidx.media3.common)
+ implementation(libs.androidx.media3.effect)
implementation(libs.androidx.media3.exoplayer)
+ implementation(libs.androidx.media3.session)
+ implementation(libs.androidx.media3.transformer)
+ implementation(libs.androidx.media3.ui)
+ implementation(libs.guava.android)
}
diff --git a/media/src/main/AndroidManifest.xml b/media/src/main/AndroidManifest.xml
index 51685f593..46e31bc20 100644
--- a/media/src/main/AndroidManifest.xml
+++ b/media/src/main/AndroidManifest.xml
@@ -16,6 +16,18 @@
-->
-
+
+
+
+
+
+
+
+
+
+
diff --git a/media/src/main/java/com/example/media/AudioFocus.kt b/media/src/main/java/com/example/media/AudioFocus.kt
new file mode 100644
index 000000000..4b4ade5f4
--- /dev/null
+++ b/media/src/main/java/com/example/media/AudioFocus.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.media
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.media.AudioAttributes
+import android.media.AudioFocusRequest
+import android.media.AudioManager
+import android.media.session.MediaController
+import android.os.Handler
+import android.os.Looper
+import java.util.concurrent.TimeUnit
+
+private class AudioFocusManager(
+ context: Context
+) : ContextWrapper(context) {
+ private lateinit var audioManager: AudioManager
+ private lateinit var focusRequest: AudioFocusRequest
+ private val handler = Handler(Looper.getMainLooper())
+
+ private val afChangeListener = AudioFocusHandler()
+
+ private var playbackDelayed = false
+ private var resumeOnFocusGain = false
+ private val focusLock = Any()
+
+ private fun requestFocusPostOreo() {
+ // [START android_media_audio_focus_request]
+ // initializing variables for audio focus and playback management.
+ audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
+ setAudioAttributes(
+ AudioAttributes.Builder().run {
+ setUsage(AudioAttributes.USAGE_GAME)
+ setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ build()
+ }
+ )
+ setAcceptsDelayedFocusGain(true)
+ setOnAudioFocusChangeListener(afChangeListener, handler)
+ build()
+ }
+ val focusLock = Any()
+
+ var playbackDelayed = false
+ var playbackNowAuthorized = false
+
+ // requesting audio focus and processing the response.
+ val res = audioManager.requestAudioFocus(focusRequest)
+ synchronized(focusLock) {
+ playbackNowAuthorized = when (res) {
+ AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false
+ AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
+ playbackNow()
+ true
+ }
+ AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
+ playbackDelayed = true
+ false
+ }
+ else -> false
+ }
+ }
+
+ // implementing OnAudioFocusChangeListener to react to focus changes.
+ // [START_EXCLUDE silent]
+ }
+
+ private inner class AudioFocusHandler : AudioManager.OnAudioFocusChangeListener {
+ // [END_EXCLUDE]
+ override fun onAudioFocusChange(focusChange: Int) {
+ when (focusChange) {
+ AudioManager.AUDIOFOCUS_GAIN ->
+ if (playbackDelayed || resumeOnFocusGain) {
+ synchronized(focusLock) {
+ playbackDelayed = false
+ resumeOnFocusGain = false
+ }
+ playbackNow()
+ }
+ AudioManager.AUDIOFOCUS_LOSS -> {
+ synchronized(focusLock) {
+ resumeOnFocusGain = false
+ playbackDelayed = false
+ }
+ pausePlayback()
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
+ synchronized(focusLock) {
+ // only resume if playback is being interrupted.
+ resumeOnFocusGain = isPlaying()
+ playbackDelayed = false
+ }
+ pausePlayback()
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
+ // ... pausing or ducking depends on your app.
+ }
+ }
+ }
+ // [END android_media_audio_focus_request]
+ }
+
+ private fun requestFocusPreOreo() {
+ // [START android_media_audio_focus_request_pre_o]
+ audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ lateinit var afChangeListener: AudioManager.OnAudioFocusChangeListener
+
+ // ...
+ // Request audio focus for playback.
+ val result: Int = audioManager.requestAudioFocus(
+ afChangeListener,
+ // Use the music stream.
+ AudioManager.STREAM_MUSIC,
+ // Request permanent focus.
+ AudioManager.AUDIOFOCUS_GAIN
+ )
+
+ if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ // Start playback.
+ }
+ // [END android_media_audio_focus_request_pre_o]
+ }
+
+ private fun abandonFocusExample(afChangeListener: AudioManager.OnAudioFocusChangeListener) {
+ // [START android_media_audio_focus_abandon]
+ audioManager.abandonAudioFocus(afChangeListener)
+ // [END android_media_audio_focus_abandon]
+ }
+
+ private fun playbackNow() {}
+ private fun pausePlayback() {}
+ private fun isPlaying(): Boolean = true
+}
+
+private class AudioFocusLegacyChangeHandler(
+ private val mediaController: MediaController
+) {
+ // [START android_media_audio_focus_change_listener]
+ private val handler = Handler()
+ private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
+ when (focusChange) {
+ AudioManager.AUDIOFOCUS_LOSS -> {
+ // Permanent loss of audio focus.
+ // Pause playback immediately.
+ mediaController.transportControls.pause()
+ // Wait 30 seconds before stopping playback.
+ handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30))
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
+ // Pause playback.
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
+ // Lower the volume, keep playing.
+ }
+ AudioManager.AUDIOFOCUS_GAIN -> {
+ // Your app has been granted audio focus again.
+ // Raise volume to normal, restart playback if necessary.
+ }
+ }
+ }
+ // [END android_media_audio_focus_change_listener]
+
+ // [START android_media_audio_focus_delayed_stop_runnable]
+ private var delayedStopRunnable = Runnable {
+ mediaController.transportControls.stop()
+ }
+ // [END android_media_audio_focus_delayed_stop_runnable]
+}
diff --git a/media/src/main/java/com/example/media/EditingApp.kt b/media/src/main/java/com/example/media/EditingApp.kt
new file mode 100644
index 000000000..64124ac55
--- /dev/null
+++ b/media/src/main/java/com/example/media/EditingApp.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.media
+
+import android.content.Context
+import android.graphics.Matrix
+import android.net.Uri
+import androidx.annotation.OptIn
+import androidx.media3.common.MediaItem
+import androidx.media3.common.MimeTypes
+import androidx.media3.common.audio.ChannelMixingAudioProcessor
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.effect.Crop
+import androidx.media3.effect.MatrixTransformation
+import androidx.media3.effect.ScaleAndRotateTransformation
+import androidx.media3.exoplayer.DefaultRenderersFactory
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.exoplayer.audio.AudioSink
+import androidx.media3.exoplayer.audio.DefaultAudioSink
+import androidx.media3.transformer.Composition
+import androidx.media3.transformer.Composition.HDR_MODE_KEEP_HDR
+import androidx.media3.transformer.EditedMediaItem
+import androidx.media3.transformer.EditedMediaItemSequence
+import androidx.media3.transformer.Effects
+import androidx.media3.transformer.Transformer
+import com.google.common.collect.ImmutableList
+import kotlin.math.min
+
+@OptIn(UnstableApi::class)
+private fun configureTranscode(context: Context) {
+ // [START android_media_editing_transcode]
+ val transformer = Transformer.Builder(context)
+ .setVideoMimeType(MimeTypes.VIDEO_H265)
+ .setAudioMimeType(MimeTypes.AUDIO_AAC)
+ .build()
+ // [END android_media_editing_transcode]
+}
+
+@OptIn(UnstableApi::class)
+private fun setHdrMode(videoSequence: EditedMediaItemSequence) {
+ // [START android_media_editing_set_hdr_mode]
+ val composition = Composition.Builder(
+ ImmutableList.of(videoSequence)
+ )
+ .setHdrMode(HDR_MODE_KEEP_HDR)
+ .build()
+ // [END android_media_editing_set_hdr_mode]
+}
+
+@OptIn(UnstableApi::class)
+private fun trimVideo(videoUri: Uri) {
+ // [START android_media_editing_trim_video]
+ val clippingConfiguration = MediaItem.ClippingConfiguration.Builder()
+ .setStartPositionMs(10_000) // start at 10 seconds.
+ .setEndPositionMs(20_000) // end at 20 seconds.
+ .build()
+ val mediaItem = MediaItem.Builder()
+ .setUri(videoUri)
+ .setClippingConfiguration(clippingConfiguration)
+ .build()
+ // [END android_media_editing_trim_video]
+}
+
+@OptIn(UnstableApi::class)
+private fun builtInEffects(mediaItem: MediaItem) {
+ // [START android_media_editing_built_in_effects]
+ val channelMixingProcessor = ChannelMixingAudioProcessor()
+ val rotateEffect = ScaleAndRotateTransformation.Builder().setRotationDegrees(60f).build()
+ val cropEffect = Crop(-0.5f, 0.5f, -0.5f, 0.5f)
+ val effects = Effects(listOf(channelMixingProcessor), listOf(rotateEffect, cropEffect))
+
+ val editedMediaItem = EditedMediaItem.Builder(mediaItem)
+ .setEffects(effects)
+ .build()
+ // [END android_media_editing_built_in_effects]
+}
+
+@OptIn(UnstableApi::class)
+private fun customEffects(inputMediaItem: MediaItem) {
+ // [START android_media_editing_custom_effects]
+ val zoomEffect = MatrixTransformation { presentationTimeUs ->
+ val transformationMatrix = Matrix()
+ // Set the scaling factor based on the playback position.
+ val scale = min(1f, presentationTimeUs / 1_000f)
+ transformationMatrix.postScale(/* x */ scale, /* y */ scale)
+ transformationMatrix
+ }
+
+ val editedMediaItem = EditedMediaItem.Builder(inputMediaItem)
+ .setEffects(Effects(listOf(), listOf(zoomEffect)))
+ .build()
+ // [END android_media_editing_custom_effects]
+}
+
+@OptIn(UnstableApi::class)
+private fun previewEffects(context: Context, inputMediaItem: MediaItem, zoomEffect: MatrixTransformation) {
+ // [START android_media_editing_preview_effects]
+ val player = ExoPlayer.Builder(context)
+ .build()
+ .also { exoPlayer ->
+ exoPlayer.setMediaItem(inputMediaItem)
+ exoPlayer.setVideoEffects(listOf(zoomEffect))
+ exoPlayer.prepare()
+ }
+ // [END android_media_editing_preview_effects]
+}
+
+@OptIn(UnstableApi::class)
+private fun previewAudioEffects(context: Context, channelMixingProcessor: ChannelMixingAudioProcessor) {
+ // [START android_media_editing_preview_audio_effects]
+ val player = ExoPlayer.Builder(
+ context,
+ object : DefaultRenderersFactory(context) {
+ override fun buildAudioSink(
+ context: Context,
+ enableFloatOutput: Boolean,
+ enableAudioTrackPlaybackParams: Boolean
+ ): AudioSink? {
+ return DefaultAudioSink.Builder(context)
+ .setEnableFloatOutput(enableFloatOutput)
+ .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
+ .setAudioProcessors(arrayOf(channelMixingProcessor))
+ .build()
+ }
+ }
+ ).build()
+ // [END android_media_editing_preview_audio_effects]
+}
+
+@OptIn(UnstableApi::class)
+private fun startTransformation(
+ context: Context,
+ editedMediaItem: EditedMediaItem,
+ outputPath: String,
+ listener: Transformer.Listener
+) {
+ // [START android_media_editing_start_transformation]
+ val transformer = Transformer.Builder(context)
+ .addListener(listener)
+ .build()
+ transformer.start(editedMediaItem, outputPath)
+ // [END android_media_editing_start_transformation]
+}
diff --git a/media/src/main/java/com/example/media/PlaybackApp.kt b/media/src/main/java/com/example/media/PlaybackApp.kt
index 827eb6720..0b63d2435 100644
--- a/media/src/main/java/com/example/media/PlaybackApp.kt
+++ b/media/src/main/java/com/example/media/PlaybackApp.kt
@@ -16,11 +16,79 @@
package com.example.media
+import android.app.Activity
+import android.content.ComponentName
import android.content.Context
import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.MediaController
+import androidx.media3.session.MediaSession
+import androidx.media3.session.MediaSessionService
+import androidx.media3.session.SessionToken
+import androidx.media3.ui.PlayerView
+import com.google.common.util.concurrent.MoreExecutors
private fun createExoPlayer(context: Context) {
// [START android_media_playback_app_create_exoplayer]
val player = ExoPlayer.Builder(context).build()
// [END android_media_playback_app_create_exoplayer]
}
+
+private fun createMediaSession(context: Context) {
+ // [START android_media_playback_app_create_media_session]
+ val player = ExoPlayer.Builder(context).build()
+ val mediaSession = MediaSession.Builder(context, player).build()
+ // [END android_media_playback_app_create_media_session]
+}
+
+// [START android_media_playback_app_playback_service]
+class PlaybackService : MediaSessionService() {
+ private var mediaSession: MediaSession? = null
+
+ // Create your Player and MediaSession in the onCreate lifecycle event.
+ override fun onCreate() {
+ super.onCreate()
+ val player = ExoPlayer.Builder(this).build()
+ mediaSession = MediaSession.Builder(this, player).build()
+ }
+
+ // Remember to release the player and media session in onDestroy.
+ override fun onDestroy() {
+ mediaSession?.run {
+ player.release()
+ release()
+ mediaSession = null
+ }
+ super.onDestroy()
+ }
+ // [START_EXCLUDE silent]
+
+ // [START android_media_playback_app_on_get_session]
+ // This example always accepts the connection request.
+ override fun onGetSession(
+ controllerInfo: MediaSession.ControllerInfo
+ ): MediaSession? = mediaSession
+ // [END android_media_playback_app_on_get_session]
+ // [END_EXCLUDE]
+}
+// [END android_media_playback_app_playback_service]
+
+private class PlayerActivity : Activity() {
+ private lateinit var playerView: PlayerView
+
+ // [START android_media_playback_app_connect_ui]
+ override fun onStart() {
+ super.onStart()
+ val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
+ val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
+ controllerFuture.addListener(
+ {
+ // Call controllerFuture.get() to retrieve the MediaController.
+ // MediaController implements the Player interface, so it can be
+ // attached to the PlayerView UI component.
+ playerView.setPlayer(controllerFuture.get())
+ },
+ MoreExecutors.directExecutor()
+ )
+ }
+ // [END android_media_playback_app_connect_ui]
+}
diff --git a/media/src/main/java/com/example/media/SurfacesMobile.kt b/media/src/main/java/com/example/media/SurfacesMobile.kt
new file mode 100644
index 000000000..a3c91b881
--- /dev/null
+++ b/media/src/main/java/com/example/media/SurfacesMobile.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.media
+
+import android.os.Bundle
+import androidx.annotation.OptIn
+import androidx.media3.common.MediaItem
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.CommandButton
+import androidx.media3.session.MediaSession
+import androidx.media3.session.MediaSession.ConnectionResult
+import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuilder
+import androidx.media3.session.MediaSessionService
+import androidx.media3.session.SessionCommand
+import androidx.media3.session.SessionResult
+import com.google.common.collect.ImmutableList
+import com.google.common.util.concurrent.Futures
+import com.google.common.util.concurrent.ListenableFuture
+
+private const val ACTION_FAVORITES = "ACTION_FAVORITES"
+
+// Wrapper avoids a collision with PlaybackService in PlaybackApp.kt.
+private object CustomControlsSnippet {
+ @OptIn(UnstableApi::class)
+ // [START android_media_surfaces_mobile_custom_command_buttons]
+ class PlaybackService : MediaSessionService() {
+ private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY)
+ private var mediaSession: MediaSession? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ val favoriteButton =
+ CommandButton.Builder(CommandButton.ICON_HEART_UNFILLED)
+ .setDisplayName("Save to favorites")
+ .setSessionCommand(customCommandFavorites)
+ .build()
+ val player = ExoPlayer.Builder(this).build()
+ // Build the session with a custom layout.
+ mediaSession =
+ MediaSession.Builder(this, player)
+ .setCallback(MyCallback())
+ .setMediaButtonPreferences(ImmutableList.of(favoriteButton))
+ .build()
+ }
+
+ private inner class MyCallback : MediaSession.Callback {
+ override fun onConnect(
+ session: MediaSession,
+ controller: MediaSession.ControllerInfo
+ ): ConnectionResult {
+ // Set available player and session commands.
+ return AcceptedResultBuilder(session)
+ .setAvailableSessionCommands(
+ ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
+ .add(customCommandFavorites)
+ .build()
+ )
+ .build()
+ }
+
+ override fun onCustomCommand(
+ session: MediaSession,
+ controller: MediaSession.ControllerInfo,
+ customCommand: SessionCommand,
+ args: Bundle
+ ): ListenableFuture {
+ if (customCommand.customAction == ACTION_FAVORITES) {
+ // Do custom logic here.
+ saveToFavorites(session.player.currentMediaItem)
+ return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
+ }
+ return super.onCustomCommand(session, controller, customCommand, args)
+ }
+ }
+ // [START_EXCLUDE silent]
+
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
+ return mediaSession
+ }
+ // [END_EXCLUDE]
+ }
+ // [END android_media_surfaces_mobile_custom_command_buttons]
+}
+
+private fun saveToFavorites(item: MediaItem?) {}