Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ import io.github.vinceglb.filekit.PlatformFile
import kotlinx.coroutines.*

@OptIn(UnstableApi::class)
actual fun createVideoPlayerState(): VideoPlayerState =
actual fun createVideoPlayerState(audioMode: AudioMode): VideoPlayerState =
try {
DefaultVideoPlayerState()
DefaultVideoPlayerState(audioMode)
} catch (e: IllegalStateException) {
PreviewableVideoPlayerState(
hasMedia = false,
Expand Down Expand Up @@ -75,7 +75,9 @@ internal val androidVideoLogger = Logger.withTag("AndroidVideoPlayerSurface")

@UnstableApi
@Stable
open class DefaultVideoPlayerState: VideoPlayerState {
open class DefaultVideoPlayerState(
private val audioMode: AudioMode = AudioMode(),
) : VideoPlayerState {
private val context: Context = ContextProvider.getContext()
internal var exoPlayer: ExoPlayer? = null
private var updateJob: Job? = null
Expand Down Expand Up @@ -365,10 +367,17 @@ open class DefaultVideoPlayerState: VideoPlayerState {
}
}

val manageFocus = audioMode.interruptionMode == InterruptionMode.DoNotMix
val audioAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build()

exoPlayer = ExoPlayer.Builder(context)
.setRenderersFactory(renderersFactory)
.setHandleAudioBecomingNoisy(true)
.setWakeMode(C.WAKE_MODE_LOCAL)
.setHandleAudioBecomingNoisy(manageFocus)
.setWakeMode(if (manageFocus) C.WAKE_MODE_LOCAL else C.WAKE_MODE_NONE)
.setAudioAttributes(audioAttributes, manageFocus)
.setPauseAtEndOfMediaItems(false)
.setReleaseTimeoutMs(2000) // Augmenter le timeout de libération
.build()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.kdroidfilter.composemediaplayer

/**
* Controls how the media player interacts with other apps' audio.
*/
enum class InterruptionMode {
/** Exclusive audio focus. Other apps' audio is paused. */
DoNotMix,
/** Mix with other apps' audio. No audio focus requested. */
MixWithOthers,
/** Other apps' audio ducks (lowers volume) while this player is active. */
DuckOthers,
}

/**
* Configures how the media player interacts with the system audio session.
*
* On iOS, this maps to AVAudioSession category, mode, and options.
* On Android, this maps to AudioAttributes and audio focus behavior.
* On other platforms (JVM desktop, web), this has no effect.
*
* The default [AudioMode] requests exclusive audio focus and ignores the iOS silent switch,
* matching standard media playback behavior.
*
* @param interruptionMode How this player interacts with other apps' audio.
* @param playsInSilentMode iOS only: whether audio plays when the device silent switch is on.
*/
data class AudioMode(
val interruptionMode: InterruptionMode = InterruptionMode.DoNotMix,
val playsInSilentMode: Boolean = true,
)
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ interface VideoPlayerState {
* Create platform-specific video player state. Supported platforms include Windows,
* macOS, and Linux.
*/
expect fun createVideoPlayerState(): VideoPlayerState
expect fun createVideoPlayerState(audioMode: AudioMode = AudioMode()): VideoPlayerState

/**
* Creates and manages an instance of `VideoPlayerState` within a composable function, ensuring
Expand All @@ -143,8 +143,8 @@ expect fun createVideoPlayerState(): VideoPlayerState
* controlling and managing video playback, such as play, pause, stop, and seek.
*/
@Composable
fun rememberVideoPlayerState(): VideoPlayerState {
val playerState = remember { createVideoPlayerState() }
fun rememberVideoPlayerState(audioMode: AudioMode = AudioMode()): VideoPlayerState {
val playerState = remember(audioMode) { createVideoPlayerState(audioMode) }
DisposableEffect(Unit) {
onDispose {
playerState.dispose()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import kotlinx.cinterop.COpaquePointer
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.useContents
import platform.AVFAudio.AVAudioSession
import platform.AVFAudio.AVAudioSessionCategoryAmbient
import platform.AVFAudio.AVAudioSessionCategoryOptionDuckOthers
import platform.AVFAudio.AVAudioSessionCategoryOptionMixWithOthers
import platform.AVFAudio.AVAudioSessionCategoryPlayback
import platform.AVFAudio.AVAudioSessionCategorySoloAmbient
import platform.AVFAudio.AVAudioSessionModeDefault
import platform.AVFAudio.AVAudioSessionModeMoviePlayback
import platform.AVFAudio.setActive
import platform.AVFoundation.*
Expand All @@ -44,10 +49,12 @@ import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_global_queue
import platform.darwin.dispatch_get_main_queue

actual fun createVideoPlayerState(): VideoPlayerState = DefaultVideoPlayerState()
actual fun createVideoPlayerState(audioMode: AudioMode): VideoPlayerState = DefaultVideoPlayerState(audioMode)

@Stable
open class DefaultVideoPlayerState: VideoPlayerState {
open class DefaultVideoPlayerState(
private val audioMode: AudioMode = AudioMode(),
) : VideoPlayerState {

// Base states
private var _volume = mutableStateOf(1.0f)
Expand Down Expand Up @@ -159,7 +166,29 @@ open class DefaultVideoPlayerState: VideoPlayerState {
private fun configureAudioSession() {
val session = AVAudioSession.sharedInstance()
try {
session.setCategory(AVAudioSessionCategoryPlayback, mode = AVAudioSessionModeMoviePlayback, options = 0u, error = null)
val category = if (audioMode.playsInSilentMode) {
AVAudioSessionCategoryPlayback
} else {
when (audioMode.interruptionMode) {
InterruptionMode.DoNotMix -> AVAudioSessionCategorySoloAmbient
InterruptionMode.MixWithOthers,
InterruptionMode.DuckOthers -> AVAudioSessionCategoryAmbient
}
}

val mode = if (audioMode.playsInSilentMode) {
AVAudioSessionModeMoviePlayback
} else {
AVAudioSessionModeDefault
}

val options: ULong = when (audioMode.interruptionMode) {
InterruptionMode.DoNotMix -> 0u
InterruptionMode.MixWithOthers -> AVAudioSessionCategoryOptionMixWithOthers
InterruptionMode.DuckOthers -> AVAudioSessionCategoryOptionMixWithOthers or AVAudioSessionCategoryOptionDuckOthers
}

session.setCategory(category, mode = mode, options = options, error = null)
session.setActive(true, error = null)
} catch (e: Exception) {
Logger.e { "Failed to configure audio session: ${e.message}" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.github.kdroidfilter.composemediaplayer.mac.MacVideoPlayerState
import io.github.kdroidfilter.composemediaplayer.windows.WindowsVideoPlayerState
import io.github.vinceglb.filekit.PlatformFile

actual fun createVideoPlayerState(): VideoPlayerState = DefaultVideoPlayerState()
actual fun createVideoPlayerState(audioMode: AudioMode): VideoPlayerState = DefaultVideoPlayerState()

/**
* Represents the state and behavior of a video player. This class provides properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeSource

actual fun createVideoPlayerState(): VideoPlayerState = DefaultVideoPlayerState()
actual fun createVideoPlayerState(audioMode: AudioMode): VideoPlayerState = DefaultVideoPlayerState()

/**
* Implementation of VideoPlayerState for WebAssembly/JavaScript platform.
Expand Down