mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-16 04:18:08 +00:00
fix: implement DesktopPlayerLaunchShield for fixing brief white flash
This commit is contained in:
parent
da9fff8795
commit
5b8a4aff9b
3 changed files with 178 additions and 7 deletions
|
|
@ -3,18 +3,22 @@ package com.nuvio.app.features.player
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.awt.SwingPanel
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.nuvio.app.features.player.desktop.DesktopHostOs
|
||||
import com.nuvio.app.features.player.desktop.DesktopPlayerLaunchShield
|
||||
import com.nuvio.app.features.player.desktop.NativePlayerController
|
||||
import com.nuvio.app.features.player.desktop.NativePlayerHost
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -86,16 +90,44 @@ private fun NativePlayerSurface(
|
|||
) {
|
||||
val host = remember { NativePlayerHost() }
|
||||
val controller = remember(host) { NativePlayerController(host) }
|
||||
val hostFirstPaintComplete = remember { mutableStateOf(false) }
|
||||
val hostFirstFullSizePaintComplete = remember { mutableStateOf(false) }
|
||||
LaunchedEffect(sourceUrl) {
|
||||
DesktopPlayerLaunchShield.showForActiveWindow()
|
||||
}
|
||||
val playbackHeaders = remember(sourceHeaders) { sanitizePlaybackHeaders(sourceHeaders) }
|
||||
val latestOnPlayerControlsAction = rememberUpdatedState(onPlayerControlsAction)
|
||||
val latestOnPlayerControlsEvent = rememberUpdatedState(onPlayerControlsEvent)
|
||||
val latestOnPlayerControlsScrubChange = rememberUpdatedState(onPlayerControlsScrubChange)
|
||||
val latestOnPlayerControlsScrubFinished = rememberUpdatedState(onPlayerControlsScrubFinished)
|
||||
val latestOnError = rememberUpdatedState(onError)
|
||||
|
||||
LaunchedEffect(controller) {
|
||||
onControllerReady(controller)
|
||||
}
|
||||
|
||||
DisposableEffect(host) {
|
||||
host.onDisplayableChanged = { displayable ->
|
||||
if (!displayable) {
|
||||
hostFirstPaintComplete.value = false
|
||||
hostFirstFullSizePaintComplete.value = false
|
||||
}
|
||||
}
|
||||
host.onFirstPaint = {
|
||||
hostFirstPaintComplete.value = true
|
||||
}
|
||||
host.onFirstFullSizePaint = {
|
||||
hostFirstFullSizePaintComplete.value = true
|
||||
DesktopPlayerLaunchShield.hideAfter()
|
||||
}
|
||||
onDispose {
|
||||
host.onDisplayableChanged = null
|
||||
host.onFirstPaint = null
|
||||
host.onFirstFullSizePaint = null
|
||||
DesktopPlayerLaunchShield.hide()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(controller) {
|
||||
controller.setControlCallbacks(
|
||||
onAction = { action -> latestOnPlayerControlsAction.value(action) },
|
||||
|
|
@ -106,14 +138,21 @@ private fun NativePlayerSurface(
|
|||
}
|
||||
|
||||
DisposableEffect(controller, sourceUrl, playbackHeaders) {
|
||||
onDispose { controller.dispose() }
|
||||
}
|
||||
|
||||
LaunchedEffect(controller, sourceUrl, playbackHeaders, hostFirstFullSizePaintComplete.value) {
|
||||
if (!hostFirstFullSizePaintComplete.value) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
delay(16L)
|
||||
controller.attach(
|
||||
sourceUrl = sourceUrl,
|
||||
sourceHeaders = playbackHeaders,
|
||||
playWhenReady = playWhenReady,
|
||||
initialPositionMs = initialPositionMs,
|
||||
onError = onError,
|
||||
onError = { message -> latestOnError.value(message) },
|
||||
)
|
||||
onDispose { controller.dispose() }
|
||||
}
|
||||
|
||||
LaunchedEffect(controller, playWhenReady) {
|
||||
|
|
@ -139,10 +178,25 @@ private fun NativePlayerSurface(
|
|||
}
|
||||
}
|
||||
|
||||
SwingPanel(
|
||||
factory = { host },
|
||||
modifier = modifier.fillMaxSize(),
|
||||
)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black),
|
||||
) {
|
||||
SwingPanel(
|
||||
factory = {
|
||||
host
|
||||
},
|
||||
modifier = if (hostFirstPaintComplete.value) {
|
||||
Modifier.fillMaxSize()
|
||||
} else {
|
||||
Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.requiredSize(1.dp)
|
||||
},
|
||||
background = Color.Black,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
package com.nuvio.app.features.player.desktop
|
||||
|
||||
import java.awt.Color
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.Rectangle
|
||||
import java.awt.Window
|
||||
import javax.swing.JWindow
|
||||
import javax.swing.RootPaneContainer
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.Timer
|
||||
|
||||
internal object DesktopPlayerLaunchShield {
|
||||
private var shieldWindow: JWindow? = null
|
||||
private var hideTimer: Timer? = null
|
||||
|
||||
fun showForActiveWindow() {
|
||||
if (DesktopHostOs.current != DesktopHostOs.WINDOWS) return
|
||||
SwingUtilities.invokeLater {
|
||||
val owner = activeOwnerWindow() ?: return@invokeLater
|
||||
val bounds = ownerContentBounds(owner) ?: return@invokeLater
|
||||
hideTimer?.stop()
|
||||
hideTimer = null
|
||||
|
||||
val shield = shieldWindow?.takeIf { it.owner === owner } ?: JWindow(owner).also { window ->
|
||||
window.background = Color.BLACK
|
||||
window.contentPane.background = Color.BLACK
|
||||
window.focusableWindowState = false
|
||||
window.setType(Window.Type.POPUP)
|
||||
shieldWindow = window
|
||||
}
|
||||
shield.bounds = bounds
|
||||
if (!shield.isVisible) {
|
||||
shield.isVisible = true
|
||||
}
|
||||
shield.toFront()
|
||||
hideAfter(3_000)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideAfter(delayMs: Int = 48) {
|
||||
SwingUtilities.invokeLater {
|
||||
hideTimer?.stop()
|
||||
hideTimer = Timer(delayMs) {
|
||||
hideNow()
|
||||
}.apply {
|
||||
isRepeats = false
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
SwingUtilities.invokeLater {
|
||||
hideTimer?.stop()
|
||||
hideTimer = null
|
||||
hideNow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideNow() {
|
||||
val shield = shieldWindow ?: return
|
||||
if (shield.isVisible) {
|
||||
shield.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun activeOwnerWindow(): Window? {
|
||||
val active = KeyboardFocusManager.getCurrentKeyboardFocusManager().activeWindow
|
||||
if (active?.isShowing == true) return active
|
||||
return Window.getWindows()
|
||||
.filter { it.isShowing && it !is JWindow }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
private fun ownerContentBounds(owner: Window): Rectangle? {
|
||||
val content = (owner as? RootPaneContainer)?.contentPane
|
||||
return runCatching {
|
||||
if (content != null && content.isShowing) {
|
||||
val location = content.locationOnScreen
|
||||
Rectangle(location.x, location.y, content.width, content.height)
|
||||
} else {
|
||||
val location = owner.locationOnScreen
|
||||
Rectangle(location.x, location.y, owner.width, owner.height)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,22 +2,52 @@ package com.nuvio.app.features.player.desktop
|
|||
|
||||
import java.awt.Canvas
|
||||
import java.awt.Color
|
||||
import java.awt.Graphics
|
||||
|
||||
internal class NativePlayerHost : Canvas() {
|
||||
var onPeerReady: (() -> Unit)? = null
|
||||
var onDisplayableChanged: ((Boolean) -> Unit)? = null
|
||||
var onFirstPaint: (() -> Unit)? = null
|
||||
var onFirstFullSizePaint: (() -> Unit)? = null
|
||||
private var firstPaintNotified = false
|
||||
private var firstFullSizePaintNotified = false
|
||||
|
||||
init {
|
||||
background = Color.BLACK
|
||||
ignoreRepaint = true
|
||||
ignoreRepaint = false
|
||||
}
|
||||
|
||||
override fun update(graphics: Graphics) {
|
||||
paint(graphics)
|
||||
}
|
||||
|
||||
override fun paint(graphics: Graphics) {
|
||||
graphics.color = Color.BLACK
|
||||
graphics.fillRect(0, 0, width, height)
|
||||
if (!firstPaintNotified) {
|
||||
firstPaintNotified = true
|
||||
onFirstPaint?.invoke()
|
||||
}
|
||||
if (!firstFullSizePaintNotified && width > 1 && height > 1) {
|
||||
firstFullSizePaintNotified = true
|
||||
onFirstFullSizePaint?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override fun addNotify() {
|
||||
super.addNotify()
|
||||
onDisplayableChanged?.invoke(true)
|
||||
repaint()
|
||||
onPeerReady?.invoke()
|
||||
}
|
||||
|
||||
override fun removeNotify() {
|
||||
onDisplayableChanged?.invoke(false)
|
||||
firstPaintNotified = false
|
||||
firstFullSizePaintNotified = false
|
||||
onPeerReady = null
|
||||
onFirstPaint = null
|
||||
onFirstFullSizePaint = null
|
||||
super.removeNotify()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue