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.
This commit is contained in:
edde746
2026-06-11 09:16:01 +02:00
parent 38afdab2e4
commit e54bbd11f1
3 changed files with 55 additions and 6 deletions
+14
View File
@@ -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
+6
View File
@@ -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
+35 -6
View File
@@ -249,7 +249,16 @@ class PlayerNative extends PlayerBase {
@override
Future<void> 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<void> 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<void> _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');
}
}