android
This commit is contained in:
@@ -26,7 +26,7 @@ android {
|
||||
applicationId = "com.edde746.plezy"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
minSdk = 26 // Required by libmpv-android
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
@@ -62,3 +62,7 @@ android {
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("dev.jdtech.mpv:libmpv:0.5.1")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.edde746.plezy.mpv
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.graphics.PixelFormat
|
||||
import android.util.Log
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import dev.jdtech.mpv.MPVLib
|
||||
|
||||
interface MpvPlayerDelegate {
|
||||
fun onPropertyChange(name: String, value: Any?)
|
||||
fun onEvent(name: String, data: Map<String, Any>?)
|
||||
}
|
||||
|
||||
class MpvPlayerCore(private val activity: Activity) :
|
||||
SurfaceHolder.Callback,
|
||||
MPVLib.EventObserver {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MpvPlayerCore"
|
||||
}
|
||||
|
||||
private var surfaceView: SurfaceView? = null
|
||||
var delegate: MpvPlayerDelegate? = null
|
||||
var isInitialized: Boolean = false
|
||||
private set
|
||||
|
||||
fun initialize(): Boolean {
|
||||
if (isInitialized) {
|
||||
Log.d(TAG, "Already initialized")
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
// Create SurfaceView for video rendering
|
||||
surfaceView = SurfaceView(activity).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
setBackgroundColor(Color.BLACK)
|
||||
holder.addCallback(this@MpvPlayerCore)
|
||||
|
||||
// Critical: Ensure SurfaceView renders BEHIND Flutter's view
|
||||
setZOrderOnTop(false)
|
||||
setZOrderMediaOverlay(false)
|
||||
}
|
||||
|
||||
// Insert SurfaceView at bottom of view hierarchy (behind Flutter)
|
||||
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
|
||||
contentView.addView(surfaceView, 0)
|
||||
|
||||
// Find FlutterView and its internal FlutterSurfaceView, set it on top
|
||||
for (i in 0 until contentView.childCount) {
|
||||
val child = contentView.getChildAt(i)
|
||||
if (child is ViewGroup && child.javaClass.name.contains("FlutterView")) {
|
||||
// Look inside FlutterView for FlutterSurfaceView
|
||||
for (j in 0 until child.childCount) {
|
||||
val flutterChild = child.getChildAt(j)
|
||||
if (flutterChild is SurfaceView) {
|
||||
// Put Flutter in media overlay layer (above our video which is in normal layer)
|
||||
flutterChild.setZOrderMediaOverlay(true)
|
||||
flutterChild.holder.setFormat(PixelFormat.TRANSLUCENT)
|
||||
Log.d(TAG, "Set FlutterSurfaceView to MediaOverlay with TRANSLUCENT: ${flutterChild.javaClass.name}")
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "SurfaceView added to content view")
|
||||
|
||||
// Initialize MPVLib
|
||||
MPVLib.create(activity.applicationContext)
|
||||
|
||||
// Configure MPV defaults
|
||||
setupMpvDefaults()
|
||||
|
||||
// Initialize MPV
|
||||
MPVLib.init()
|
||||
|
||||
// Register event observer
|
||||
MPVLib.addObserver(this)
|
||||
|
||||
isInitialized = true
|
||||
Log.d(TAG, "Initialized successfully")
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initialize: ${e.message}", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMpvDefaults() {
|
||||
// Video output configuration
|
||||
MPVLib.setOptionString("vo", "gpu")
|
||||
MPVLib.setOptionString("gpu-context", "android")
|
||||
MPVLib.setOptionString("hwdec", "mediacodec-copy")
|
||||
|
||||
// Audio configuration
|
||||
MPVLib.setOptionString("ao", "audiotrack")
|
||||
}
|
||||
|
||||
// SurfaceHolder.Callback
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
Log.d(TAG, "Surface created")
|
||||
MPVLib.attachSurface(holder.surface)
|
||||
MPVLib.setOptionString("force-window", "yes")
|
||||
}
|
||||
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
Log.d(TAG, "Surface changed: ${width}x${height}")
|
||||
MPVLib.setPropertyString("android-surface-size", "${width}x${height}")
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
Log.d(TAG, "Surface destroyed")
|
||||
MPVLib.setOptionString("force-window", "no")
|
||||
MPVLib.detachSurface()
|
||||
}
|
||||
|
||||
// MPVLib.EventObserver
|
||||
|
||||
override fun eventProperty(property: String) {
|
||||
// No value provided
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Long) {
|
||||
activity.runOnUiThread {
|
||||
delegate?.onPropertyChange(property, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Double) {
|
||||
activity.runOnUiThread {
|
||||
delegate?.onPropertyChange(property, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Boolean) {
|
||||
activity.runOnUiThread {
|
||||
delegate?.onPropertyChange(property, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: String) {
|
||||
activity.runOnUiThread {
|
||||
delegate?.onPropertyChange(property, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun event(eventId: Int) {
|
||||
val eventName = when (eventId) {
|
||||
MPVLib.MPV_EVENT_FILE_LOADED -> "file-loaded"
|
||||
MPVLib.MPV_EVENT_END_FILE -> "end-file"
|
||||
MPVLib.MPV_EVENT_PLAYBACK_RESTART -> "playback-restart"
|
||||
else -> null
|
||||
}
|
||||
eventName?.let { name ->
|
||||
activity.runOnUiThread {
|
||||
delegate?.onEvent(name, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
fun setProperty(name: String, value: String) {
|
||||
if (!isInitialized) return
|
||||
MPVLib.setPropertyString(name, value)
|
||||
}
|
||||
|
||||
fun getProperty(name: String): String? {
|
||||
if (!isInitialized) return null
|
||||
return try {
|
||||
MPVLib.getPropertyString(name)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun observeProperty(name: String, format: String) {
|
||||
if (!isInitialized) return
|
||||
|
||||
val mpvFormat = when (format) {
|
||||
"double" -> MPVLib.MPV_FORMAT_DOUBLE
|
||||
"flag" -> MPVLib.MPV_FORMAT_FLAG
|
||||
"string" -> MPVLib.MPV_FORMAT_STRING
|
||||
"node" -> MPVLib.MPV_FORMAT_NODE
|
||||
else -> MPVLib.MPV_FORMAT_NONE
|
||||
}
|
||||
MPVLib.observeProperty(name, mpvFormat)
|
||||
}
|
||||
|
||||
fun command(args: Array<String>) {
|
||||
if (!isInitialized || args.isEmpty()) return
|
||||
MPVLib.command(args)
|
||||
}
|
||||
|
||||
fun setVisible(visible: Boolean) {
|
||||
activity.runOnUiThread {
|
||||
surfaceView?.visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
||||
Log.d(TAG, "setVisible($visible)")
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle handling
|
||||
|
||||
fun onPause() {
|
||||
if (!isInitialized) return
|
||||
Log.d(TAG, "onPause - disabling video")
|
||||
setProperty("vid", "no")
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
if (!isInitialized) return
|
||||
Log.d(TAG, "onResume - enabling video")
|
||||
setProperty("vid", "auto")
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
|
||||
fun dispose() {
|
||||
Log.d(TAG, "Disposing")
|
||||
|
||||
MPVLib.removeObserver(this)
|
||||
|
||||
surfaceView?.holder?.removeCallback(this)
|
||||
|
||||
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
|
||||
surfaceView?.let { contentView.removeView(it) }
|
||||
surfaceView = null
|
||||
|
||||
MPVLib.destroy()
|
||||
isInitialized = false
|
||||
|
||||
Log.d(TAG, "Disposed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.edde746.plezy.mpv
|
||||
|
||||
import android.app.Activity
|
||||
import android.util.Log
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
EventChannel.StreamHandler, ActivityAware, MpvPlayerDelegate {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MpvPlayerPlugin"
|
||||
private const val METHOD_CHANNEL = "com.plezy/mpv_player"
|
||||
private const val EVENT_CHANNEL = "com.plezy/mpv_player/events"
|
||||
}
|
||||
|
||||
private lateinit var methodChannel: MethodChannel
|
||||
private lateinit var eventChannel: EventChannel
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
private var playerCore: MpvPlayerCore? = null
|
||||
private var activity: Activity? = null
|
||||
private var activityBinding: ActivityPluginBinding? = null
|
||||
|
||||
// FlutterPlugin
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
methodChannel = MethodChannel(binding.binaryMessenger, METHOD_CHANNEL)
|
||||
methodChannel.setMethodCallHandler(this)
|
||||
|
||||
eventChannel = EventChannel(binding.binaryMessenger, EVENT_CHANNEL)
|
||||
eventChannel.setStreamHandler(this)
|
||||
|
||||
Log.d(TAG, "Attached to engine")
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
methodChannel.setMethodCallHandler(null)
|
||||
eventChannel.setStreamHandler(null)
|
||||
Log.d(TAG, "Detached from engine")
|
||||
}
|
||||
|
||||
// ActivityAware
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
activityBinding = binding
|
||||
binding.addOnUserLeaveHintListener { playerCore?.onPause() }
|
||||
Log.d(TAG, "Attached to activity")
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
playerCore?.dispose()
|
||||
playerCore = null
|
||||
activity = null
|
||||
activityBinding = null
|
||||
Log.d(TAG, "Detached from activity")
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
activityBinding = binding
|
||||
Log.d(TAG, "Reattached to activity for config changes")
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activity = null
|
||||
activityBinding = null
|
||||
Log.d(TAG, "Detached from activity for config changes")
|
||||
}
|
||||
|
||||
// EventChannel.StreamHandler
|
||||
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
eventSink = events
|
||||
Log.d(TAG, "Event stream connected")
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
eventSink = null
|
||||
Log.d(TAG, "Event stream disconnected")
|
||||
}
|
||||
|
||||
// MethodChannel.MethodCallHandler
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"initialize" -> handleInitialize(result)
|
||||
"dispose" -> handleDispose(result)
|
||||
"setProperty" -> handleSetProperty(call, result)
|
||||
"getProperty" -> handleGetProperty(call, result)
|
||||
"observeProperty" -> handleObserveProperty(call, result)
|
||||
"command" -> handleCommand(call, result)
|
||||
"setVisible" -> handleSetVisible(call, result)
|
||||
"isInitialized" -> result.success(playerCore?.isInitialized ?: false)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInitialize(result: MethodChannel.Result) {
|
||||
val currentActivity = activity
|
||||
if (currentActivity == null) {
|
||||
result.error("NO_ACTIVITY", "Activity not available", null)
|
||||
return
|
||||
}
|
||||
|
||||
if (playerCore?.isInitialized == true) {
|
||||
Log.d(TAG, "Already initialized")
|
||||
result.success(true)
|
||||
return
|
||||
}
|
||||
|
||||
currentActivity.runOnUiThread {
|
||||
try {
|
||||
playerCore = MpvPlayerCore(currentActivity).apply {
|
||||
delegate = this@MpvPlayerPlugin
|
||||
}
|
||||
val success = playerCore?.initialize() ?: false
|
||||
|
||||
// Start hidden
|
||||
playerCore?.setVisible(false)
|
||||
|
||||
Log.d(TAG, "Initialized: $success")
|
||||
result.success(success)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initialize: ${e.message}", e)
|
||||
result.error("INIT_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDispose(result: MethodChannel.Result) {
|
||||
activity?.runOnUiThread {
|
||||
playerCore?.dispose()
|
||||
playerCore = null
|
||||
Log.d(TAG, "Disposed")
|
||||
result.success(null)
|
||||
} ?: result.success(null)
|
||||
}
|
||||
|
||||
private fun handleSetProperty(call: MethodCall, result: MethodChannel.Result) {
|
||||
val name = call.argument<String>("name")
|
||||
val value = call.argument<String>("value")
|
||||
|
||||
if (name == null || value == null) {
|
||||
result.error("INVALID_ARGS", "Missing 'name' or 'value'", null)
|
||||
return
|
||||
}
|
||||
|
||||
playerCore?.setProperty(name, value)
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleGetProperty(call: MethodCall, result: MethodChannel.Result) {
|
||||
val name = call.argument<String>("name")
|
||||
|
||||
if (name == null) {
|
||||
result.error("INVALID_ARGS", "Missing 'name'", null)
|
||||
return
|
||||
}
|
||||
|
||||
val value = playerCore?.getProperty(name)
|
||||
result.success(value)
|
||||
}
|
||||
|
||||
private fun handleObserveProperty(call: MethodCall, result: MethodChannel.Result) {
|
||||
val name = call.argument<String>("name")
|
||||
val format = call.argument<String>("format")
|
||||
|
||||
if (name == null || format == null) {
|
||||
result.error("INVALID_ARGS", "Missing 'name' or 'format'", null)
|
||||
return
|
||||
}
|
||||
|
||||
playerCore?.observeProperty(name, format)
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleCommand(call: MethodCall, result: MethodChannel.Result) {
|
||||
val args = call.argument<List<String>>("args")
|
||||
|
||||
if (args == null) {
|
||||
result.error("INVALID_ARGS", "Missing 'args'", null)
|
||||
return
|
||||
}
|
||||
|
||||
playerCore?.command(args.toTypedArray())
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun handleSetVisible(call: MethodCall, result: MethodChannel.Result) {
|
||||
val visible = call.argument<Boolean>("visible")
|
||||
|
||||
if (visible == null) {
|
||||
result.error("INVALID_ARGS", "Missing 'visible'", null)
|
||||
return
|
||||
}
|
||||
|
||||
playerCore?.setVisible(visible)
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// MpvPlayerDelegate
|
||||
|
||||
override fun onPropertyChange(name: String, value: Any?) {
|
||||
eventSink?.success(
|
||||
mapOf(
|
||||
"type" to "property",
|
||||
"name" to name,
|
||||
"value" to value
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onEvent(name: String, data: Map<String, Any>?) {
|
||||
val event = mutableMapOf<String, Any>(
|
||||
"type" to "event",
|
||||
"name" to name
|
||||
)
|
||||
data?.let { event["data"] = it }
|
||||
eventSink?.success(event)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
package com.edde746.plezy
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import com.edde746.plezy.mpv.MpvPlayerPlugin
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
class MainActivity : FlutterActivity() {
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(MpvPlayerPlugin())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user