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:
edde746
2026-07-05 19:26:28 +02:00
parent 05a631415e
commit 422db75b5b
41 changed files with 3353 additions and 269 deletions
+56 -36
View File
@@ -73,7 +73,7 @@ void EnsureMpvInnerSubclassed(HWND host) {
} // namespace
MpvPlayer::MpvPlayer() {}
MpvPlayer::MpvPlayer(bool audio_only) : audio_only_(audio_only) {}
MpvPlayer::~MpvPlayer() { Dispose(); }
@@ -88,31 +88,43 @@ bool MpvPlayer::Initialize(HWND view) {
return false;
}
// Create a child window for mpv to render into, parented to the Flutter
// |view|. The video child then sits in the view's own per-window layer
// stack, above the view's (never-painted) layer-1 content and below the
// engine's topmost DComp visual carrying the UI. WS_CLIPSIBLINGS keeps it
// from painting over neighboring view children. Mouse input over the video
// is delivered to mpv's own inner window (on mpv's thread); the subclass
// installed in EnsureMpvInnerSubclassed forwards it back to the view.
hwnd_ = ::CreateWindowExW(
WS_EX_NOPARENTNOTIFY, L"STATIC", L"", WS_CHILD | WS_CLIPSIBLINGS, 0, 0, 100, 100, view, nullptr,
GetModuleHandle(nullptr), nullptr);
if (!hwnd_) {
mpv_destroy(mpv_);
mpv_ = nullptr;
return false;
}
g_forward_target_view = view;
if (audio_only_) {
// Windowless music core: no HWND, no VO, no video decode. vid=no keeps
// embedded cover art from ever becoming a video track, and
// force-window/audio-display make sure mpv never opens a video output
// for it either.
mpv_set_option_string(mpv_, "vid", "no");
mpv_set_option_string(mpv_, "force-window", "no");
mpv_set_option_string(mpv_, "audio-display", "no");
mpv_set_option_string(mpv_, "gapless-audio", "weak");
} else {
// Create a child window for mpv to render into, parented to the Flutter
// |view|. The video child then sits in the view's own per-window layer
// stack, above the view's (never-painted) layer-1 content and below the
// engine's topmost DComp visual carrying the UI. WS_CLIPSIBLINGS keeps it
// from painting over neighboring view children. Mouse input over the video
// is delivered to mpv's own inner window (on mpv's thread); the subclass
// installed in EnsureMpvInnerSubclassed forwards it back to the view.
hwnd_ = ::CreateWindowExW(
WS_EX_NOPARENTNOTIFY, L"STATIC", L"", WS_CHILD | WS_CLIPSIBLINGS, 0, 0, 100, 100, view, nullptr,
GetModuleHandle(nullptr), nullptr);
if (!hwnd_) {
mpv_destroy(mpv_);
mpv_ = nullptr;
return false;
}
g_forward_target_view = view;
// Set the wid option to embed mpv in our window.
int64_t wid = reinterpret_cast<int64_t>(hwnd_);
mpv_set_option(mpv_, "wid", MPV_FORMAT_INT64, &wid);
// Set the wid option to embed mpv in our window.
int64_t wid = reinterpret_cast<int64_t>(hwnd_);
mpv_set_option(mpv_, "wid", MPV_FORMAT_INT64, &wid);
mpv_set_option_string(mpv_, "vo", "gpu-next");
mpv_set_option_string(mpv_, "gpu-api", "auto");
// hwdec is set from Flutter via setProperty based on user preference
}
// Configure mpv for embedded playback.
mpv_set_option_string(mpv_, "vo", "gpu-next");
mpv_set_option_string(mpv_, "gpu-api", "auto");
// hwdec is set from Flutter via setProperty based on user preference
mpv_set_option_string(mpv_, "keep-open", "yes");
mpv_set_option_string(mpv_, "idle", "yes");
mpv_set_option_string(mpv_, "input-default-bindings", "no");
@@ -122,12 +134,14 @@ bool MpvPlayer::Initialize(HWND view) {
mpv_set_option_string(mpv_, "input-media-keys", "no");
mpv_set_option_string(mpv_, "osc", "no");
// Let mpv use display/context detection instead of forcing HDR signaling.
mpv_set_option_string(mpv_, "target-colorspace-hint", "auto");
if (!audio_only_) {
// Let mpv use display/context detection instead of forcing HDR signaling.
mpv_set_option_string(mpv_, "target-colorspace-hint", "auto");
// Fallback tone mapping when display doesn't support HDR
mpv_set_option_string(mpv_, "tone-mapping", "auto");
mpv_set_option_string(mpv_, "hdr-compute-peak", "auto");
// Fallback tone mapping when display doesn't support HDR
mpv_set_option_string(mpv_, "tone-mapping", "auto");
mpv_set_option_string(mpv_, "hdr-compute-peak", "auto");
}
// When WASAPI becomes unavailable (sleep, device unplug), fall back to null
// audio output instead of permanently dropping the audio track. Recovery is
@@ -140,15 +154,19 @@ bool MpvPlayer::Initialize(HWND view) {
// Initialize mpv.
int err = mpv_initialize(mpv_);
if (err < 0) {
::DestroyWindow(hwnd_);
hwnd_ = nullptr;
if (hwnd_) {
::DestroyWindow(hwnd_);
hwnd_ = nullptr;
}
mpv_destroy(mpv_);
mpv_ = nullptr;
return false;
}
// Observe video-params/sig-peak for HDR detection
mpv_observe_property(mpv_, 0, "video-params/sig-peak", MPV_FORMAT_DOUBLE);
// Observe video-params/sig-peak for HDR detection (video core only).
if (!audio_only_) {
mpv_observe_property(mpv_, 0, "video-params/sig-peak", MPV_FORMAT_DOUBLE);
}
mpv_observe_property(mpv_, 0, "current-ao", MPV_FORMAT_STRING);
// Native observation so audio recovery doesn't depend on the Dart side
// choosing to observe the device list.
@@ -192,11 +210,13 @@ void MpvPlayer::Dispose() {
if (hwnd_) {
::DestroyWindow(hwnd_);
hwnd_ = nullptr;
}
// The subclassed inner window died with hwnd_; clear the forwarding state.
g_mpv_inner_hwnd = nullptr;
g_mpv_inner_original_proc = nullptr;
// The subclassed inner window died with hwnd_; clear the forwarding
// state. Only the owner of the window may do this: the audio-only core
// (which never has an hwnd_) must not wipe the video instance's state.
g_mpv_inner_hwnd = nullptr;
g_mpv_inner_original_proc = nullptr;
}
observed_properties_.clear();
}