Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
6 changes: 6 additions & 0 deletions media/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
14 changes: 13 additions & 1 deletion media/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<application>
<service
android:name=".PlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>
</application>

</manifest>
184 changes: 184 additions & 0 deletions media/src/main/java/com/example/media/AudioFocus.kt
Original file line number Diff line number Diff line change
@@ -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]
}
157 changes: 157 additions & 0 deletions media/src/main/java/com/example/media/EditingApp.kt
Original file line number Diff line number Diff line change
@@ -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]
}
Loading
Loading