feat: scrcpy following with auto fullscreen
This commit is contained in:
@@ -33,4 +33,9 @@ class MainActivity : ComponentActivity() {
|
||||
MainScreen()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
StreamActivity.dismissActivePictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.github.miuzarte.scrcpyforandroid
|
||||
|
||||
import android.R.drawable
|
||||
import android.app.PictureInPictureUiState
|
||||
import android.app.RemoteAction
|
||||
import android.content.Context
|
||||
@@ -8,16 +9,18 @@ import android.graphics.drawable.Icon
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.core.app.PictureInPictureParamsCompat
|
||||
import androidx.core.app.PictureInPictureParamsCompat.Builder
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.pip.BasicPictureInPicture
|
||||
import io.github.miuzarte.scrcpyforandroid.pages.StreamScreen
|
||||
import io.github.miuzarte.scrcpyforandroid.services.PictureInPictureActionReceiver
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class StreamActivity : ComponentActivity() {
|
||||
private val basicPip = BasicPictureInPicture(this)
|
||||
|
||||
private val pipActionReceiver = PictureInPictureActionReceiver()
|
||||
private var isPipActionReceiverRegistered = false
|
||||
|
||||
@@ -28,7 +31,7 @@ class StreamActivity : ComponentActivity() {
|
||||
|
||||
val pipStopAction: RemoteAction by lazy {
|
||||
RemoteAction(
|
||||
Icon.createWithResource(this, android.R.drawable.ic_menu_close_clear_cancel),
|
||||
Icon.createWithResource(this, drawable.ic_menu_close_clear_cancel),
|
||||
"停止投屏",
|
||||
"停止投屏",
|
||||
PictureInPictureActionReceiver.createPendingIntent(this),
|
||||
@@ -39,6 +42,7 @@ class StreamActivity : ComponentActivity() {
|
||||
// 都会重建 activity
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
currentActivityRef = WeakReference(this)
|
||||
|
||||
registerPipActionReceiver()
|
||||
|
||||
@@ -76,11 +80,13 @@ class StreamActivity : ComponentActivity() {
|
||||
*/
|
||||
}
|
||||
|
||||
fun configurePip(params: PictureInPictureParamsCompat) {
|
||||
basicPip.setPictureInPictureParams(params)
|
||||
}
|
||||
fun configurePip(block: Builder.() -> Builder) =
|
||||
basicPip.setPictureInPictureParams(Builder().block().build())
|
||||
|
||||
override fun onDestroy() {
|
||||
currentActivityRef?.get()
|
||||
?.takeIf { it === this }
|
||||
?.let { currentActivityRef = null }
|
||||
unregisterPipActionReceiver()
|
||||
super.onDestroy()
|
||||
}
|
||||
@@ -138,8 +144,16 @@ class StreamActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var currentActivityRef: WeakReference<StreamActivity>? = null
|
||||
|
||||
fun createIntent(context: Context): Intent {
|
||||
return Intent(context, StreamActivity::class.java)
|
||||
}
|
||||
|
||||
fun dismissActivePictureInPicture() {
|
||||
currentActivityRef?.get()
|
||||
?.takeIf { it.isInPictureInPictureMode }
|
||||
?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|
||||
port: Int? = null,
|
||||
name: String? = null,
|
||||
startScrcpyOnConnect: Boolean? = null,
|
||||
openFullscreenOnStart: Boolean? = null,
|
||||
newPort: Int? = null,
|
||||
updateNameOnlyWhenEmpty: Boolean = false,
|
||||
): DeviceShortcuts {
|
||||
@@ -59,6 +60,7 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|
||||
val finalHost = if (updateById) host ?: old.host else old.host
|
||||
val finalPort = if (updateById) port ?: old.port else newPort ?: old.port
|
||||
val finalStartScrcpyOnConnect = startScrcpyOnConnect ?: old.startScrcpyOnConnect
|
||||
val finalOpenFullscreenOnStart = openFullscreenOnStart ?: old.openFullscreenOnStart
|
||||
|
||||
// 若无任何变化,返回原实例
|
||||
if (
|
||||
@@ -66,6 +68,7 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|
||||
&& finalHost == old.host
|
||||
&& finalPort == old.port
|
||||
&& finalStartScrcpyOnConnect == old.startScrcpyOnConnect
|
||||
&& finalOpenFullscreenOnStart == old.openFullscreenOnStart
|
||||
)
|
||||
return this
|
||||
|
||||
@@ -75,6 +78,7 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|
||||
host = finalHost,
|
||||
port = finalPort,
|
||||
startScrcpyOnConnect = finalStartScrcpyOnConnect,
|
||||
openFullscreenOnStart = finalOpenFullscreenOnStart,
|
||||
)
|
||||
}
|
||||
return DeviceShortcuts(
|
||||
@@ -128,13 +132,18 @@ data class DeviceShortcut(
|
||||
val host: String,
|
||||
val port: Int = Defaults.ADB_PORT,
|
||||
val startScrcpyOnConnect: Boolean = false,
|
||||
val openFullscreenOnStart: Boolean = false,
|
||||
) {
|
||||
val id: String get() = "$host:$port"
|
||||
|
||||
fun marshalToString(
|
||||
separator: String = DEFAULT_SEPARATOR,
|
||||
): String = listOf(
|
||||
name.trim(), host.trim(), port.toString(), if (startScrcpyOnConnect) "1" else "0"
|
||||
name.trim(),
|
||||
host.trim(),
|
||||
port.toString(),
|
||||
if (startScrcpyOnConnect) "1" else "0",
|
||||
if (openFullscreenOnStart) "1" else "0",
|
||||
).joinToString(
|
||||
separator = separator
|
||||
)
|
||||
@@ -147,16 +156,19 @@ data class DeviceShortcut(
|
||||
): DeviceShortcut? {
|
||||
val parts = s.split(separator)
|
||||
return when (parts.size) {
|
||||
3, 4 -> {
|
||||
3, 4, 5 -> {
|
||||
val name = parts[0].trim()
|
||||
val host = parts[1].trim()
|
||||
val port = parts[2].trim().toIntOrNull() ?: Defaults.ADB_PORT
|
||||
val startScrcpyOnConnect = parts.getOrNull(3)?.trim() == "1"
|
||||
val openFullscreenOnStart =
|
||||
startScrcpyOnConnect && parts.getOrNull(4)?.trim() == "1"
|
||||
if (host.isNotBlank()) DeviceShortcut(
|
||||
name = name,
|
||||
host = host,
|
||||
port = port,
|
||||
startScrcpyOnConnect = startScrcpyOnConnect,
|
||||
openFullscreenOnStart = openFullscreenOnStart,
|
||||
)
|
||||
else null
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -37,7 +38,6 @@ import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts
|
||||
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
||||
import io.github.miuzarte.scrcpyforandroid.services.EventLogger
|
||||
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
|
||||
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
|
||||
@@ -232,10 +232,9 @@ fun DeviceTabPage(
|
||||
var adbConnected by rememberSaveable { mutableStateOf(false) }
|
||||
var isQuickConnected by rememberSaveable { mutableStateOf(false) }
|
||||
var currentTargetHost by rememberSaveable { mutableStateOf("") }
|
||||
var currentTargetPort by rememberSaveable { mutableStateOf(Defaults.ADB_PORT) }
|
||||
var currentTargetPort by rememberSaveable { mutableIntStateOf(Defaults.ADB_PORT) }
|
||||
var connectedDeviceLabel by rememberSaveable { mutableStateOf("未连接") }
|
||||
val sessionInfo by scrcpy.currentSessionState.collectAsState()
|
||||
var previewControlsVisible by rememberSaveable { mutableStateOf(false) }
|
||||
var editingDeviceId by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var activeDeviceActionId by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var adbConnecting by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -277,9 +276,6 @@ fun DeviceTabPage(
|
||||
qdBundle = qdBundle.copy(quickDevicesList = serialized)
|
||||
}
|
||||
}
|
||||
val editingDevice = remember(savedShortcuts, editingDeviceId) {
|
||||
editingDeviceId?.let(savedShortcuts::get)
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the current ADB connection and stop any running scrcpy session.
|
||||
@@ -400,10 +396,7 @@ fun DeviceTabPage(
|
||||
suspend fun keepAliveCheck(host: String, port: Int): Boolean {
|
||||
return withTimeout(ADB_KEEPALIVE_TIMEOUT_MS) {
|
||||
val connected = NativeAdbService.isConnected()
|
||||
if (!connected) {
|
||||
return@withTimeout false
|
||||
}
|
||||
return@withTimeout true
|
||||
return@withTimeout connected
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,10 +529,15 @@ fun DeviceTabPage(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startScrcpySession() {
|
||||
suspend fun startScrcpySession(openFullscreen: Boolean = false) {
|
||||
val options = scrcpyOptions.toClientOptions(soBundleShared).fix()
|
||||
val session = scrcpy.start(options)
|
||||
pendingScrollToPreview = true
|
||||
if (openFullscreen) {
|
||||
withContext(Dispatchers.Main) {
|
||||
context.startActivity(StreamActivity.createIntent(context))
|
||||
}
|
||||
}
|
||||
if (options.disableScreensaver) {
|
||||
setKeepScreenOn(true)
|
||||
}
|
||||
@@ -571,7 +569,11 @@ fun DeviceTabPage(
|
||||
listState.animateScrollToItem(PREVIEW_CARD_ITEM_INDEX)
|
||||
}
|
||||
|
||||
suspend fun handleAdbConnected(host: String, port: Int) {
|
||||
suspend fun handleAdbConnected(
|
||||
host: String, port: Int,
|
||||
autoStartScrcpy: Boolean = false,
|
||||
autoEnterFullScreen: Boolean = false,
|
||||
) {
|
||||
currentTargetHost = host
|
||||
currentTargetPort = port
|
||||
|
||||
@@ -603,15 +605,11 @@ fun DeviceTabPage(
|
||||
)
|
||||
snackbar.show("ADB 已连接")
|
||||
|
||||
if (
|
||||
savedShortcuts.get(host, port)?.startScrcpyOnConnect == true &&
|
||||
sessionInfo == null
|
||||
) {
|
||||
runBusy("启动 scrcpy") {
|
||||
startScrcpySession()
|
||||
}
|
||||
if (autoStartScrcpy && sessionInfo == null) runBusy("启动 scrcpy") {
|
||||
startScrcpySession(openFullscreen = autoStartScrcpy && autoEnterFullScreen)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(
|
||||
adbConnected,
|
||||
asBundle.adbAutoReconnectPairedDevice,
|
||||
@@ -751,8 +749,9 @@ fun DeviceTabPage(
|
||||
adbConnecting && activeDeviceActionId == device.id
|
||||
},
|
||||
editingDeviceId = editingDeviceId,
|
||||
onClick = {
|
||||
snackbar.show("长按可编辑")
|
||||
onClick = { device ->
|
||||
if (editingDeviceId != device.id)
|
||||
snackbar.show("长按可编辑")
|
||||
},
|
||||
onLongClick = { device ->
|
||||
val connected = adbConnected
|
||||
@@ -768,11 +767,14 @@ fun DeviceTabPage(
|
||||
},
|
||||
onAction = { device ->
|
||||
haptics.contextClick()
|
||||
if (editingDeviceId == device.id) editingDeviceId = null
|
||||
|
||||
val host = device.host
|
||||
val port = device.port
|
||||
val connected = adbConnected
|
||||
&& currentTarget?.host == host
|
||||
&& currentTarget.port == port
|
||||
|
||||
if (!connected) {
|
||||
runAdbConnect(
|
||||
"连接 ADB",
|
||||
@@ -787,7 +789,12 @@ fun DeviceTabPage(
|
||||
savedShortcuts = savedShortcuts.update(
|
||||
host = host, port = port,
|
||||
)
|
||||
handleAdbConnected(host, port)
|
||||
handleAdbConnected(
|
||||
host = host, port = port,
|
||||
autoStartScrcpy = device.startScrcpyOnConnect,
|
||||
autoEnterFullScreen = device.startScrcpyOnConnect
|
||||
&& device.openFullscreenOnStart,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
statusLine = "ADB 连接失败"
|
||||
logEvent("ADB 连接失败: $e", Log.ERROR)
|
||||
@@ -817,6 +824,7 @@ fun DeviceTabPage(
|
||||
host = updated.host,
|
||||
port = updated.port,
|
||||
startScrcpyOnConnect = updated.startScrcpyOnConnect,
|
||||
openFullscreenOnStart = updated.openFullscreenOnStart,
|
||||
)
|
||||
},
|
||||
onEditorDelete = { device ->
|
||||
@@ -962,10 +970,6 @@ fun DeviceTabPage(
|
||||
modifier = Modifier,
|
||||
sessionInfo = sessionInfo,
|
||||
previewHeightDp = asBundle.devicePreviewCardHeightDp.coerceAtLeast(120),
|
||||
controlsVisible = previewControlsVisible,
|
||||
onTapped = {
|
||||
previewControlsVisible = !previewControlsVisible
|
||||
},
|
||||
onOpenFullscreen = {
|
||||
context.startActivity(StreamActivity.createIntent(context))
|
||||
},
|
||||
|
||||
@@ -955,7 +955,7 @@ internal fun ScrcpyAllOptionsPage(
|
||||
}
|
||||
},
|
||||
)
|
||||
// TODO: 在 OverlaySpinnerPreference / OverlayDropdownPreference 支持展开状态回调后, 在展开时触发获取
|
||||
// TODO: 等 MIUIX 发版, 在 OverlaySpinnerPreference / OverlayDropdownPreference 支持展开状态回调后, 在展开时触发获取
|
||||
OverlaySpinnerPreference(
|
||||
title = "视频编码器",
|
||||
summary = "--video-encoder",
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.app.PictureInPictureParamsCompat
|
||||
import io.github.miuzarte.scrcpyforandroid.StreamActivity
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
|
||||
import io.github.miuzarte.scrcpyforandroid.haptics.LocalAppHaptics
|
||||
@@ -72,15 +71,13 @@ fun StreamScreen(activity: StreamActivity) {
|
||||
lastPipAspectRatio = ratio
|
||||
}
|
||||
|
||||
activity.configurePip(
|
||||
PictureInPictureParamsCompat.Builder()
|
||||
.setEnabled(true)
|
||||
.setAspectRatio(pipAspectRatio)
|
||||
.setSourceRectHint(pipSourceRectHint)
|
||||
.setSeamlessResizeEnabled(true)
|
||||
.setCloseAction(activity.pipStopAction)
|
||||
.build(),
|
||||
)
|
||||
activity.configurePip {
|
||||
setEnabled(true)
|
||||
setAspectRatio(pipAspectRatio)
|
||||
setSourceRectHint(pipSourceRectHint)
|
||||
setSeamlessResizeEnabled(true)
|
||||
setCloseAction(activity.pipStopAction)
|
||||
}
|
||||
}
|
||||
|
||||
val themeMode =
|
||||
|
||||
@@ -23,10 +23,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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.width
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@@ -250,14 +250,17 @@ internal fun PreviewCard(
|
||||
modifier: Modifier,
|
||||
sessionInfo: Scrcpy.Session.SessionInfo?,
|
||||
previewHeightDp: Int,
|
||||
controlsVisible: Boolean,
|
||||
onTapped: () -> Unit,
|
||||
onOpenFullscreen: () -> Unit,
|
||||
autoBringIntoView: Boolean = false,
|
||||
onAutoBringIntoViewConsumed: () -> Unit = {},
|
||||
) {
|
||||
val haptics = rememberAppHaptics()
|
||||
val alpha by animateFloatAsState(if (controlsVisible) 1f else 0f, label = "preview-controls")
|
||||
|
||||
var previewControlsVisible by rememberSaveable { mutableStateOf(false) }
|
||||
val alpha by animateFloatAsState(
|
||||
if (previewControlsVisible) 1f else 0f,
|
||||
label = "preview-controls"
|
||||
)
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
|
||||
@@ -268,6 +271,9 @@ internal fun PreviewCard(
|
||||
}
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||
VideoOutputTargetState.set(VideoOutputTarget.PREVIEW)
|
||||
}
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_START -> VideoOutputTargetState.set(VideoOutputTarget.PREVIEW)
|
||||
@@ -287,12 +293,20 @@ internal fun PreviewCard(
|
||||
}
|
||||
}
|
||||
|
||||
Card(modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester).then(modifier)) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.bringIntoViewRequester(bringIntoViewRequester)
|
||||
.then(modifier)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(previewHeightDp.coerceAtLeast(120).dp)
|
||||
.pointerInput(sessionInfo) { detectTapGestures(onTap = { onTapped() }) },
|
||||
.pointerInput(sessionInfo) {
|
||||
detectTapGestures(onTap = {
|
||||
previewControlsVisible = !previewControlsVisible
|
||||
})
|
||||
},
|
||||
) {
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||
val sessionAspect =
|
||||
@@ -801,6 +815,17 @@ fun ScrcpyVideoSurface(
|
||||
}
|
||||
|
||||
DisposableEffect(lifecycleOwner, session, currentSurface) {
|
||||
val surface = currentSurface
|
||||
if (
|
||||
lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) &&
|
||||
session != null &&
|
||||
surface != null &&
|
||||
surface.isValid
|
||||
) {
|
||||
scope.launch {
|
||||
NativeCoreFacade.attachVideoSurface(surface)
|
||||
}
|
||||
}
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_START) {
|
||||
val surface = currentSurface
|
||||
@@ -916,6 +941,8 @@ internal fun DeviceTile(
|
||||
host = trimmedHost,
|
||||
port = currentDraft.port,
|
||||
startScrcpyOnConnect = currentDraft.startScrcpyOnConnect,
|
||||
openFullscreenOnStart = currentDraft.startScrcpyOnConnect
|
||||
&& currentDraft.openFullscreenOnStart,
|
||||
)
|
||||
if (updated != device) {
|
||||
onEditorSave(updated)
|
||||
@@ -1048,6 +1075,18 @@ internal fun DeviceTile(
|
||||
draft = currentDraft.copy(startScrcpyOnConnect = it)
|
||||
},
|
||||
)
|
||||
AnimatedVisibility(currentDraft.startScrcpyOnConnect) {
|
||||
CheckboxPreference(
|
||||
title = "直接进入全屏",
|
||||
checkboxLocation = CheckboxLocation.End,
|
||||
checked = currentDraft.startScrcpyOnConnect
|
||||
&& currentDraft.openFullscreenOnStart,
|
||||
enabled = currentDraft.startScrcpyOnConnect,
|
||||
onCheckedChange = {
|
||||
draft = currentDraft.copy(openFullscreenOnStart = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
||||
Reference in New Issue
Block a user