fix: play videos from sd card

close #323
This commit is contained in:
edde746
2026-02-01 02:41:38 +01:00
parent 5121b95aec
commit b332e8e57a
4 changed files with 81 additions and 4 deletions
@@ -1,6 +1,7 @@
package com.edde746.plezy.exoplayer
import android.app.Activity
import android.net.Uri
import android.util.Log
import com.edde746.plezy.mpv.MpvPlayerCore
import com.edde746.plezy.mpv.MpvPlayerDelegate
@@ -190,7 +191,9 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
options.add("http-header-fields-append=$key: $value")
}
val optionsStr = options.joinToString(",")
mpvCore?.command(arrayOf("loadfile", uri, "replace", "-1", optionsStr))
// Convert content:// URIs to fdclose:// for MPV (SAF SD card downloads)
val mpvUri = openContentFd(uri)?.let { "fdclose://$it" } ?: uri
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
} else {
playerCore?.open(uri, headers, startPositionMs, autoPlay)
}
@@ -502,6 +505,25 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
eventSink?.success(event)
}
/**
* Opens a content:// URI via ContentResolver and returns the raw FD number,
* or null if the URI is not a content:// scheme or opening fails.
* The returned FD is detached so MPV can own and close it via fdclose://.
*/
private fun openContentFd(uriString: String): Int? {
if (!uriString.startsWith("content://")) return null
return try {
val uri = Uri.parse(uriString)
val pfd = activity?.contentResolver?.openFileDescriptor(uri, "r") ?: return null
val fd = pfd.detachFd()
Log.d(TAG, "Opened content FD $fd for $uriString")
fd
} catch (e: Exception) {
Log.e(TAG, "Failed to open content FD: ${e.message}", e)
null
}
}
override fun onFormatUnsupported(
uri: String,
headers: Map<String, String>?,
@@ -561,7 +583,9 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
options.add("http-header-fields-append=$key: $value")
}
val optionsStr = options.joinToString(",")
mpvCore?.command(arrayOf("loadfile", uri, "replace", "-1", optionsStr))
// Convert content:// URIs to fdclose:// for MPV (SAF SD card downloads)
val mpvUri = openContentFd(uri)?.let { "fdclose://$it" } ?: uri
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
// Request audio focus
mpvCore?.requestAudioFocus()
@@ -1,6 +1,7 @@
package com.edde746.plezy.mpv
import android.app.Activity
import android.net.Uri
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
@@ -98,6 +99,7 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
"clearVideoFrameRate" -> handleClearVideoFrameRate(result)
"requestAudioFocus" -> handleRequestAudioFocus(result)
"abandonAudioFocus" -> handleAbandonAudioFocus(result)
"openContentFd" -> handleOpenContentFd(call, result)
"isInitialized" -> result.success(playerCore?.isInitialized ?: false)
else -> result.notImplemented()
}
@@ -236,6 +238,37 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
result.success(null)
}
private fun handleOpenContentFd(call: MethodCall, result: MethodChannel.Result) {
val uriString = call.argument<String>("uri")
if (uriString == null) {
result.error("INVALID_ARGS", "Missing 'uri'", null)
return
}
try {
val uri = Uri.parse(uriString)
val contentResolver = activity?.contentResolver
if (contentResolver == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
val pfd = contentResolver.openFileDescriptor(uri, "r")
if (pfd == null) {
result.error("OPEN_FAILED", "Failed to open file descriptor for $uriString", null)
return
}
// detachFd() transfers ownership of the FD to the caller (MPV via fdclose://)
val fd = pfd.detachFd()
Log.d(TAG, "Opened content FD $fd for $uriString")
result.success(fd)
} catch (e: Exception) {
Log.e(TAG, "Failed to open content FD: ${e.message}", e)
result.error("OPEN_FAILED", e.message, null)
}
}
// MpvPlayerDelegate
override fun onPropertyChange(name: String, value: Any?) {
+21 -1
View File
@@ -84,6 +84,17 @@ class PlayerNative extends PlayerBase {
// Playback Control
// ============================================
/// Opens a content:// URI via the platform channel and returns the raw FD number.
/// Returns null if the call fails.
Future<int?> _openContentFd(String contentUri) async {
try {
final fd = await methodChannel.invokeMethod<int>('openContentFd', {'uri': contentUri});
return fd;
} catch (e) {
return null;
}
}
@override
Future<void> open(Media media, {bool play = true}) async {
checkDisposed();
@@ -113,7 +124,16 @@ class PlayerNative extends PlayerBase {
await setProperty('pause', 'yes');
}
await command(['loadfile', media.uri, 'replace']);
// Convert content:// URIs to fdclose:// for MPV on Android (SAF SD card downloads)
var uri = media.uri;
if (Platform.isAndroid && uri.startsWith('content://')) {
final fd = await _openContentFd(uri);
if (fd != null) {
uri = 'fdclose://$fd';
}
}
await command(['loadfile', uri, 'replace']);
}
@override
+1 -1
View File
@@ -885,7 +885,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
return PlaybackInitializationResult(
availableVersions: [],
videoUrl: 'file://$videoPath',
videoUrl: videoPath.contains('://') ? videoPath : 'file://$videoPath',
mediaInfo: null,
externalSubtitles: const [],
isOffline: true,