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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user