@@ -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?) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user