feat: keep screen awake during fullscreen video and show media controls
Prevent the device from sleeping during fullscreen video playback with FLAG_KEEP_SCREEN_ON. Add a MediaSessionService so Android shows media controls in the notification shade and lock screen when the app is backgrounded, covering both fullscreen and PiP video modes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7648e5a5b8
commit
77148d127f
@@ -88,6 +88,7 @@ dependencies {
|
||||
implementation(libs.media3.exoplayer.hls)
|
||||
implementation(libs.media3.datasource.okhttp)
|
||||
implementation(libs.media3.ui)
|
||||
implementation(libs.media3.session)
|
||||
implementation(libs.biometric)
|
||||
implementation(libs.splashscreen)
|
||||
implementation(libs.profileinstaller)
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
|
||||
@@ -41,6 +43,14 @@
|
||||
<data android:scheme="nostr" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".ui.component.VideoPlaybackService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.net.Uri
|
||||
import android.view.Gravity
|
||||
import android.view.WindowManager
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
@@ -159,11 +160,15 @@ fun FullScreenVideoPlayer(
|
||||
insetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
insetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
VideoMediaSession.attach(activity, exoPlayer)
|
||||
|
||||
dialog.show()
|
||||
|
||||
onDispose {
|
||||
if (!minimizedToPip) VideoMediaSession.release()
|
||||
if (ownsPlayer) exoPlayer.release()
|
||||
if (dialog.isShowing) dialog.dismiss()
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import android.view.TextureView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
@@ -72,6 +73,7 @@ object PipController {
|
||||
|
||||
fun exitPip() {
|
||||
val state = pipState.value ?: return
|
||||
VideoMediaSession.release()
|
||||
state.player.release()
|
||||
pipState.value = null
|
||||
activeVideoUrl.compareAndSet(state.url, null)
|
||||
@@ -93,6 +95,8 @@ fun FloatingVideoPlayer(
|
||||
val state by PipController.pipState.collectAsState()
|
||||
val currentState = state
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = currentState != null,
|
||||
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
|
||||
@@ -102,6 +106,11 @@ fun FloatingVideoPlayer(
|
||||
val pipState = currentState ?: return@AnimatedVisibility
|
||||
val isMuted by PipController.globalMuted.collectAsState()
|
||||
|
||||
DisposableEffect(pipState.player) {
|
||||
VideoMediaSession.attach(context, pipState.player)
|
||||
onDispose {}
|
||||
}
|
||||
|
||||
LaunchedEffect(isMuted) {
|
||||
pipState.player.volume = if (isMuted) 0f else 1f
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.wisp.app.ui.component
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.MediaSession
|
||||
|
||||
object VideoMediaSession {
|
||||
internal var session: MediaSession? = null
|
||||
private set
|
||||
private var appContext: Context? = null
|
||||
|
||||
fun attach(context: Context, player: Player) {
|
||||
if (session?.player === player) return
|
||||
val ctx = context.applicationContext
|
||||
release()
|
||||
appContext = ctx
|
||||
session = MediaSession.Builder(ctx, player).build()
|
||||
ctx.startService(Intent(ctx, VideoPlaybackService::class.java))
|
||||
}
|
||||
|
||||
fun release() {
|
||||
session?.release()
|
||||
session = null
|
||||
appContext?.let {
|
||||
it.stopService(Intent(it, VideoPlaybackService::class.java))
|
||||
}
|
||||
appContext = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.wisp.app.ui.component
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
|
||||
class VideoPlaybackService : MediaSessionService() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
VideoMediaSession.session?.let { addSession(it) }
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
||||
return VideoMediaSession.session
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
val session = VideoMediaSession.session
|
||||
if (session == null || !session.player.playWhenReady) {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
VideoMediaSession.session?.let { removeSession(it) }
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", versi
|
||||
media3-exoplayer-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" }
|
||||
media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3" }
|
||||
media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
|
||||
media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
|
||||
biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" }
|
||||
profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
|
||||
splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" }
|
||||
|
||||
Reference in New Issue
Block a user