feat: PreviewCard auto focus
This commit is contained in:
@@ -20,6 +20,10 @@
|
|||||||
"name": "shizuku",
|
"name": "shizuku",
|
||||||
"path": "../shizuku"
|
"path": "../shizuku"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "moonlight-android",
|
||||||
|
"path": "../moonlight-android"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "KernelSU",
|
"name": "KernelSU",
|
||||||
"path": "../KernelSU"
|
"path": "../KernelSU"
|
||||||
@@ -27,6 +31,9 @@
|
|||||||
{
|
{
|
||||||
"name": "adblib",
|
"name": "adblib",
|
||||||
"path": "../adblib"
|
"path": "../adblib"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../socialite"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,6 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.ScrcpyForAndroid">
|
android:theme="@style/Theme.ScrcpyForAndroid">
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".services.PictureInPictureActionReceiver"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize"
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid
|
package io.github.miuzarte.scrcpyforandroid
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.PictureInPictureUiState
|
import android.app.PictureInPictureUiState
|
||||||
import android.app.RemoteAction
|
import android.app.RemoteAction
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.app.PictureInPictureParamsCompat
|
import androidx.core.app.PictureInPictureParamsCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.pip.BasicPictureInPicture
|
import androidx.core.pip.BasicPictureInPicture
|
||||||
import io.github.miuzarte.scrcpyforandroid.pages.StreamScreen
|
import io.github.miuzarte.scrcpyforandroid.pages.StreamScreen
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.PictureInPictureActionReceiver
|
import io.github.miuzarte.scrcpyforandroid.services.PictureInPictureActionReceiver
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.VideoOutputTarget
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.VideoOutputTargetState
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
class StreamActivity : ComponentActivity() {
|
class StreamActivity : ComponentActivity() {
|
||||||
private val basicPip = BasicPictureInPicture(this)
|
private val basicPip = BasicPictureInPicture(this)
|
||||||
|
private val pipActionReceiver = PictureInPictureActionReceiver()
|
||||||
|
private var isPipActionReceiverRegistered = false
|
||||||
|
|
||||||
// 是否处于 pip
|
// 是否处于 pip
|
||||||
// 回到全屏时会因重建而变回初始值
|
// 回到全屏时会因重建而变回初始值
|
||||||
@@ -29,22 +27,11 @@ class StreamActivity : ComponentActivity() {
|
|||||||
val pipModeState: StateFlow<Boolean> = _pipModeState
|
val pipModeState: StateFlow<Boolean> = _pipModeState
|
||||||
|
|
||||||
val pipStopAction: RemoteAction by lazy {
|
val pipStopAction: RemoteAction by lazy {
|
||||||
val intent = Intent(this, PictureInPictureActionReceiver::class.java)
|
|
||||||
.apply {
|
|
||||||
action = PictureInPictureActionReceiver.ACTION_STOP_SCRCPY
|
|
||||||
`package` = packageName
|
|
||||||
}
|
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
|
||||||
this,
|
|
||||||
1,
|
|
||||||
intent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
||||||
)
|
|
||||||
RemoteAction(
|
RemoteAction(
|
||||||
Icon.createWithResource(this, android.R.drawable.ic_menu_close_clear_cancel),
|
Icon.createWithResource(this, android.R.drawable.ic_menu_close_clear_cancel),
|
||||||
"停止投屏",
|
"停止投屏",
|
||||||
"停止投屏",
|
"停止投屏",
|
||||||
pendingIntent,
|
PictureInPictureActionReceiver.createPendingIntent(this),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +40,8 @@ class StreamActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
registerPipActionReceiver()
|
||||||
|
|
||||||
// 声明要画中画
|
// 声明要画中画
|
||||||
basicPip.setEnabled(true)
|
basicPip.setEnabled(true)
|
||||||
|
|
||||||
@@ -91,6 +80,11 @@ class StreamActivity : ComponentActivity() {
|
|||||||
basicPip.setPictureInPictureParams(params)
|
basicPip.setPictureInPictureParams(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
unregisterPipActionReceiver()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// 回到全屏也会停止, 暂时不做
|
// 回到全屏也会停止, 暂时不做
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -110,22 +104,37 @@ class StreamActivity : ComponentActivity() {
|
|||||||
//+ onPictureInPictureUiStateChanged
|
//+ onPictureInPictureUiStateChanged
|
||||||
//- onUserLeaveHint
|
//- onUserLeaveHint
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
// @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||||
override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
|
override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
|
||||||
super.onPictureInPictureUiStateChanged(pipState)
|
super.onPictureInPictureUiStateChanged(pipState)
|
||||||
|
|
||||||
|
_pipModeState.value = true
|
||||||
|
|
||||||
|
/*
|
||||||
when {
|
when {
|
||||||
// 进入画中画
|
// 进入画中画
|
||||||
pipState.isTransitioningToPip -> if (!_pipModeState.value) {
|
pipState.isTransitioningToPip -> {}
|
||||||
_pipModeState.value = true
|
|
||||||
VideoOutputTargetState.set(VideoOutputTarget.PICTURE_IN_PICTURE)
|
|
||||||
}
|
|
||||||
// 收进边缘
|
// 收进边缘
|
||||||
pipState.isStashed -> if (!_pipModeState.value) {
|
pipState.isStashed -> {}
|
||||||
_pipModeState.value = true
|
|
||||||
VideoOutputTargetState.set(VideoOutputTarget.PICTURE_IN_PICTURE)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerPipActionReceiver() {
|
||||||
|
if (isPipActionReceiverRegistered) return
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
this,
|
||||||
|
pipActionReceiver,
|
||||||
|
PictureInPictureActionReceiver.createIntentFilter(),
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||||
|
)
|
||||||
|
isPipActionReceiverRegistered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterPipActionReceiver() {
|
||||||
|
if (!isPipActionReceiverRegistered) return
|
||||||
|
unregisterReceiver(pipActionReceiver)
|
||||||
|
isPipActionReceiverRegistered = false
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -86,6 +87,8 @@ private const val ADB_KEEPALIVE_TIMEOUT_MS = 1_500L
|
|||||||
private const val ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS = 2_000L
|
private const val ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS = 2_000L
|
||||||
private const val ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS = 2_000L
|
private const val ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS = 2_000L
|
||||||
private const val ADB_TCP_PROBE_TIMEOUT_MS = 500
|
private const val ADB_TCP_PROBE_TIMEOUT_MS = 500
|
||||||
|
private const val PREVIEW_CARD_ITEM_KEY = "preview_card"
|
||||||
|
private const val PREVIEW_CARD_ITEM_INDEX = 3
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceTabScreen(
|
fun DeviceTabScreen(
|
||||||
@@ -241,6 +244,11 @@ fun DeviceTabPage(
|
|||||||
var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) }
|
var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) }
|
||||||
var pendingScrollToPreview by rememberSaveable { mutableStateOf(false) }
|
var pendingScrollToPreview by rememberSaveable { mutableStateOf(false) }
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
val isPreviewCardVisible by remember(listState) {
|
||||||
|
derivedStateOf {
|
||||||
|
listState.layoutInfo.visibleItemsInfo.any { it.key == PREVIEW_CARD_ITEM_KEY }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val currentTarget =
|
val currentTarget =
|
||||||
if (currentTargetHost.isNotBlank())
|
if (currentTargetHost.isNotBlank())
|
||||||
@@ -557,13 +565,10 @@ fun DeviceTabPage(
|
|||||||
snackbar.show("scrcpy 已启动")
|
snackbar.show("scrcpy 已启动")
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(pendingScrollToPreview, sessionInfo) {
|
LaunchedEffect(pendingScrollToPreview, isPreviewCardVisible) {
|
||||||
if (!pendingScrollToPreview) return@LaunchedEffect
|
if (!pendingScrollToPreview) return@LaunchedEffect
|
||||||
val session = sessionInfo ?: return@LaunchedEffect
|
if (isPreviewCardVisible) return@LaunchedEffect
|
||||||
if (session.width <= 0 || session.height <= 0) return@LaunchedEffect
|
listState.animateScrollToItem(PREVIEW_CARD_ITEM_INDEX)
|
||||||
// status/device list/scrcpy panel are above the preview card
|
|
||||||
listState.animateScrollToItem(index = 3)
|
|
||||||
pendingScrollToPreview = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun handleAdbConnected(host: String, port: Int) {
|
suspend fun handleAdbConnected(host: String, port: Int) {
|
||||||
@@ -952,8 +957,9 @@ fun DeviceTabPage(
|
|||||||
sessionInfo!!.width > 0 &&
|
sessionInfo!!.width > 0 &&
|
||||||
sessionInfo!!.height > 0
|
sessionInfo!!.height > 0
|
||||||
) {
|
) {
|
||||||
item {
|
item(key = PREVIEW_CARD_ITEM_KEY) {
|
||||||
PreviewCard(
|
PreviewCard(
|
||||||
|
modifier = Modifier,
|
||||||
sessionInfo = sessionInfo,
|
sessionInfo = sessionInfo,
|
||||||
previewHeightDp = asBundle.devicePreviewCardHeightDp.coerceAtLeast(120),
|
previewHeightDp = asBundle.devicePreviewCardHeightDp.coerceAtLeast(120),
|
||||||
controlsVisible = previewControlsVisible,
|
controlsVisible = previewControlsVisible,
|
||||||
@@ -963,6 +969,8 @@ fun DeviceTabPage(
|
|||||||
onOpenFullscreen = {
|
onOpenFullscreen = {
|
||||||
context.startActivity(StreamActivity.createIntent(context))
|
context.startActivity(StreamActivity.createIntent(context))
|
||||||
},
|
},
|
||||||
|
autoBringIntoView = pendingScrollToPreview,
|
||||||
|
onAutoBringIntoViewConsumed = { pendingScrollToPreview = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -333,7 +333,6 @@ fun FullscreenControlPage(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
session = session,
|
session = session,
|
||||||
target = if (interactive) VideoOutputTarget.FULLSCREEN else VideoOutputTarget.PICTURE_IN_PICTURE,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,12 +45,6 @@ fun StreamScreen(activity: StreamActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(isInPip) {
|
DisposableEffect(isInPip) {
|
||||||
VideoOutputTargetState.set(
|
|
||||||
if (isInPip)
|
|
||||||
VideoOutputTarget.PICTURE_IN_PICTURE
|
|
||||||
else
|
|
||||||
VideoOutputTarget.FULLSCREEN
|
|
||||||
)
|
|
||||||
onDispose {
|
onDispose {
|
||||||
VideoOutputTargetState.set(VideoOutputTarget.PREVIEW)
|
VideoOutputTargetState.set(VideoOutputTarget.PREVIEW)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.services
|
package io.github.miuzarte.scrcpyforandroid.services
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
// MIUI 不进
|
// not working in MIUI
|
||||||
class PictureInPictureActionReceiver : BroadcastReceiver() {
|
class PictureInPictureActionReceiver : BroadcastReceiver() {
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
@@ -31,5 +33,18 @@ class PictureInPictureActionReceiver : BroadcastReceiver() {
|
|||||||
companion object {
|
companion object {
|
||||||
const val ACTION_STOP_SCRCPY =
|
const val ACTION_STOP_SCRCPY =
|
||||||
"io.github.miuzarte.scrcpyforandroid.action.STOP_SCRCPY_FROM_PIP"
|
"io.github.miuzarte.scrcpyforandroid.action.STOP_SCRCPY_FROM_PIP"
|
||||||
|
private const val REQUEST_CODE_STOP_SCRCPY = 1
|
||||||
|
|
||||||
|
fun createIntentFilter(): IntentFilter = IntentFilter(ACTION_STOP_SCRCPY)
|
||||||
|
|
||||||
|
fun createPendingIntent(context: Context): PendingIntent {
|
||||||
|
val intent = Intent(ACTION_STOP_SCRCPY).setPackage(context.packageName)
|
||||||
|
return PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
REQUEST_CODE_STOP_SCRCPY,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
@@ -245,15 +247,25 @@ internal fun PairingCard(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PreviewCard(
|
internal fun PreviewCard(
|
||||||
|
modifier: Modifier,
|
||||||
sessionInfo: Scrcpy.Session.SessionInfo?,
|
sessionInfo: Scrcpy.Session.SessionInfo?,
|
||||||
previewHeightDp: Int,
|
previewHeightDp: Int,
|
||||||
controlsVisible: Boolean,
|
controlsVisible: Boolean,
|
||||||
onTapped: () -> Unit,
|
onTapped: () -> Unit,
|
||||||
onOpenFullscreen: () -> Unit,
|
onOpenFullscreen: () -> Unit,
|
||||||
|
autoBringIntoView: Boolean = false,
|
||||||
|
onAutoBringIntoViewConsumed: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val haptics = rememberAppHaptics()
|
val haptics = rememberAppHaptics()
|
||||||
val alpha by animateFloatAsState(if (controlsVisible) 1f else 0f, label = "preview-controls")
|
val alpha by animateFloatAsState(if (controlsVisible) 1f else 0f, label = "preview-controls")
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
|
|
||||||
|
LaunchedEffect(autoBringIntoView) {
|
||||||
|
if (!autoBringIntoView) return@LaunchedEffect
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
onAutoBringIntoViewConsumed()
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(lifecycleOwner) {
|
DisposableEffect(lifecycleOwner) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
@@ -275,7 +287,7 @@ internal fun PreviewCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Card {
|
Card(modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester).then(modifier)) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -283,11 +295,11 @@ internal fun PreviewCard(
|
|||||||
.pointerInput(sessionInfo) { detectTapGestures(onTap = { onTapped() }) },
|
.pointerInput(sessionInfo) { detectTapGestures(onTap = { onTapped() }) },
|
||||||
) {
|
) {
|
||||||
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||||
val sessionAspect = if (sessionInfo == null || sessionInfo.height == 0) {
|
val sessionAspect =
|
||||||
16f / 9f
|
if (sessionInfo == null || sessionInfo.height == 0)
|
||||||
} else {
|
16f / 9f
|
||||||
sessionInfo.width.toFloat() / sessionInfo.height.toFloat()
|
else sessionInfo.width.toFloat() / sessionInfo.height.toFloat()
|
||||||
}
|
|
||||||
val containerAspect = maxWidth.value / maxHeight.value
|
val containerAspect = maxWidth.value / maxHeight.value
|
||||||
val fittedModifier = if (sessionAspect > containerAspect) {
|
val fittedModifier = if (sessionAspect > containerAspect) {
|
||||||
Modifier
|
Modifier
|
||||||
@@ -307,7 +319,6 @@ internal fun PreviewCard(
|
|||||||
ScrcpyVideoSurface(
|
ScrcpyVideoSurface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
session = sessionInfo,
|
session = sessionInfo,
|
||||||
target = VideoOutputTarget.PREVIEW,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -762,7 +773,6 @@ private fun PairingDialog(
|
|||||||
fun ScrcpyVideoSurface(
|
fun ScrcpyVideoSurface(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
session: Scrcpy.Session.SessionInfo?,
|
session: Scrcpy.Session.SessionInfo?,
|
||||||
target: VideoOutputTarget,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||||
@@ -772,37 +782,29 @@ fun ScrcpyVideoSurface(
|
|||||||
var currentSurface by remember { mutableStateOf<Surface?>(null) }
|
var currentSurface by remember { mutableStateOf<Surface?>(null) }
|
||||||
var currentSurfaceView by remember { mutableStateOf<SurfaceView?>(null) }
|
var currentSurfaceView by remember { mutableStateOf<SurfaceView?>(null) }
|
||||||
|
|
||||||
val currentTarget by VideoOutputTargetState.current.collectAsState()
|
|
||||||
val latestCurrentTarget by rememberUpdatedState(currentTarget)
|
|
||||||
val latestSession by rememberUpdatedState(session)
|
val latestSession by rememberUpdatedState(session)
|
||||||
val latestRequestedTarget by rememberUpdatedState(target)
|
|
||||||
|
|
||||||
LaunchedEffect(session?.width, session?.height, currentSurfaceView, target) {
|
LaunchedEffect(session, currentSurface) {
|
||||||
val surfaceView = currentSurfaceView ?: return@LaunchedEffect
|
val surface = currentSurface ?: return@LaunchedEffect
|
||||||
if (target == VideoOutputTarget.PICTURE_IN_PICTURE) {
|
if (session != null && surface.isValid) {
|
||||||
// In PiP, let SurfaceView buffer follow viewport to avoid stale portrait frame crop.
|
NativeCoreFacade.attachVideoSurface(surface)
|
||||||
surfaceView.holder.setSizeFromLayout()
|
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(session?.width, session?.height, currentSurfaceView) {
|
||||||
|
val surfaceView = currentSurfaceView ?: return@LaunchedEffect
|
||||||
val currentSession = session ?: return@LaunchedEffect
|
val currentSession = session ?: return@LaunchedEffect
|
||||||
|
|
||||||
if (currentSession.width > 0 && currentSession.height > 0) {
|
if (currentSession.width > 0 && currentSession.height > 0) {
|
||||||
surfaceView.holder.setFixedSize(currentSession.width, currentSession.height)
|
surfaceView.holder.setFixedSize(currentSession.width, currentSession.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(session, currentSurface, currentTarget, target) {
|
DisposableEffect(lifecycleOwner, session, currentSurface) {
|
||||||
val surface = currentSurface ?: return@LaunchedEffect
|
|
||||||
if (currentTarget == target && session != null && surface.isValid) {
|
|
||||||
NativeCoreFacade.attachVideoSurface(surface)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(lifecycleOwner, session, currentSurface, currentTarget, target) {
|
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_START) {
|
if (event == Lifecycle.Event.ON_START) {
|
||||||
val surface = currentSurface
|
val surface = currentSurface
|
||||||
if (currentTarget == target && session != null && surface != null && surface.isValid) {
|
if (session != null && surface != null && surface.isValid) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
NativeCoreFacade.attachVideoSurface(surface)
|
NativeCoreFacade.attachVideoSurface(surface)
|
||||||
}
|
}
|
||||||
@@ -820,10 +822,7 @@ fun ScrcpyVideoSurface(
|
|||||||
val surface = currentSurface
|
val surface = currentSurface
|
||||||
if (surface != null) {
|
if (surface != null) {
|
||||||
taskScope.launch {
|
taskScope.launch {
|
||||||
NativeCoreFacade.detachVideoSurface(
|
NativeCoreFacade.detachVideoSurface(surface)
|
||||||
surface,
|
|
||||||
releaseDecoder = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -840,7 +839,7 @@ fun ScrcpyVideoSurface(
|
|||||||
if (!newSurface.isValid) return
|
if (!newSurface.isValid) return
|
||||||
currentSurface = newSurface
|
currentSurface = newSurface
|
||||||
// Register immediately when surface becomes available
|
// Register immediately when surface becomes available
|
||||||
if (latestCurrentTarget == target && latestSession != null) {
|
if (latestSession != null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
NativeCoreFacade.attachVideoSurface(newSurface)
|
NativeCoreFacade.attachVideoSurface(newSurface)
|
||||||
}
|
}
|
||||||
@@ -857,7 +856,7 @@ fun ScrcpyVideoSurface(
|
|||||||
if (!holder.surface.isValid) return
|
if (!holder.surface.isValid) return
|
||||||
val surface = holder.surface
|
val surface = holder.surface
|
||||||
currentSurface = surface
|
currentSurface = surface
|
||||||
if (latestCurrentTarget == latestRequestedTarget && latestSession != null) {
|
if (latestSession != null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
NativeCoreFacade.attachVideoSurface(surface)
|
NativeCoreFacade.attachVideoSurface(surface)
|
||||||
}
|
}
|
||||||
@@ -868,7 +867,7 @@ fun ScrcpyVideoSurface(
|
|||||||
val surface = currentSurface
|
val surface = currentSurface
|
||||||
if (surface != null) {
|
if (surface != null) {
|
||||||
taskScope.launch {
|
taskScope.launch {
|
||||||
NativeCoreFacade.detachVideoSurface(surface, releaseDecoder = false)
|
NativeCoreFacade.detachVideoSurface(surface)
|
||||||
}
|
}
|
||||||
currentSurface = null
|
currentSurface = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
enum class VideoOutputTarget {
|
enum class VideoOutputTarget {
|
||||||
NONE,
|
NONE,
|
||||||
PREVIEW,
|
PREVIEW,
|
||||||
FULLSCREEN,
|
|
||||||
PICTURE_IN_PICTURE,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object VideoOutputTargetState {
|
object VideoOutputTargetState {
|
||||||
|
|||||||
Reference in New Issue
Block a user