Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3ce4aa9fd | ||
|
|
926dbd9ff3 |
@@ -24,6 +24,7 @@ import com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidShareBridge
|
|||||||
import com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidDesktopLyricController
|
import com.zhushenwudi.lovelivemusicplayer.native.platform.AndroidDesktopLyricController
|
||||||
import com.zhushenwudi.lovelivemusicplayer.native.platform.installAndroidPlatformServices
|
import com.zhushenwudi.lovelivemusicplayer.native.platform.installAndroidPlatformServices
|
||||||
import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
|
import com.zhushenwudi.lovelivemusicplayer.native.util.AppUtils
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.native.util.applyScreenOrientationPolicy
|
||||||
import com.zhushenwudi.lovelivemusicplayer.platform.AndroidUiHost
|
import com.zhushenwudi.lovelivemusicplayer.platform.AndroidUiHost
|
||||||
import com.zhushenwudi.lovelivemusicplayer.platform.PlatformServices
|
import com.zhushenwudi.lovelivemusicplayer.platform.PlatformServices
|
||||||
import com.zhushenwudi.lovelivemusicplayer.platform.initAndroidFileSystem
|
import com.zhushenwudi.lovelivemusicplayer.platform.initAndroidFileSystem
|
||||||
@@ -36,6 +37,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
applyScreenOrientationPolicy()
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
AndroidUiHost.activity = this
|
AndroidUiHost.activity = this
|
||||||
initAndroidFileSystem(applicationContext)
|
initAndroidFileSystem(applicationContext)
|
||||||
|
|||||||
+4
-1
@@ -5,6 +5,8 @@ import androidx.activity.ComponentActivity
|
|||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.native.util.applyScreenOrientationPolicy
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.native.util.isLargeScreenDevice
|
||||||
import com.zhushenwudi.lovelivemusicplayer.platform.AndroidUiHost
|
import com.zhushenwudi.lovelivemusicplayer.platform.AndroidUiHost
|
||||||
|
|
||||||
class QrScanActivity : ComponentActivity() {
|
class QrScanActivity : ComponentActivity() {
|
||||||
@@ -22,11 +24,12 @@ class QrScanActivity : ComponentActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
applyScreenOrientationPolicy()
|
||||||
val options = ScanOptions().apply {
|
val options = ScanOptions().apply {
|
||||||
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
setPrompt(I18n.t("scan_qr_prompt"))
|
setPrompt(I18n.t("scan_qr_prompt"))
|
||||||
setBeepEnabled(false)
|
setBeepEnabled(false)
|
||||||
setOrientationLocked(false)
|
setOrientationLocked(!isLargeScreenDevice())
|
||||||
setCaptureActivity(com.journeyapps.barcodescanner.CaptureActivity::class.java)
|
setCaptureActivity(com.journeyapps.barcodescanner.CaptureActivity::class.java)
|
||||||
}
|
}
|
||||||
barcodeLauncher.launch(options)
|
barcodeLauncher.launch(options)
|
||||||
|
|||||||
+2
@@ -42,6 +42,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import com.zhushenwudi.lovelivemusicplayer.i18n.tr
|
import com.zhushenwudi.lovelivemusicplayer.i18n.tr
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.native.util.applyScreenOrientationPolicy
|
||||||
import com.zhushenwudi.lovelivemusicplayer.platform.WebViewAdBlock
|
import com.zhushenwudi.lovelivemusicplayer.platform.WebViewAdBlock
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -56,6 +57,7 @@ class WebViewActivity : ComponentActivity() {
|
|||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
applyScreenOrientationPolicy()
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
val url = intent.getStringExtra(EXTRA_URL).orEmpty()
|
val url = intent.getStringExtra(EXTRA_URL).orEmpty()
|
||||||
val title = intent.getStringExtra(EXTRA_TITLE)
|
val title = intent.getStringExtra(EXTRA_TITLE)
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package com.zhushenwudi.lovelivemusicplayer.native.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
|
||||||
|
private const val LARGE_SCREEN_MIN_SMALLEST_WIDTH_DP = 600
|
||||||
|
|
||||||
|
/** 手机固定竖屏;平板和展开后达到大屏阈值的设备遵循系统旋转设置。 */
|
||||||
|
fun Activity.applyScreenOrientationPolicy() {
|
||||||
|
val isLargeScreen = resources.configuration.smallestScreenWidthDp >=
|
||||||
|
LARGE_SCREEN_MIN_SMALLEST_WIDTH_DP
|
||||||
|
val targetOrientation = if (isLargeScreen) {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
}
|
||||||
|
if (requestedOrientation != targetOrientation) {
|
||||||
|
requestedOrientation = targetOrientation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 扫码组件使用同一设备分类,避免手机进入扫码页后重新开放旋转。 */
|
||||||
|
fun Activity.isLargeScreenDevice(): Boolean =
|
||||||
|
resources.configuration.smallestScreenWidthDp >= LARGE_SCREEN_MIN_SMALLEST_WIDTH_DP
|
||||||
@@ -276,13 +276,13 @@ private fun ObserveDesktopLyricLifecycle() {
|
|||||||
DisposableEffect(owner) {
|
DisposableEffect(owner) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
Lifecycle.Event.ON_PAUSE -> AppContainer.setAppInBackground(true)
|
Lifecycle.Event.ON_STOP -> AppContainer.setAppInBackground(true)
|
||||||
Lifecycle.Event.ON_RESUME -> AppContainer.setAppInBackground(false)
|
Lifecycle.Event.ON_START -> AppContainer.setAppInBackground(false)
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
owner.lifecycle.addObserver(observer)
|
owner.lifecycle.addObserver(observer)
|
||||||
if (!owner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
if (!owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||||
AppContainer.setAppInBackground(true)
|
AppContainer.setAppInBackground(true)
|
||||||
}
|
}
|
||||||
onDispose { owner.lifecycle.removeObserver(observer) }
|
onDispose { owner.lifecycle.removeObserver(observer) }
|
||||||
|
|||||||
+9
-6
@@ -223,12 +223,15 @@ private fun CoverAsyncImage(
|
|||||||
// 进程级 Image 底图:LazyList dispose 会丢弃 remember(Painter),导致
|
// 进程级 Image 底图:LazyList dispose 会丢弃 remember(Painter),导致
|
||||||
// Loading(null) → 空白,即使磁盘/内存仍有封面。
|
// Loading(null) → 空白,即使磁盘/内存仍有封面。
|
||||||
val baseCacheKey = remember(modelUri, stableCacheKey) {
|
val baseCacheKey = remember(modelUri, stableCacheKey) {
|
||||||
stableCacheKey?.takeIf { it.isNotBlank() }
|
if (
|
||||||
?: if (modelUri.startsWith("http", ignoreCase = true)) {
|
modelUri.startsWith("http://", ignoreCase = true) ||
|
||||||
modelUri.toImageCacheKey()
|
modelUri.startsWith("https://", ignoreCase = true)
|
||||||
} else {
|
) {
|
||||||
modelUri
|
// 远程封面以 URL 为内容身份:同一专辑在列表、详情和歌曲条目间共享缓存。
|
||||||
}
|
modelUri.toImageCacheKey()
|
||||||
|
} else {
|
||||||
|
stableCacheKey?.takeIf { it.isNotBlank() } ?: modelUri
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val bucketPx = remember(decodeSize, density) {
|
val bucketPx = remember(decodeSize, density) {
|
||||||
decodeSize?.let { dp ->
|
decodeSize?.let { dp ->
|
||||||
|
|||||||
+8
-2
@@ -29,6 +29,7 @@ import coil3.memory.MemoryCache
|
|||||||
import coil3.network.CacheStrategy
|
import coil3.network.CacheStrategy
|
||||||
import coil3.network.CacheStrategy.ReadResult
|
import coil3.network.CacheStrategy.ReadResult
|
||||||
import coil3.network.CacheStrategy.WriteResult
|
import coil3.network.CacheStrategy.WriteResult
|
||||||
|
import coil3.network.DeDupeConcurrentRequestStrategy
|
||||||
import coil3.network.NetworkRequest
|
import coil3.network.NetworkRequest
|
||||||
import coil3.network.NetworkResponse
|
import coil3.network.NetworkResponse
|
||||||
import coil3.network.ktor3.KtorNetworkFetcherFactory
|
import coil3.network.ktor3.KtorNetworkFetcherFactory
|
||||||
@@ -193,6 +194,9 @@ fun createLlmpImageLoader(context: PlatformContext): ImageLoader {
|
|||||||
add(
|
add(
|
||||||
KtorNetworkFetcherFactory(
|
KtorNetworkFetcherFactory(
|
||||||
cacheStrategy = { PermanentImageCacheStrategy },
|
cacheStrategy = { PermanentImageCacheStrategy },
|
||||||
|
// Hero/页面切换期间同一封面可能被不同尺寸的组件同时请求。
|
||||||
|
// 以磁盘缓存键合并并发网络请求,避免相同 URL 重复下载。
|
||||||
|
concurrentRequestStrategy = { DeDupeConcurrentRequestStrategy() },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -239,8 +243,10 @@ fun LlmpAsyncImage(
|
|||||||
val context = LocalPlatformContext.current
|
val context = LocalPlatformContext.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val httpUrl = (model as? String)?.takeIf { it.isHttpUrl() }
|
val httpUrl = (model as? String)?.takeIf { it.isHttpUrl() }
|
||||||
val baseCacheKey = stableCacheKey?.takeIf { it.isNotBlank() }
|
// 远程图片按 URL 复用原始字节,避免同一专辑封面因 music/album id 不同而重复下载。
|
||||||
?: httpUrl?.toImageCacheKey()
|
// stableCacheKey 仅用于没有远程 URL 的资源;解码尺寸仍只区分内存缓存。
|
||||||
|
val baseCacheKey = httpUrl?.toImageCacheKey()
|
||||||
|
?: stableCacheKey?.takeIf { it.isNotBlank() }
|
||||||
val bucketPx = remember(decodeSize, density) {
|
val bucketPx = remember(decodeSize, density) {
|
||||||
decodeSize?.let { dp ->
|
decodeSize?.let { dp ->
|
||||||
decodeSizeBucketPx(with(density) { dp.roundToPx() })
|
decodeSizeBucketPx(with(density) { dp.roundToPx() })
|
||||||
|
|||||||
@@ -167,8 +167,6 @@
|
|||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
|
|||||||
@@ -30,12 +30,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
func sceneWillResignActive(_ scene: UIScene) {
|
func sceneWillResignActive(_ scene: UIScene) {
|
||||||
// 键盘还在时,系统会把键盘场景挂到画中画上;连接一旦被拆掉就会报 scene 不存在。
|
// 键盘还在时,系统会把键盘场景挂到画中画上;连接一旦被拆掉就会报 scene 不存在。
|
||||||
window?.endEditing(true)
|
window?.endEditing(true)
|
||||||
let defaults = UserDefaults(suiteName: "group.com.zhushenwudi.lovelivemusicplayer")
|
|
||||||
let playing = defaults?.bool(forKey: "pipIsPlaying") ?? false
|
|
||||||
let enabled = defaults?.bool(forKey: "desktopLyricEnabled") ?? false
|
|
||||||
if playing && enabled {
|
|
||||||
pipManager.manualChangePicInPic(needStart: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||||
@@ -44,9 +38,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
let defaults = UserDefaults(suiteName: "group.com.zhushenwudi.lovelivemusicplayer")
|
let defaults = UserDefaults(suiteName: "group.com.zhushenwudi.lovelivemusicplayer")
|
||||||
let playing = defaults?.bool(forKey: "pipIsPlaying") ?? false
|
let playing = defaults?.bool(forKey: "pipIsPlaying") ?? false
|
||||||
let enabled = defaults?.bool(forKey: "desktopLyricEnabled") ?? false
|
let enabled = defaults?.bool(forKey: "desktopLyricEnabled") ?? false
|
||||||
if playing && enabled {
|
pipManager.picInPicAutoOpen(enabled)
|
||||||
pipManager.picInPicAutoOpen(nil)
|
pipManager.setPlaying(playing)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
||||||
@@ -72,7 +65,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
pipManager.addScreenView(on: window)
|
pipManager.addScreenView(on: window)
|
||||||
let enabled = UserDefaults(suiteName: "group.com.zhushenwudi.lovelivemusicplayer")?
|
let enabled = UserDefaults(suiteName: "group.com.zhushenwudi.lovelivemusicplayer")?
|
||||||
.bool(forKey: "desktopLyricEnabled") ?? false
|
.bool(forKey: "desktopLyricEnabled") ?? false
|
||||||
|
let playing = UserDefaults(suiteName: "group.com.zhushenwudi.lovelivemusicplayer")?
|
||||||
|
.bool(forKey: "pipIsPlaying") ?? false
|
||||||
pipManager.picInPicAutoOpen(enabled)
|
pipManager.picInPicAutoOpen(enabled)
|
||||||
|
pipManager.setPlaying(playing)
|
||||||
didSetupPip = true
|
didSetupPip = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,13 +87,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
self.pipManager.picInPicAutoOpen(enabled)
|
self.pipManager.picInPicAutoOpen(enabled)
|
||||||
case "playing":
|
case "playing":
|
||||||
let playing = (notification.userInfo?["playing"] as? String) == "true"
|
let playing = (notification.userInfo?["playing"] as? String) == "true"
|
||||||
if playing {
|
self.pipManager.setPlaying(playing)
|
||||||
self.pipManager.picInPicAutoOpen(nil)
|
|
||||||
} else {
|
|
||||||
self.pipManager.manualChangePicInPic(needStart: false)
|
|
||||||
}
|
|
||||||
case "show":
|
case "show":
|
||||||
self.pipManager.manualChangePicInPic(needStart: true)
|
// PiP 需要在前台时就准备好,由系统在真正转入后台时自动启动。
|
||||||
|
self.pipManager.picInPicAutoOpen(nil)
|
||||||
case "hide":
|
case "hide":
|
||||||
self.pipManager.stopPictureInPictureOnly()
|
self.pipManager.stopPictureInPictureOnly()
|
||||||
case "foreground":
|
case "foreground":
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ class PipScreenManager: NSObject, @preconcurrency AVPictureInPictureControllerDe
|
|||||||
private let sampleHost = SampleBufferHostView()
|
private let sampleHost = SampleBufferHostView()
|
||||||
private var didPrepare = false
|
private var didPrepare = false
|
||||||
private var wantsPip = false
|
private var wantsPip = false
|
||||||
private var isOpenPicInPic = false
|
private var isAutoOpenEnabled = false
|
||||||
private var isStartingPip = false
|
private var isPlaying = false
|
||||||
private var timebase: CMTimebase?
|
private var timebase: CMTimebase?
|
||||||
private var line1 = ""
|
private var line1 = ""
|
||||||
private var line2 = ""
|
private var line2 = ""
|
||||||
@@ -54,8 +54,14 @@ class PipScreenManager: NSObject, @preconcurrency AVPictureInPictureControllerDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func picInPicAutoOpen(_ isOpen: Bool?) {
|
func picInPicAutoOpen(_ isOpen: Bool?) {
|
||||||
if let isOpen { isOpenPicInPic = isOpen }
|
if let isOpen { isAutoOpenEnabled = isOpen }
|
||||||
if isOpenPicInPic { enqueueCurrentFrame() }
|
refreshAutomaticStartEligibility()
|
||||||
|
if isAutoOpenEnabled { enqueueCurrentFrame() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPlaying(_ playing: Bool) {
|
||||||
|
isPlaying = playing
|
||||||
|
refreshAutomaticStartEligibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func updatePipScreenView(lyricLine1: String?, lyricLine2: String?, currentLine: Int) {
|
@objc func updatePipScreenView(lyricLine1: String?, lyricLine2: String?, currentLine: Int) {
|
||||||
@@ -65,19 +71,7 @@ class PipScreenManager: NSObject, @preconcurrency AVPictureInPictureControllerDe
|
|||||||
enqueueCurrentFrame()
|
enqueueCurrentFrame()
|
||||||
}
|
}
|
||||||
|
|
||||||
func manualChangePicInPic(needStart: Bool) {
|
|
||||||
if needStart {
|
|
||||||
wantsPip = true
|
|
||||||
isOpenPicInPic = true
|
|
||||||
pipController?.invalidatePlaybackState()
|
|
||||||
startPipIfPossible(retries: 8)
|
|
||||||
} else {
|
|
||||||
stopPictureInPictureOnly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopPictureInPictureOnly() {
|
func stopPictureInPictureOnly() {
|
||||||
wantsPip = false
|
|
||||||
if pipController?.isPictureInPictureActive == true {
|
if pipController?.isPictureInPictureActive == true {
|
||||||
pipController?.stopPictureInPicture()
|
pipController?.stopPictureInPicture()
|
||||||
}
|
}
|
||||||
@@ -86,7 +80,6 @@ class PipScreenManager: NSObject, @preconcurrency AVPictureInPictureControllerDe
|
|||||||
func pictureInPictureControllerDidStartPictureInPicture(
|
func pictureInPictureControllerDidStartPictureInPicture(
|
||||||
_ pictureInPictureController: AVPictureInPictureController
|
_ pictureInPictureController: AVPictureInPictureController
|
||||||
) {
|
) {
|
||||||
isStartingPip = false
|
|
||||||
if !wantsPip {
|
if !wantsPip {
|
||||||
pictureInPictureController.stopPictureInPicture()
|
pictureInPictureController.stopPictureInPicture()
|
||||||
return
|
return
|
||||||
@@ -98,7 +91,6 @@ class PipScreenManager: NSObject, @preconcurrency AVPictureInPictureControllerDe
|
|||||||
_ pictureInPictureController: AVPictureInPictureController,
|
_ pictureInPictureController: AVPictureInPictureController,
|
||||||
failedToStartPictureInPictureWithError error: Error
|
failedToStartPictureInPictureWithError error: Error
|
||||||
) {
|
) {
|
||||||
isStartingPip = false
|
|
||||||
print("PiP failed to start: \(error)")
|
print("PiP failed to start: \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +110,7 @@ class PipScreenManager: NSObject, @preconcurrency AVPictureInPictureControllerDe
|
|||||||
func pictureInPictureControllerIsPlaybackPaused(
|
func pictureInPictureControllerIsPlaybackPaused(
|
||||||
_ pictureInPictureController: AVPictureInPictureController
|
_ pictureInPictureController: AVPictureInPictureController
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
!isOpenPicInPic
|
!wantsPip
|
||||||
}
|
}
|
||||||
|
|
||||||
func pictureInPictureController(
|
func pictureInPictureController(
|
||||||
@@ -153,33 +145,23 @@ class PipScreenManager: NSObject, @preconcurrency AVPictureInPictureControllerDe
|
|||||||
controller.requiresLinearPlayback = true
|
controller.requiresLinearPlayback = true
|
||||||
controller.setValue(1, forKey: "controlsStyle")
|
controller.setValue(1, forKey: "controlsStyle")
|
||||||
controller.delegate = self
|
controller.delegate = self
|
||||||
// 自动开启会和 sceneWillResignActive 里的手动开启抢同一次系统连接,快速上滑时连接会被拆掉。
|
// 系统只会在 App 真正转入后台时自动开启;临时 inactive
|
||||||
controller.canStartPictureInPictureAutomaticallyFromInline = false
|
// (下拉系统界面、未完成的任务切换手势)不会误弹出桌面歌词。
|
||||||
|
controller.canStartPictureInPictureAutomaticallyFromInline = true
|
||||||
pipController = controller
|
pipController = controller
|
||||||
didPrepare = true
|
didPrepare = true
|
||||||
|
refreshAutomaticStartEligibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 必须在还没进后台时同步调用。启动过程中再调一次会拆掉系统连接。
|
private func refreshAutomaticStartEligibility() {
|
||||||
private func startPipIfPossible(retries: Int) {
|
wantsPip = isAutoOpenEnabled && isPlaying
|
||||||
guard wantsPip, let pip = pipController else { return }
|
pipController?.invalidatePlaybackState()
|
||||||
if pip.isPictureInPictureActive || isStartingPip { return }
|
if !wantsPip {
|
||||||
guard UIApplication.shared.applicationState != .background else { return }
|
stopPictureInPictureOnly()
|
||||||
guard pip.isPictureInPicturePossible else {
|
|
||||||
if retries > 0 {
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.startPipIfPossible(retries: retries - 1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print("PiP not possible after retries")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
isStartingPip = true
|
|
||||||
pip.startPictureInPicture()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func enqueueCurrentFrame() {
|
private func enqueueCurrentFrame() {
|
||||||
if isStartingPip { return }
|
|
||||||
guard didPrepare || sampleHost.superview != nil else { return }
|
guard didPrepare || sampleHost.superview != nil else { return }
|
||||||
guard let pixelBuffer = makePixelBuffer(size: Self.renderSize) else { return }
|
guard let pixelBuffer = makePixelBuffer(size: Self.renderSize) else { return }
|
||||||
CVPixelBufferLockBaseAddress(pixelBuffer, [])
|
CVPixelBufferLockBaseAddress(pixelBuffer, [])
|
||||||
|
|||||||
@@ -85,8 +85,6 @@ targets:
|
|||||||
- remote-notification
|
- remote-notification
|
||||||
UISupportedInterfaceOrientations:
|
UISupportedInterfaceOrientations:
|
||||||
- UIInterfaceOrientationPortrait
|
- UIInterfaceOrientationPortrait
|
||||||
- UIInterfaceOrientationLandscapeLeft
|
|
||||||
- UIInterfaceOrientationLandscapeRight
|
|
||||||
"UISupportedInterfaceOrientations~ipad":
|
"UISupportedInterfaceOrientations~ipad":
|
||||||
- UIInterfaceOrientationPortrait
|
- UIInterfaceOrientationPortrait
|
||||||
- UIInterfaceOrientationLandscapeLeft
|
- UIInterfaceOrientationLandscapeLeft
|
||||||
|
|||||||
Reference in New Issue
Block a user