fix(music): convert SAF content:// downloads for gapless arming

setNext() appended raw content:// URIs that mpv cannot open, stalling
playback at the SD-card-download track boundary. Convert to fdclose://
like open(), track the armed fd, and reclaim it via a new closeContentFd
method when the entry is dropped unplayed (close only when provably
unconsumed — playlist-pos 0 before and after the remove; leak on doubt).
The playlist-pos pre-check also keeps the clear path from removing the
playing entry when mpv rolls into the armed track mid-clear.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
edde746
2026-07-06 15:26:59 +02:00
co-authored by Claude Fable 5
parent 4de38f4b17
commit 73be8ab1c8
5 changed files with 531 additions and 90 deletions
@@ -5,6 +5,7 @@ import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.os.ParcelFileDescriptor
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
@@ -156,6 +157,7 @@ open class MpvPlayerPlugin(
"requestAudioFocus" -> handleRequestAudioFocus(result)
"abandonAudioFocus" -> handleAbandonAudioFocus(result)
"openContentFd" -> handleOpenContentFd(call, result)
"closeContentFd" -> handleCloseContentFd(call, result)
"isInitialized" -> result.success(playerCore?.isInitialized ?: false)
"setLogLevel" -> result.success(null)
else -> result.notImplemented()
@@ -454,6 +456,26 @@ open class MpvPlayerPlugin(
}.start()
}
// Reclaims a detached fd from handleOpenContentFd that mpv will never
// consume (a gapless-armed entry dropped before mpv opened it). The Dart
// side guarantees single-close and only calls this when the entry provably
// never played.
private fun handleCloseContentFd(call: MethodCall, result: MethodChannel.Result) {
val fd = call.argument<Int>("fd")
if (fd == null || fd < 0) {
result.error("INVALID_ARGS", "Missing 'fd'", null)
return
}
try {
ParcelFileDescriptor.adoptFd(fd).close()
Log.d(tag, "Closed content FD $fd")
result.success(null)
} catch (e: Exception) {
Log.e(tag, "Failed to close content FD $fd: ${e.message}", e)
result.error("CLOSE_FAILED", e.message, null)
}
}
// PlayerDelegate
override fun onPropertyChange(name: String, value: Any?) {