-
Notifications
You must be signed in to change notification settings - Fork 54
[video_player_avplay] Handle consecutive seekTo calls #1152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| ## NEXT | ||
|
|
||
| * Handle consecutive seekTo calls. | ||
|
|
||
| ## 0.8.18 | ||
|
|
||
| * Replace ecore-wl2 code with tizen window manager plugin. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'dart:async'; | ||
|
|
||
| import 'package:flutter/services.dart'; | ||
| import 'package:flutter/widgets.dart'; | ||
| import 'package:tizen_window_manager/tizen_window_manager.dart'; | ||
|
|
@@ -11,18 +13,31 @@ import '../video_player_platform_interface.dart'; | |
| import 'messages.g.dart'; | ||
| import 'tracks.dart'; | ||
|
|
||
| class _SeekOperation { | ||
| _SeekOperation(this.position); | ||
|
|
||
| final int position; | ||
| final Completer<void> completer = Completer<void>(); | ||
| } | ||
|
|
||
| /// An implementation of [VideoPlayerPlatform] that uses the | ||
| /// Pigeon-generated [VideoPlayerAvplayApi]. | ||
| class VideoPlayerTizen extends VideoPlayerPlatform { | ||
| final VideoPlayerAvplayApi _api = VideoPlayerAvplayApi(); | ||
|
|
||
| final Map<int, _SeekOperation> _activeSeeks = <int, _SeekOperation>{}; | ||
|
|
||
| final Map<int, List<_SeekOperation>> _pendingSeeks = | ||
| <int, List<_SeekOperation>>{}; | ||
|
|
||
| @override | ||
| Future<void> init() { | ||
| return _api.initialize(); | ||
| } | ||
|
|
||
| @override | ||
| Future<void> dispose(int playerId) { | ||
| _cancelAllSeeks(playerId); | ||
| return _api.dispose(PlayerMessage(playerId: playerId)); | ||
| } | ||
|
|
||
|
|
@@ -116,9 +131,85 @@ class VideoPlayerTizen extends VideoPlayerPlatform { | |
|
|
||
| @override | ||
| Future<void> seekTo(int playerId, Duration position) { | ||
| return _api.seekTo( | ||
| PositionMessage(playerId: playerId, position: position.inMilliseconds), | ||
| ); | ||
| final int targetPosition = position.inMilliseconds; | ||
|
|
||
| if (_activeSeeks.containsKey(playerId)) { | ||
| final _SeekOperation op = _SeekOperation(targetPosition); | ||
| _pendingSeeks.putIfAbsent(playerId, () => <_SeekOperation>[]); | ||
| _pendingSeeks[playerId]!.add(op); | ||
| return op.completer.future; | ||
| } | ||
|
|
||
| return _startSeek(playerId, targetPosition); | ||
| } | ||
|
|
||
| Future<void> _startSeek(int playerId, int position) async { | ||
| final _SeekOperation op = _SeekOperation(position); | ||
| _activeSeeks[playerId] = op; | ||
|
|
||
| try { | ||
| await _api.seekTo( | ||
| PositionMessage(playerId: playerId, position: position), | ||
| ); | ||
| } catch (e) { | ||
| _completeSeekWithError(playerId, e); | ||
|
Comment on lines
+154
to
+155
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the native seek rejects, such as for non-seekable content or an invalid player state, this catch completes an internal completer but then returns normally. The first caller therefore observes a successful Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
||
| void _handleSeekCompleted(int playerId) { | ||
| final _SeekOperation? op = _activeSeeks.remove(playerId); | ||
| if (op != null && !op.completer.isCompleted) { | ||
| op.completer.complete(); | ||
| } | ||
| _startPendingSeekIfAny(playerId); | ||
| } | ||
|
|
||
| void _startPendingSeekIfAny(int playerId) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the description, we only need seek the last position. |
||
| final List<_SeekOperation>? pending = _pendingSeeks[playerId]; | ||
| if (pending != null && pending.isNotEmpty) { | ||
| final _SeekOperation next = pending.removeAt(0); | ||
| _startSeek(playerId, next.position).then((_) { | ||
| if (!next.completer.isCompleted) { | ||
| next.completer.complete(); | ||
| } | ||
| }).catchError((Object e) { | ||
| if (!next.completer.isCompleted) { | ||
| next.completer.completeError(e); | ||
| } | ||
| }); | ||
| } else { | ||
| _pendingSeeks.remove(playerId); | ||
| } | ||
| } | ||
|
|
||
| void _completeSeekWithError(int playerId, Object error) { | ||
| final _SeekOperation? op = _activeSeeks.remove(playerId); | ||
| if (op != null && !op.completer.isCompleted) { | ||
| op.completer.completeError(error); | ||
| } | ||
| final List<_SeekOperation>? pending = _pendingSeeks.remove(playerId); | ||
| if (pending != null) { | ||
| for (final _SeekOperation p in pending) { | ||
| if (!p.completer.isCompleted) { | ||
| p.completer.completeError(error); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void _cancelAllSeeks(int playerId) { | ||
| final _SeekOperation? op = _activeSeeks.remove(playerId); | ||
| if (op != null && !op.completer.isCompleted) { | ||
| op.completer.completeError('Player was disposed.'); | ||
|
Comment on lines
+201
to
+203
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a player is disposed while its first seek is in flight, this reports cancellation through the active operation's completer, but that future was never returned—the caller received Useful? React with 👍 / 👎. |
||
| } | ||
| final List<_SeekOperation>? pending = _pendingSeeks.remove(playerId); | ||
| if (pending != null) { | ||
| for (final _SeekOperation p in pending) { | ||
| if (!p.completer.isCompleted) { | ||
| p.completer.completeError('Player was disposed.'); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @override | ||
|
|
@@ -443,7 +534,6 @@ class VideoPlayerTizen extends VideoPlayerPlatform { | |
| return VideoEvent(eventType: VideoEventType.completed); | ||
| case 'bufferingUpdate': | ||
| final int value = map['value']! as int; | ||
|
|
||
| return VideoEvent( | ||
| buffered: value, | ||
| eventType: VideoEventType.bufferingUpdate, | ||
|
|
@@ -475,6 +565,9 @@ class VideoPlayerTizen extends VideoPlayerPlatform { | |
| eventType: VideoEventType.manifestInfoUpdated, | ||
| manifestInfo: map['manifestInfo'] as String?, | ||
| ); | ||
| case 'seekCompleted': | ||
| _handleSeekCompleted(playerId); | ||
| return VideoEvent(eventType: VideoEventType.unknown); | ||
| default: | ||
| return VideoEvent(eventType: VideoEventType.unknown); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When several
seekTocalls arrive while one is active, every requested position is appended and later issued as a separate native seek. During rapid scrubbing this replays stale intermediate positions before reaching the latest request, causing latency proportional to the number of calls and defeating the intended latest-pending-position behavior. Retain only the newest pending target while preserving completion of all associated futures.Useful? React with 👍 / 👎.