Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@
android:exported="false"
android:foregroundServiceType="dataSync" />

<!-- The process suffix MUST stay in sync with SceneCommandProcess.SUFFIX; App.onCreate keys
its lightweight-init early return off it. Renaming one without the other silently
reintroduces full DI/Conscrypt/WebView init in this FFmpeg-only child process. -->
<service
android:name=".ui.player.scene.IsolatedSceneCommandService"
android:exported="false"
android:process=":scene_processing" />

<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="eu.kanade.tachiyomi.ui.player.cast.CastOptionsProvider" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.ui.player.scene;

oneway interface ISceneCommandCallback {
void onCompleted(long requestId, boolean success, String output);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.ui.player.scene;

import eu.kanade.tachiyomi.ui.player.scene.ISceneCommandCallback;

interface ISceneCommandService {
void execute(
long requestId,
int commandType,
in String[] arguments,
ISceneCommandCallback callback
);

void cancel(long requestId);
}
10 changes: 10 additions & 0 deletions app/src/main/java/eu/kanade/tachiyomi/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import eu.kanade.tachiyomi.di.PreferenceModule
import eu.kanade.tachiyomi.di.SYPreferenceModule
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.player.scene.SceneCommandProcess
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.GLUtil
Expand Down Expand Up @@ -114,6 +115,15 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
@SuppressLint("LaunchActivityFromNotification")
override fun onCreate() {
super<Application>.onCreate()
if (SceneCommandProcess.isCurrent()) {
if (!LogcatLogger.isInstalled) {
LogcatLogger.install()
}
if (LogcatLogger.loggers.none { it is AndroidLogcatLogger }) {
LogcatLogger.loggers += AndroidLogcatLogger(LogPriority.INFO)
}
return
}
patchInjekt()

// KMK -->
Expand Down
28 changes: 28 additions & 0 deletions app/src/main/java/eu/kanade/tachiyomi/ui/player/AniyomiMPVView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package eu.kanade.tachiyomi.ui.player
import android.content.Context
import android.os.Build
import android.os.Environment
import android.os.Looper
import android.util.AttributeSet
import android.view.KeyCharacterMap
import android.view.KeyEvent
Expand Down Expand Up @@ -51,17 +52,44 @@ class AniyomiMPVView(context: Context, attributes: AttributeSet) : BaseMPVView(c
var isExiting = false
var surfaceReady = false
private set
private val playbackLoadGate = SurfacePlaybackLoadGate { url ->
if (isExiting) {
false
} else {
MPVLib.command(arrayOf("loadfile", url, "replace"))
true
}
}

override fun surfaceCreated(holder: SurfaceHolder) {
super.surfaceCreated(holder)
surfaceReady = true
playbackLoadGate.onSurfaceCreated()
}

override fun surfaceDestroyed(holder: SurfaceHolder) {
playbackLoadGate.onSurfaceDestroyed()
surfaceReady = false
super.surfaceDestroyed(holder)
}

fun loadFileWhenSurfaceReady(url: String) {
if (Looper.myLooper() == Looper.getMainLooper()) {
playbackLoadGate.load(url)
} else {
post { playbackLoadGate.load(url) }
}
}

fun retryPendingLoad() {
playbackLoadGate.retryPending()
}

fun destroyPlayer() {
playbackLoadGate.close()
destroy()
}

private fun getPropertyInt(property: String): Int? {
return MPVLib.getPropertyInt(property) as Int?
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.ui.player

internal fun resolveMpvConfigDirectory(
internalConfigDirectory: String,
useExternalConfigDirectory: Boolean,
externalConfigDirectory: () -> String?,
onExternalFailure: (Exception) -> Unit = {},
): String {
if (!useExternalConfigDirectory) return internalConfigDirectory

return try {
externalConfigDirectory()?.takeIf { it.isNotBlank() } ?: internalConfigDirectory
} catch (error: Exception) {
onExternalFailure(error)
internalConfigDirectory
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.ui.player

internal class PictureInPictureGuard(
initiallyAvailable: Boolean,
private val onRejected: (IllegalStateException) -> Unit = {},
) {
var isAvailable = initiallyAvailable
private set

fun runIfAvailable(operation: () -> Boolean): Boolean {
if (!isAvailable) return false

return try {
operation()
} catch (error: IllegalStateException) {
isAvailable = false
onRejected(error)
false
}
}
}
72 changes: 55 additions & 17 deletions app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Looper
import android.util.Rational
import android.view.KeyEvent
import android.view.View
Expand Down Expand Up @@ -142,10 +143,19 @@ class PlayerActivity : BaseActivity() {
private var restoreAudioFocus: () -> Unit = {}

private var pipRect: Rect? = null
val isPipSupportedAndEnabled by lazy {
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) &&
playerPreferences.enablePip().get()
private val pipGuard by lazy {
PictureInPictureGuard(
initiallyAvailable = packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) &&
playerPreferences.enablePip().get(),
onRejected = { error ->
logcat(LogPriority.WARN, error) {
"Picture-in-picture disabled after framework rejection"
}
},
)
}
val isPipSupportedAndEnabled: Boolean
get() = pipGuard.isAvailable

private var pipReceiver: BroadcastReceiver? = null

Expand Down Expand Up @@ -396,7 +406,9 @@ class PlayerActivity : BaseActivity() {
castManager = castManager, // Pass the castManager instance
onBackPress = {
if (isPipSupportedAndEnabled && player.paused == false && playerPreferences.pipOnExit().get()) {
enterPictureInPictureMode(createPipParams())
if (!enterPictureInPictureIfAvailable()) {
finish()
}
} else {
finish()
}
Expand Down Expand Up @@ -526,7 +538,7 @@ class PlayerActivity : BaseActivity() {

MPVLib.removeLogObserver(playerObserver)
MPVLib.removeObserver(playerObserver)
player.destroy()
player.destroyPlayer()
castManager.cleanup()


Expand Down Expand Up @@ -575,7 +587,7 @@ class PlayerActivity : BaseActivity() {
@SuppressLint("MissingSuperCall")
override fun onUserLeaveHint() {
if (isPipSupportedAndEnabled && player.paused == false && playerPreferences.pipOnExit().get()) {
enterPictureInPictureMode()
enterPictureInPictureIfAvailable()
}
super.onUserLeaveHint()
}
Expand All @@ -587,7 +599,9 @@ class PlayerActivity : BaseActivity() {
viewModel.panelShown.value == Panels.None &&
viewModel.dialogShown.value == Dialogs.None
) {
enterPictureInPictureMode()
if (!enterPictureInPictureIfAvailable()) {
super.onBackPressed()
}
}
} else {
super.onBackPressed()
Expand All @@ -596,7 +610,7 @@ class PlayerActivity : BaseActivity() {

override fun onStart() {
super.onStart()
setPictureInPictureParams(createPipParams())
updatePictureInPictureParamsIfAvailable()
WindowCompat.setDecorFitsSystemWindows(window, false)
window.setFlags(
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
Expand Down Expand Up @@ -643,18 +657,24 @@ class PlayerActivity : BaseActivity() {
}

private fun loadPlayableUrl(url: String) {
MPVLib.command(arrayOf("loadfile", url, "replace"))
player.loadFileWhenSurfaceReady(url)
}

private fun setupPlayerMPV() {
val logLevel = if (networkPreferences.verboseLogging().get()) "info" else "warn"
val internalConfigDir = applicationContext.filesDir.path

val configDir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
storageManager.getMPVConfigDirectory()!!.filePath!!
} else {
internalConfigDir
}
val configDir = resolveMpvConfigDirectory(
internalConfigDirectory = internalConfigDir,
useExternalConfigDirectory = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
Environment.isExternalStorageManager(),
externalConfigDirectory = { storageManager.getMPVConfigDirectory()?.filePath },
onExternalFailure = { error ->
logcat(LogPriority.WARN, error) {
"Failed to resolve external MPV config directory; using internal storage"
}
},
)

val mpvConfFile = File("$configDir/mpv.conf")
advancedPlayerPreferences.mpvConf().get().let { mpvConfFile.writeText(it) }
Expand Down Expand Up @@ -891,6 +911,7 @@ class PlayerActivity : BaseActivity() {
}

player.isExiting = false
player.retryPendingLoad()
super.onResume()

viewModel.currentVolume.update {
Expand Down Expand Up @@ -963,7 +984,7 @@ class PlayerActivity : BaseActivity() {
}

runCatching {
setPictureInPictureParams(createPipParams())
updatePictureInPictureParamsIfAvailable()
}

}
Expand Down Expand Up @@ -1024,6 +1045,19 @@ class PlayerActivity : BaseActivity() {
}
}

internal fun updatePictureInPictureParamsIfAvailable(): Boolean {
return pipGuard.runIfAvailable {
setPictureInPictureParams(createPipParams())
true
}
}

internal fun enterPictureInPictureIfAvailable(): Boolean {
return pipGuard.runIfAvailable {
enterPictureInPictureMode(createPipParams())
}
}

fun createPipParams(): PictureInPictureParams {
val builder = PictureInPictureParams.Builder()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Expand Down Expand Up @@ -1066,7 +1100,7 @@ class PlayerActivity : BaseActivity() {
pipReceiver = null
}
} else {
setPictureInPictureParams(createPipParams())
updatePictureInPictureParamsIfAvailable()
viewModel.hideControls()
viewModel.hideSeekBar()
viewModel.isBrightnessSliderShown.update { false }
Expand All @@ -1082,7 +1116,7 @@ class PlayerActivity : BaseActivity() {
PIP_PREVIOUS -> viewModel.changeEpisode(true)
PIP_SKIP -> viewModel.seekBy(10)
}
setPictureInPictureParams(createPipParams())
updatePictureInPictureParamsIfAvailable()
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Expand Down Expand Up @@ -1342,6 +1376,10 @@ class PlayerActivity : BaseActivity() {
}

fun setVideo(video: Video?, position: Long? = null) {
if (Looper.myLooper() != Looper.getMainLooper()) {
runOnUiThread { setVideo(video, position) }
return
}
if (player.isExiting) return
if (video == null) return

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1491,7 +1491,7 @@ class PlayerViewModel @JvmOverloads internal constructor(
activity.player.paused = true
_paused.update { true }
runCatching {
activity.setPictureInPictureParams(activity.createPipParams())
activity.updatePictureInPictureParamsIfAvailable()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package eu.kanade.tachiyomi.ui.player

internal class SurfacePlaybackLoadGate(
private val loadNow: (String) -> Boolean,
) {
private var isSurfaceReady = false
private var isClosed = false
private var pendingUrl: String? = null

fun load(url: String) {
if (isClosed) return

if (isSurfaceReady && loadNow(url)) {
pendingUrl = null
} else {
pendingUrl = url
}
}

fun onSurfaceCreated() {
if (isClosed) return

isSurfaceReady = true
retryPending()
}

fun retryPending() {
if (isClosed || !isSurfaceReady) return

val url = pendingUrl ?: return
if (loadNow(url)) {
pendingUrl = null
}
}

fun onSurfaceDestroyed() {
isSurfaceReady = false
}

fun close() {
isClosed = true
isSurfaceReady = false
pendingUrl = null
}
}
Loading
Loading