Move MPV init & destroy to looper thread (#534)

## Description
No user facing changes in this PR

This is a continuation of #481 to move initializing and tearing down
`libmpv` onto the looper thread to prevent main thread blocking.

~The only function left that runs on the main thread is
`setVideoSurfaceView`. Since this has to directly interact with the UI,
I need to read the docs and ExoPlayer code a bit more to see how this
can safely done multi-threaded.~ It is safe/correct to attach on another
thread.
This commit is contained in:
Ray 2025-12-22 10:40:42 -05:00 committed by GitHub
parent 60838c2a86
commit 20fe5e2626
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 26 deletions

View file

@ -38,9 +38,17 @@ object MPVLib {
external fun create(appctx: Context) external fun create(appctx: Context)
external fun init() fun initialize() {
synchronized(this) { init() }
}
external fun destroy() private external fun init()
fun tearDown() {
synchronized(this) { destroy() }
}
private external fun destroy()
external fun attachSurface(surface: Surface) external fun attachSurface(surface: Surface)

View file

@ -76,6 +76,7 @@ class MpvPlayer(
private var surface: Surface? = null private var surface: Surface? = null
private val playbackState = AtomicReference<PlaybackState>(PlaybackState.EMPTY) private val playbackState = AtomicReference<PlaybackState>(PlaybackState.EMPTY)
private val mpvLogger = MpvLogger()
// This looper will sent events to the main thread // This looper will sent events to the main thread
private val looper = Util.getCurrentOrMainLooper() private val looper = Util.getCurrentOrMainLooper()
@ -87,7 +88,7 @@ class MpvPlayer(
) { listener, eventFlags -> ) { listener, eventFlags ->
listener.onEvents(this@MpvPlayer, Player.Events(eventFlags)) listener.onEvents(this@MpvPlayer, Player.Events(eventFlags))
} }
private val availableCommands: Player.Commands private var availableCommands: Player.Commands = Player.Commands.Builder().build()
private val trackSelector = DefaultTrackSelector(context) private val trackSelector = DefaultTrackSelector(context)
// This thread/looper will receive commands from the main thread to execute // This thread/looper will receive commands from the main thread to execute
@ -101,7 +102,13 @@ class MpvPlayer(
private set private set
init { init {
sendCommand(MpvCommand.INITIALIZE, null)
}
private fun init() {
Timber.v("config-dir=${context.filesDir.path}") Timber.v("config-dir=${context.filesDir.path}")
MPVLib.addLogObserver(mpvLogger)
MPVLib.create(context) MPVLib.create(context)
MPVLib.setOptionString("config", "yes") MPVLib.setOptionString("config", "yes")
MPVLib.setOptionString("config-dir", context.filesDir.path) MPVLib.setOptionString("config-dir", context.filesDir.path)
@ -120,17 +127,14 @@ class MpvPlayer(
MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}") MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}")
MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}") MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}")
MPVLib.init() MPVLib.initialize()
MPVLib.setOptionString("force-window", "no") MPVLib.setOptionString("force-window", "no")
MPVLib.setOptionString("idle", "yes") MPVLib.setOptionString("idle", "yes")
// MPVLib.setOptionString("sub-fonts-dir", File(context.filesDir, "fonts").absolutePath) // MPVLib.setOptionString("sub-fonts-dir", File(context.filesDir, "fonts").absolutePath)
internalHandler.post { MPVLib.addObserver(this@MpvPlayer)
MPVLib.addObserver(this@MpvPlayer) MPVProperty.observedProperties.forEach(MPVLib::observeProperty)
MPVProperty.observedProperties.forEach(MPVLib::observeProperty)
MPVLib.addLogObserver(MpvLogger())
}
availableCommands = availableCommands =
Player.Commands Player.Commands
@ -298,15 +302,14 @@ class MpvPlayer(
override fun release() { override fun release() {
Timber.i("release") Timber.i("release")
internalHandler.removeCallbacks(updatePlaybackState)
playbackState.update { playbackState.update {
PlaybackState.EMPTY PlaybackState.EMPTY
} }
if (!isReleased) { if (!isReleased) {
thread.quit() internalHandler.removeCallbacks(updatePlaybackState)
MPVLib.removeObserver(this) MPVLib.removeObserver(this@MpvPlayer)
clearVideoSurfaceView(null) sendCommand(MpvCommand.DESTROY, null)
MPVLib.destroy() thread.quitSafely()
} }
isReleased = true isReleased = true
} }
@ -466,15 +469,9 @@ class MpvPlayer(
throwIfReleased() throwIfReleased()
if (DEBUG) Timber.v("setVideoSurfaceView") if (DEBUG) Timber.v("setVideoSurfaceView")
val surface = surfaceView?.holder?.surface val surface = surfaceView?.holder?.surface
if (surface != null) { if (surface != null && surface.isValid) {
this.surface = surface
Timber.v("Queued attach") Timber.v("Queued attach")
MPVLib.attachSurface(surface) sendCommand(MpvCommand.ATTACH_SURFACE, surface)
MPVLib.setOptionString("force-window", "yes")
Timber.d("Attached surface")
playbackState.load().media?.let {
sendCommand(MpvCommand.LOAD_FILE, it)
}
} else { } else {
clearVideoSurfaceView(null) clearVideoSurfaceView(null)
} }
@ -483,9 +480,7 @@ class MpvPlayer(
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) { override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
if (surface == surfaceView?.holder?.surface) { if (surface == surfaceView?.holder?.surface) {
Timber.d("clearVideoSurfaceView") Timber.d("clearVideoSurfaceView")
MPVLib.detachSurface() sendCommand(MpvCommand.ATTACH_SURFACE, null)
MPVLib.setPropertyString("vo", "null")
MPVLib.setPropertyString("force-window", "no")
} else { } else {
Timber.w("clearVideoSurfaceView called with different surface") Timber.w("clearVideoSurfaceView called with different surface")
} }
@ -621,7 +616,7 @@ class MpvPlayer(
} }
override fun event(eventId: Int) { override fun event(eventId: Int) {
Timber.v("event: thread=${Thread.currentThread().name}, eventId=$eventId") Timber.v("event: eventId=%s", eventId)
when (eventId) { when (eventId) {
MPV_EVENT_START_FILE -> { MPV_EVENT_START_FILE -> {
internalHandler.post(updatePlaybackState) internalHandler.post(updatePlaybackState)
@ -891,6 +886,33 @@ class MpvPlayer(
MpvCommand.LOAD_FILE -> { MpvCommand.LOAD_FILE -> {
loadFile(msg.obj as MediaAndPosition) loadFile(msg.obj as MediaAndPosition)
} }
MpvCommand.ATTACH_SURFACE -> {
val surface = msg.obj as Surface?
if (surface == null || (this.surface != null && this.surface != surface)) {
// If clearing or changing the surface
MPVLib.detachSurface()
MPVLib.setPropertyString("vo", "null")
MPVLib.setPropertyString("force-window", "no")
}
if (surface != null) {
MPVLib.attachSurface(surface)
this.surface = surface
MPVLib.setOptionString("force-window", "yes")
Timber.d("Attached surface")
}
}
MpvCommand.INITIALIZE -> {
init()
}
MpvCommand.DESTROY -> {
clearVideoSurfaceView(null)
MPVLib.removeLogObserver(mpvLogger)
MPVLib.tearDown()
Timber.d("MPVLib destroyed")
}
} }
return true return true
} }
@ -1020,4 +1042,7 @@ enum class MpvCommand {
SET_SPEED, SET_SPEED,
SET_SUBTITLE_DELAY, SET_SUBTITLE_DELAY,
LOAD_FILE, LOAD_FILE,
ATTACH_SURFACE,
INITIALIZE,
DESTROY,
} }