fix(player): stabilize Apple timeline progress

This commit is contained in:
edde746
2026-05-08 12:34:07 +02:00
parent 51a8b971f6
commit 5d2302b322
4 changed files with 66 additions and 9 deletions
+4 -2
View File
@@ -104,6 +104,8 @@ class PlayerNative extends PlayerBase {
}) async {
if (disposed) return;
await _ensureInitialized();
final startPosition = media.start ?? Duration.zero;
resetPlaybackProgress(startPosition);
setSeekable(false);
await setVisible(true);
@@ -114,8 +116,8 @@ class PlayerNative extends PlayerBase {
}
// 'start' must be set before loadfile.
if (media.start != null && media.start!.inSeconds > 0) {
await setProperty('start', media.start!.inSeconds.toString());
if (startPosition.inSeconds > 0) {
await setProperty('start', (startPosition.inMilliseconds / 1000.0).toString());
} else {
await setProperty('start', 'none');
}
@@ -139,6 +139,11 @@ class _TimelineSliderState extends State<TimelineSlider> {
// Calculate the actual track width by subtracting the thumb padding on each side
final trackWidth = sliderWidth - 2 * _sliderPadding;
final durationMs = widget.duration.inMilliseconds;
final max = durationMs > 0 ? durationMs.toDouble() : 0.0;
final displayValue = max > 0
? (_dragValue ?? widget.position.inMilliseconds.toDouble()).clamp(0.0, max).toDouble()
: 0.0;
final displayPosition = Duration(milliseconds: displayValue.toInt());
// Resolve tooltip position (drag takes priority over hover)
Widget? tooltip;
@@ -146,9 +151,9 @@ class _TimelineSliderState extends State<TimelineSlider> {
if (_dragValue != null) {
// Convert drag value (ms) to a 0..1 fraction, then map to pixel
// position on the track (offset by padding to align with the slider)
final fraction = (_dragValue! / durationMs).clamp(0.0, 1.0);
final fraction = (displayValue / durationMs).clamp(0.0, 1.0);
final px = _sliderPadding + fraction * trackWidth;
tooltip = _buildTooltip(sliderWidth, px, Duration(milliseconds: _dragValue!.toInt()));
tooltip = _buildTooltip(sliderWidth, px, displayPosition);
} else if (_mousePosition != null) {
// Convert mouse pixel position to a 0..1 fraction of the track
// (subtract padding to get position relative to track start),
@@ -160,9 +165,9 @@ class _TimelineSliderState extends State<TimelineSlider> {
// Preview thumbnail at the current playback position while the
// user holds a dpad/keyboard direction. The decoder lags behind
// rapid seeks, so the BIF thumbnail is the only live feedback.
final fraction = (widget.position.inMilliseconds / durationMs).clamp(0.0, 1.0);
final fraction = (displayValue / durationMs).clamp(0.0, 1.0);
final px = _sliderPadding + fraction * trackWidth;
tooltip = _buildTooltip(sliderWidth, px, widget.position);
tooltip = _buildTooltip(sliderWidth, px, displayPosition);
}
}
@@ -205,9 +210,9 @@ class _TimelineSliderState extends State<TimelineSlider> {
label: t.videoControls.timelineSlider,
slider: true,
child: Slider(
value: widget.duration.inMilliseconds > 0 ? widget.position.inMilliseconds.toDouble() : 0.0,
value: displayValue,
min: 0.0,
max: widget.duration.inMilliseconds.toDouble(),
max: max,
onChanged: (value) {
setState(() => _dragValue = value);
widget.onSeek(Duration(milliseconds: value.toInt()));
@@ -57,7 +57,7 @@ class MpvPlayerCoreBase: NSObject {
/// Properties that must still flow to Dart while backgrounded (state-critical).
private static let criticalProperties: Set<String> = [
"pause", "eof-reached", "paused-for-cache",
"pause", "eof-reached", "paused-for-cache", "time-pos", "duration", "seekable",
]
private static let internalSigPeakObserverId: UInt64 = UInt64.max - 1
+50
View File
@@ -139,5 +139,55 @@ void main() {
expect((customPaint.painter! as BufferRangePainter).chapters, isEmpty);
});
testWidgets('clamps stale position beyond duration before building slider', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 400,
child: TimelineSlider(
position: const Duration(minutes: 12),
duration: const Duration(minutes: 10),
chapters: const [],
chaptersLoaded: true,
onSeek: (_) {},
onSeekEnd: (_) {},
),
),
),
),
);
final slider = tester.widget<Slider>(find.byType(Slider));
expect(slider.value, const Duration(minutes: 10).inMilliseconds.toDouble());
expect(slider.max, const Duration(minutes: 10).inMilliseconds.toDouble());
});
testWidgets('clamps stale position when duration is unknown', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 400,
child: TimelineSlider(
position: const Duration(minutes: 12),
duration: Duration.zero,
chapters: const [],
chaptersLoaded: true,
onSeek: (_) {},
onSeekEnd: (_) {},
),
),
),
),
);
final slider = tester.widget<Slider>(find.byType(Slider));
expect(slider.value, 0.0);
expect(slider.max, 0.0);
});
});
}