Audio-only mpv core on every platform (dedicated com.plezy/mpv_audio_player channels): parameterized android/windows/ linux mpv plugins and a new apple MpvAudioPlayerCore, all skipping video/window paths (vid=no, audio-display=no, gapless-audio=weak). MusicPlaybackService drives an in-memory queue with shuffle/repeat, file-loaded-event gapless arming (property edges coalesce and the android bridge drops them), per-track progress reporting, OS media controls, audio focus, sleep timer, and error auto-skip. PlaybackCoordinator enforces one live native player: starting video disposes the audio core first.
128 lines
3.8 KiB
Swift
128 lines
3.8 KiB
Swift
#if os(iOS) || os(tvOS)
|
|
import Flutter
|
|
#elseif os(macOS)
|
|
import FlutterMacOS
|
|
#endif
|
|
|
|
/// Flutter plugin for the dedicated audio-only mpv core (music playback).
|
|
///
|
|
/// Registers `com.plezy/mpv_audio_player` + `/events` and delegates all
|
|
/// generic property/command/observe traffic to the shared [MpvPluginShared]
|
|
/// handlers. There is no render layer, so the visual hooks are no-ops and
|
|
/// `setVisible`/`updateFrame` succeed without doing anything. Shared across
|
|
/// iOS, tvOS, and macOS — unlike the video plugin there is nothing
|
|
/// platform-specific beyond the messenger accessor.
|
|
class MpvAudioPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginShared {
|
|
|
|
private var playerCore: MpvAudioPlayerCore?
|
|
var eventSink: FlutterEventSink?
|
|
var nameToId: [String: Int] = [:]
|
|
|
|
// MpvPluginShared conformance — the audio core has no visual surface.
|
|
var coreBase: MpvPlayerCoreBase? { playerCore }
|
|
func setPlayerVisible(_ visible: Bool, restoreOnWindowVisible _: Bool) {}
|
|
func updatePlayerFrame() {}
|
|
func didSetPauseProperty(value _: String) {}
|
|
|
|
// MARK: - FlutterPlugin Registration
|
|
|
|
static func register(with registrar: FlutterPluginRegistrar) {
|
|
#if os(macOS)
|
|
let messenger = registrar.messenger
|
|
#else
|
|
let messenger = registrar.messenger()
|
|
#endif
|
|
|
|
let methodChannel = FlutterMethodChannel(
|
|
name: "com.plezy/mpv_audio_player",
|
|
binaryMessenger: messenger
|
|
)
|
|
let eventChannel = FlutterEventChannel(
|
|
name: "com.plezy/mpv_audio_player/events",
|
|
binaryMessenger: messenger
|
|
)
|
|
|
|
let instance = MpvAudioPlayerPlugin()
|
|
registrar.addMethodCallDelegate(instance, channel: methodChannel)
|
|
eventChannel.setStreamHandler(instance)
|
|
}
|
|
|
|
// MARK: - FlutterStreamHandler
|
|
|
|
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
|
|
-> FlutterError?
|
|
{
|
|
self.eventSink = events
|
|
return nil
|
|
}
|
|
|
|
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
|
self.eventSink = nil
|
|
return nil
|
|
}
|
|
|
|
// MARK: - FlutterPlugin Method Handler
|
|
|
|
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
switch call.method {
|
|
case "initialize":
|
|
handleInitialize(result: result)
|
|
case "dispose":
|
|
handleDispose(result: result)
|
|
case "setProperty":
|
|
handleSetProperty(call: call, result: result)
|
|
case "getProperty":
|
|
handleGetProperty(call: call, result: result)
|
|
case "observeProperty":
|
|
handleObserveProperty(call: call, result: result)
|
|
case "command":
|
|
handleCommand(call: call, result: result)
|
|
case "isInitialized":
|
|
result(playerCore?.isInitialized ?? false)
|
|
case "setVisible", "updateFrame":
|
|
// No render layer — succeed so shared Dart call sites stay unconditional.
|
|
result(nil)
|
|
case "setLogLevel":
|
|
handleSetLogLevel(call: call, result: result)
|
|
default:
|
|
result(FlutterMethodNotImplemented)
|
|
}
|
|
}
|
|
|
|
private func handleInitialize(result: @escaping FlutterResult) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self else {
|
|
result(FlutterError(code: "ERROR", message: "Plugin deallocated", details: nil))
|
|
return
|
|
}
|
|
|
|
if self.playerCore?.isInitialized == true {
|
|
result(true)
|
|
return
|
|
}
|
|
|
|
let core = MpvAudioPlayerCore()
|
|
core.delegate = self
|
|
|
|
guard core.initialize() else {
|
|
result(
|
|
FlutterError(
|
|
code: "MPV_INIT_FAILED", message: "Failed to initialize MPV audio core", details: nil))
|
|
return
|
|
}
|
|
|
|
self.playerCore = core
|
|
result(true)
|
|
}
|
|
}
|
|
|
|
private func handleDispose(result: @escaping FlutterResult) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self else { result(nil); return }
|
|
self.playerCore?.dispose()
|
|
self.playerCore = nil
|
|
result(nil)
|
|
}
|
|
}
|
|
}
|