From ef91156827a3fda4d462e994d57dbddb693bbdc9 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:08:46 +0200 Subject: [PATCH] perf(music): observe only the properties the audio-only core needs The audio-only core reused the video core's property registrations wholesale, so every music session observed track-list, demuxer-cache-state, audio-device-list, audio-device, secondary-sid, seekable, paused-for-cache, volume, speed, aid and sid. libmpv sends an initial notification plus an event per edge for each one, and three of them decode structured nodes into track and device models nothing on the music path reads. Register time-pos, duration, pause, eof-reached and playlist-pos there; the video path keeps the full set. Also drop the post-initialization `gapless-audio` write: every native audio core already sets it before mpv_initialize, which is where libmpv wants initial configuration. --- lib/mpv/player/player_native.dart | 37 +++++++++++++++---------- test/mpv/property_observation_test.dart | 13 +++++++++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/lib/mpv/player/player_native.dart b/lib/mpv/player/player_native.dart index e702cf947..061403f15 100644 --- a/lib/mpv/player/player_native.dart +++ b/lib/mpv/player/player_native.dart @@ -274,31 +274,38 @@ class PlayerNative extends PlayerBase { // Subscribe to MPV properties before flipping `initialized` so partial // failures don't leave us in a half-initialized state that the memoized // future would falsely treat as ready. - await observeCoreProperties(trackListFormat: _nodeFormat); - await observeProperty('secondary-sid', 'string'); - await observeProperty('demuxer-cache-state', _nodeFormat); - await observeProperty('audio-device-list', _nodeFormat); - await observeProperty('audio-device', 'string'); - if (audioOnly) { - // Debug aid only: raw playlist positions in the log trail. Gapless - // advance DETECTION rides the file-loaded event instead — see - // _handleAudioFileLoaded for why property edges are unreliable. + // Only what the music service consumes (position/duration/playing/ + // completed). Every observation costs an initial notification plus a + // channel event per edge; the video set's track-list, + // demuxer-cache-state, and audio-device-list decode structured nodes + // into models nobody on the music path reads. + await observeProperty('time-pos', 'double'); + await observeProperty('duration', 'double'); + await observeProperty('pause', 'flag'); + await observeProperty('eof-reached', 'flag'); + // Debug aid plus the freeze point for the outgoing track's final + // position (see handlePropertyChange). Gapless advance DETECTION + // rides the file-loaded event instead — see _handleAudioFileLoaded + // for why property edges are unreliable. await observeProperty('playlist-pos', _nodeFormat); - // The Apple audio core sets this at context init; set it defensively - // here so every mpv audio backend behaves identically. Direct invoke — - // setProperty() would await _ensureInitialized and deadlock on the - // memoized future of this very _doInitialize call. - await invoke('setProperty', {'name': 'gapless-audio', 'value': 'weak'}); // ao_audiounit requests mixWithOthers unless audio-exclusive is set, // and a mixable session disqualifies the app from iOS Now Playing — // no lock-screen/headphone controls (#1921). Same contract as the // video path (VideoPlayerScreen sets it at playback start). iOS-only: // elsewhere audio-exclusive means exclusive device access (hog-mode - // CoreAudio on macOS, exclusive WASAPI on Windows). + // CoreAudio on macOS, exclusive WASAPI on Windows). Direct invoke — + // setProperty() would await _ensureInitialized and deadlock on the + // memoized future of this very _doInitialize call. if (Platform.isIOS) { await invoke('setProperty', {'name': 'audio-exclusive', 'value': 'yes'}); } + } else { + await observeCoreProperties(trackListFormat: _nodeFormat); + await observeProperty('secondary-sid', 'string'); + await observeProperty('demuxer-cache-state', _nodeFormat); + await observeProperty('audio-device-list', _nodeFormat); + await observeProperty('audio-device', 'string'); } if (_nativeCoreUnavailable) throw StateError('Player was disposed during initialization'); diff --git a/test/mpv/property_observation_test.dart b/test/mpv/property_observation_test.dart index 128187ac1..c423e28d0 100644 --- a/test/mpv/property_observation_test.dart +++ b/test/mpv/property_observation_test.dart @@ -122,4 +122,17 @@ void main() { expect((call.arguments as Map)['format'], structuredFormat); } }); + + test('audio-only mpv registers only the properties the music path consumes', () async { + final player = PlayerNative.audio(); + final observations = await capturedObservations( + channelName: 'com.plezy/mpv_audio_player', + initialize: () => player.setLogLevel('warn'), // forces _ensureInitialized + dispose: () => player.dispose(), + ); + + expect(names(observations), {'time-pos', 'duration', 'pause', 'eof-reached', 'playlist-pos'}); + final playlistPos = observations.singleWhere((call) => (call.arguments as Map)['name'] == 'playlist-pos'); + expect((playlistPos.arguments as Map)['format'], Platform.isAndroid ? 'string' : 'node'); + }); }