From e54bbd11f17b661d239ea595435c3338b2964406 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:15:02 +0200 Subject: [PATCH] feat(player): passthrough state tracking + sync-layer position/passthrough getters Includes the mpv passthrough rate-suspend wiring (passthrough cannot scaletempo; suspended while rate != 1.0) that the new getters report. --- lib/mpv/player/player.dart | 14 +++++++++++ lib/mpv/player/player_base.dart | 6 +++++ lib/mpv/player/player_native.dart | 41 ++++++++++++++++++++++++++----- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/lib/mpv/player/player.dart b/lib/mpv/player/player.dart index 0fa19c3b..0f008480 100644 --- a/lib/mpv/player/player.dart +++ b/lib/mpv/player/player.dart @@ -45,6 +45,20 @@ abstract class Player { /// Use these for reactive UI updates. PlayerStreams get streams; + /// Fresh playback position, stored on every native position tick. + /// + /// [PlayerState.position] is only refreshed at ~4Hz alongside the position + /// stream; use this for time-sensitive reads (sync anchors, drift math). + /// ExoPlayer's native tick is itself 250ms, which bounds freshness there. + Duration get currentPosition; + + /// Whether audio passthrough (bitstream output) is currently active. + /// + /// [setRate] with a non-1.0 rate tears passthrough down, so callers that + /// adjust the rate transiently (e.g. sync micro-corrections) must check + /// this first. + bool get audioPassthroughActive; + /// Texture ID for Flutter's Texture widget (video rendering). /// /// This is set by the platform implementation when video diff --git a/lib/mpv/player/player_base.dart b/lib/mpv/player/player_base.dart index 8741e720..00217955 100644 --- a/lib/mpv/player/player_base.dart +++ b/lib/mpv/player/player_base.dart @@ -31,6 +31,12 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { @override PlayerState get state => _state; + @override + Duration get currentPosition => Duration(milliseconds: _positionMs); + + @override + bool get audioPassthroughActive => false; + late final PlayerStreams _streams; @override diff --git a/lib/mpv/player/player_native.dart b/lib/mpv/player/player_native.dart index 59ac780e..50c4b4af 100644 --- a/lib/mpv/player/player_native.dart +++ b/lib/mpv/player/player_native.dart @@ -249,7 +249,16 @@ class PlayerNative extends PlayerBase { @override Future setRate(double rate) async { + // mpv cannot scaletempo compressed (spdif) audio and silently keeps + // playing at 1x, so suspend passthrough while the rate is not 1.0. + _currentRate = rate; + if (_passthroughActive && rate != 1.0) { + await _applyPassthrough(false); + } await setProperty('speed', rate.toString()); + if (_passthroughRequested && !_passthroughActive && rate == 1.0) { + await _applyPassthrough(true); + } } @override @@ -312,14 +321,34 @@ class PlayerNative extends PlayerBase { await invoke('setLogLevel', {'level': level}); } + bool _passthroughRequested = false; + bool _passthroughActive = false; + double _currentRate = 1.0; + + @override + bool get audioPassthroughActive => _passthroughActive; + + /// Codecs the platform can take as a bitstream. On iOS/tvOS compressed + /// audio goes through the system renderer, which only handles Dolby + /// Digital (Plus); desktop does real device passthrough for the full list. + static final String _passthroughCodecs = Platform.isIOS ? 'ac3,eac3' : 'ac3,eac3,dts,dts-hd,truehd'; + @override Future setAudioPassthrough(bool enabled) async { - if (enabled) { - await setProperty('audio-spdif', 'ac3,eac3,dts,dts-hd,truehd'); - await setProperty('audio-exclusive', 'yes'); - } else { - await setProperty('audio-spdif', ''); - await setProperty('audio-exclusive', 'no'); + _passthroughRequested = enabled; + // Deferred until the rate returns to 1.0 (see setRate). + if (enabled && _currentRate != 1.0) return; + await _applyPassthrough(enabled); + } + + Future _applyPassthrough(bool enabled) async { + _passthroughActive = enabled; + await setProperty('audio-spdif', enabled ? _passthroughCodecs : ''); + // audio-exclusive redirects coreaudio to coreaudio_exclusive on macOS + // (and exclusive WASAPI on Windows); on iOS/tvOS it is set once at + // playback start and must not be clobbered here. + if (!Platform.isIOS) { + await setProperty('audio-exclusive', enabled ? 'yes' : 'no'); } }