diff --git a/README.MD b/README.MD index 10d1570..7b3258f 100644 --- a/README.MD +++ b/README.MD @@ -47,6 +47,7 @@ - [Fullscreen Mode](#️-fullscreen-mode) - [Picture-in-Picture (PiP)](#-picture-in-picture-pip) - [Audio Mode](#-audio-mode) + - [Background Playback Behavior](#-background-playback-behavior) - [Video Caching](#-video-caching) - [Metadata Support](#-metadata-support) - [Example Usage](#example-usage) @@ -71,6 +72,7 @@ Try the online demo here : [🎥 Live Demo](https://kdroidfilter.github.io/Compo - **Fullscreen Mode**: Toggle between windowed and fullscreen playback modes. - **Picture-in-Picture (PiP)**: Continue watching in a floating window on Android (8.0+) and iOS. - **Audio Mode**: Configure audio interruption behavior and iOS silent switch handling. +- **Background Playback Control**: Automatically pauses on background and resumes on foreground (Android & iOS), with an opt-out for background audio playback. - **Video Caching**: Opt-in disk caching for video data on Android and iOS, ideal for scroll-based UIs. - **Error handling** Simple error handling for network or playback issues. @@ -214,11 +216,11 @@ VideoPlayerSurface( horizontalArrangement = Arrangement.SpaceBetween ) { // Your custom controls here - IconButton(onClick = { - if (playerState.isPlaying) playerState.pause() else playerState.play() + IconButton(onClick = { + if (playerState.isPlaying) playerState.pause() else playerState.play() }) { Icon( - imageVector = if (playerState.isPlaying) + imageVector = if (playerState.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, contentDescription = "Play/Pause", tint = Color.White @@ -374,7 +376,7 @@ playerState.error?.let { error -> To detect if the video is buffering: -```kotlin +```kotlin if (playerState.isLoading) { CircularProgressIndicator() } @@ -545,6 +547,27 @@ val playerState = rememberVideoPlayerState( > [!NOTE] > Audio mode is only effective on **Android** and **iOS**. On desktop and web, the parameter is accepted but ignored. +### 📱 Background Playback Behavior + +By default, the player automatically pauses when the app moves to the background and resumes when returning to the foreground. You can disable this to allow audio to keep playing in the background: + +```kotlin +val playerState = rememberVideoPlayerState() + +// Allow playback to continue in the background (audio will keep playing) +playerState.pauseOnBackground = false +``` + +| Platform | Default behavior | `pauseOnBackground = false` | +| :--- | :--- | :--- | +| **Android** | Pauses on `ON_STOP`, resumes on `ON_START` | Audio continues in background | +| **iOS** | Pauses on `didEnterBackground`, resumes on `willEnterForeground` | Audio continues in background (requires `AVAudioSessionCategoryPlayback`) | +| **Desktop** | No effect | No effect | +| **Web** | No effect | No effect | + +> [!NOTE] +> When Picture-in-Picture is active, the player keeps playing regardless of the `pauseOnBackground` setting. + ### 💾 Video Caching You can enable disk-based caching so that video data fetched via `openUri()` is stored locally. Subsequent plays of the same URL load from the cache instead of re-downloading, which is especially useful for scroll-based UIs like TikTok/Reels-style `VerticalPager`. @@ -670,7 +693,7 @@ fun App() { ) { Text("Play Video") } - + Button( onClick = { val url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" diff --git a/mediaplayer/ComposeMediaPlayer.podspec b/mediaplayer/ComposeMediaPlayer.podspec index 2e15d1f..a99bb03 100644 --- a/mediaplayer/ComposeMediaPlayer.podspec +++ b/mediaplayer/ComposeMediaPlayer.podspec @@ -41,5 +41,5 @@ Pod::Spec.new do |spec| SCRIPT } ] - spec.resources = ['build\compose\cocoapods\compose-resources'] + spec.resources = ['build/compose/cocoapods/compose-resources'] end diff --git a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt index d4b530d..a16b5f3 100644 --- a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt +++ b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt @@ -117,6 +117,10 @@ open class DefaultVideoPlayerState( private var screenLockReceiver: BroadcastReceiver? = null private var wasPlayingBeforeScreenLock: Boolean = false + // Background detection + override var pauseOnBackground: Boolean = true + internal var wasPlayingBeforeBackground: Boolean = false + private var _hasMedia by mutableStateOf(false) override val hasMedia: Boolean get() = _hasMedia diff --git a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt index c14ab80..7657766 100644 --- a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt +++ b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt @@ -103,8 +103,36 @@ private fun VideoPlayerSurfaceInternal( } } - DisposableEffect(playerState) { + // Pause video when app goes to background, resume when coming back + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(playerState, lifecycleOwner) { + val observer = + LifecycleEventObserver { _, event -> + if (playerState is DefaultVideoPlayerState && playerState.pauseOnBackground) { + when (event) { + Lifecycle.Event.ON_STOP -> { + // Skip if PiP is active — video should keep playing + if (!playerState.isPipActive) { + playerState.wasPlayingBeforeBackground = playerState.isPlaying + if (playerState.isPlaying) { + playerState.pause() + } + } + } + Lifecycle.Event.ON_START -> { + if (playerState.wasPlayingBeforeBackground && !playerState.isPipActive) { + playerState.play() + playerState.wasPlayingBeforeBackground = false + } + } + else -> {} + } + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) try { // Detach the view from the player if (playerState is DefaultVideoPlayerState) { diff --git a/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.kt b/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.kt index fc46cdf..67e1303 100644 --- a/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.kt +++ b/mediaplayer/src/commonMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.kt @@ -87,6 +87,17 @@ interface VideoPlayerState { var isFullscreen: Boolean val aspectRatio: Float + /** + * Controls whether playback automatically pauses when the app moves to the background + * and resumes when returning to the foreground. + * + * When `true` (default), the player pauses on background and resumes on foreground + * if it was playing before. When `false`, playback continues uninterrupted in the background + * (audio will keep playing on Android; iOS may still pause video rendering). + */ + var pauseOnBackground: Boolean get() = true + set(_) {} + val isPipSupported: Boolean get() = false var isPipActive: Boolean get() = false set(value) {} @@ -309,6 +320,7 @@ data class PreviewableVideoPlayerState( override var isPipEnabled: Boolean = false, override var onPlaybackEnded: (() -> Unit)? = null, override var onRestart: (() -> Unit)? = null, + override var pauseOnBackground: Boolean = true, ) : VideoPlayerState { override fun play() {} diff --git a/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoCache.ios.kt b/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoCache.ios.kt index ec49a9a..f92ec4a 100644 --- a/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoCache.ios.kt +++ b/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoCache.ios.kt @@ -1,8 +1,8 @@ package io.github.kdroidfilter.composemediaplayer import io.github.kdroidfilter.composemediaplayer.util.TaggedLogger -import kotlin.concurrent.AtomicInt import platform.Foundation.NSURLCache +import kotlin.concurrent.AtomicInt private val cacheLogger = TaggedLogger("iOSVideoCache") diff --git a/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.ios.kt b/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.ios.kt index 75992a8..3813f2e 100644 --- a/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.ios.kt +++ b/mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.ios.kt @@ -164,6 +164,9 @@ open class DefaultVideoPlayerState( private var backgroundObserver: Any? = null private var foregroundObserver: Any? = null + // Background behavior + override var pauseOnBackground: Boolean = true + // Flag to track if player was playing before going to background private var wasPlayingBeforeBackground: Boolean = false @@ -401,11 +404,13 @@ open class DefaultVideoPlayerState( // Store current playing state before background wasPlayingBeforeBackground = _isPlaying - // If player is paused by the system, update our state to match - player?.let { player -> - if (player.rate == 0.0f) { - iosLogger.d { "Player was paused by system, updating isPlaying state" } - _isPlaying = false + if (pauseOnBackground) { + // If player is paused by the system, update our state to match + player?.let { player -> + if (player.rate == 0.0f) { + iosLogger.d { "Player was paused by system, updating isPlaying state" } + _isPlaying = false + } } } } @@ -422,7 +427,7 @@ open class DefaultVideoPlayerState( if (wasPlayingBeforeBackground) { iosLogger.d { "Player was playing before background, resuming" } player?.let { player -> - // Only resume if the player is overridely paused + // Only resume if the player is paused if (player.rate == 0.0f) { player.playImmediatelyAtRate(_playbackSpeed) }