feat(music): mpv audio playback engine with gapless queue service
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.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
#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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user