From e56086814faca65c3209f52cc5b893b9a19d4eec Mon Sep 17 00:00:00 2001 From: "yying.jin" Date: Wed, 16 Sep 2026 15:51:49 +0800 Subject: [PATCH 1/6] Handle consecutive seekTo calls --- packages/video_player_avplay/CHANGELOG.md | 4 + .../lib/src/video_player_tizen.dart | 101 +++++++++++++++++- .../tizen/src/media_player.cc | 43 +++++--- .../tizen/src/media_player.h | 1 + .../tizen/src/plus_player.cc | 40 ++++--- .../tizen/src/plus_player.h | 1 + .../tizen/src/video_player.cc | 8 ++ .../tizen/src/video_player.h | 1 + 8 files changed, 165 insertions(+), 34 deletions(-) diff --git a/packages/video_player_avplay/CHANGELOG.md b/packages/video_player_avplay/CHANGELOG.md index 377203b7d..09f9a2f34 100644 --- a/packages/video_player_avplay/CHANGELOG.md +++ b/packages/video_player_avplay/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Handle consecutive seekTo calls. + ## 0.8.18 * Replace ecore-wl2 code with tizen window manager plugin. diff --git a/packages/video_player_avplay/lib/src/video_player_tizen.dart b/packages/video_player_avplay/lib/src/video_player_tizen.dart index 5df865ef4..3a9e6491a 100644 --- a/packages/video_player_avplay/lib/src/video_player_tizen.dart +++ b/packages/video_player_avplay/lib/src/video_player_tizen.dart @@ -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,11 +13,23 @@ import '../video_player_platform_interface.dart'; import 'messages.g.dart'; import 'tracks.dart'; +class _SeekOperation { + _SeekOperation(this.position); + + final int position; + final Completer completer = Completer(); +} + /// An implementation of [VideoPlayerPlatform] that uses the /// Pigeon-generated [VideoPlayerAvplayApi]. class VideoPlayerTizen extends VideoPlayerPlatform { final VideoPlayerAvplayApi _api = VideoPlayerAvplayApi(); + final Map _activeSeeks = {}; + + final Map> _pendingSeeks = + >{}; + @override Future init() { return _api.initialize(); @@ -23,6 +37,7 @@ class VideoPlayerTizen extends VideoPlayerPlatform { @override Future dispose(int playerId) { + _cancelAllSeeks(playerId); return _api.dispose(PlayerMessage(playerId: playerId)); } @@ -116,9 +131,85 @@ class VideoPlayerTizen extends VideoPlayerPlatform { @override Future 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 _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); + } + } + + 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) { + 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.'); + } + 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); } diff --git a/packages/video_player_avplay/tizen/src/media_player.cc b/packages/video_player_avplay/tizen/src/media_player.cc index bd11f7536..dac28e084 100644 --- a/packages/video_player_avplay/tizen/src/media_player.cc +++ b/packages/video_player_avplay/tizen/src/media_player.cc @@ -231,7 +231,7 @@ bool MediaPlayer::Play() { } if (state == PLAYER_STATE_PLAYING) { LOG_INFO("[MediaPlayer] Player already playing."); - return false; + return true; } ret = player_start(player_); if (ret != PLAYER_ERROR_NONE) { @@ -256,7 +256,7 @@ bool MediaPlayer::Pause() { } if (state != PLAYER_STATE_PLAYING) { LOG_INFO("[MediaPlayer] Player not playing."); - return false; + return true; } ret = player_pause(player_); if (ret != PLAYER_ERROR_NONE) { @@ -306,11 +306,18 @@ bool MediaPlayer::SetPlaybackSpeed(double speed) { bool MediaPlayer::SeekTo(int64_t position, SeekCompletedCallback callback) { LOG_INFO("[MediaPlayer] position: %lld.", position); + if (is_seeking_) { + LOG_ERROR("[MediaPlayer] Seek is already in progress."); + return false; + } + on_seek_completed_ = std::move(callback); + is_seeking_ = true; int ret = player_set_play_position(player_, position, true, OnSeekCompleted, this); if (ret != PLAYER_ERROR_NONE) { on_seek_completed_ = nullptr; + is_seeking_ = false; LOG_ERROR("[MediaPlayer] player_set_play_position failed: %s.", get_error_message(ret)); return false; @@ -705,10 +712,12 @@ void MediaPlayer::OnSeekCompleted(void *user_data) { LOG_INFO("[MediaPlayer] Seek completed."); MediaPlayer *self = static_cast(user_data); + self->is_seeking_ = false; if (self->on_seek_completed_) { self->on_seek_completed_(); self->on_seek_completed_ = nullptr; } + self->SendSeekCompleted(); } void MediaPlayer::OnPlayCompleted(void *user_data) { @@ -809,36 +818,40 @@ bool MediaPlayer::StopAndDestroy() { return false; } + bool success = true; is_buffering_ = false; + on_seek_completed_ = nullptr; + is_seeking_ = false; player_state_e player_state = PLAYER_STATE_NONE; int ret = player_get_state(player_, &player_state); if (ret != PLAYER_ERROR_NONE) { LOG_ERROR("[MediaPlayer] player_get_state failed: %s.", get_error_message(ret)); - return false; - } - if (player_state == PLAYER_STATE_NONE || player_state == PLAYER_STATE_IDLE) { - LOG_INFO("[MediaPlayer] Player already stop, nothing to do."); - return true; + success = false; } - if (player_stop(player_) != PLAYER_ERROR_NONE) { - LOG_ERROR("[MediaPlayer] Player fail to stop."); - return false; + if (player_state == PLAYER_STATE_PLAYING || + player_state == PLAYER_STATE_PAUSED) { + if (player_stop(player_) != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] Player fail to stop."); + success = false; + } } - if (player_unprepare(player_) != PLAYER_ERROR_NONE) { - LOG_ERROR("[MediaPlayer] Player fail to unprepare."); - return false; + if (player_state != PLAYER_STATE_NONE && player_state != PLAYER_STATE_IDLE) { + if (player_unprepare(player_) != PLAYER_ERROR_NONE) { + LOG_ERROR("[MediaPlayer] Player fail to unprepare."); + success = false; + } } if (player_destroy(player_) != PLAYER_ERROR_NONE) { LOG_ERROR("[MediaPlayer] Player fail to destroy."); - return false; + success = false; } player_ = nullptr; - return true; + return success; } bool MediaPlayer::Suspend() { diff --git a/packages/video_player_avplay/tizen/src/media_player.h b/packages/video_player_avplay/tizen/src/media_player.h index a7d4a71d6..2fa58d68b 100644 --- a/packages/video_player_avplay/tizen/src/media_player.h +++ b/packages/video_player_avplay/tizen/src/media_player.h @@ -78,6 +78,7 @@ class MediaPlayer : public VideoPlayer { std::unique_ptr drm_manager_; bool is_buffering_ = false; SeekCompletedCallback on_seek_completed_; + bool is_seeking_ = false; std::string url_; player_state_e pre_state_; int64_t pre_playing_time_; diff --git a/packages/video_player_avplay/tizen/src/plus_player.cc b/packages/video_player_avplay/tizen/src/plus_player.cc index 43be1db01..b4d67b692 100644 --- a/packages/video_player_avplay/tizen/src/plus_player.cc +++ b/packages/video_player_avplay/tizen/src/plus_player.cc @@ -194,6 +194,8 @@ int64_t PlusPlayer::Create(const std::string &uri, void PlusPlayer::Dispose() { LOG_INFO("[PlusPlayer] Player disposing."); + on_seek_completed_ = nullptr; + is_seeking_ = false; ClearUpEventChannel(); } @@ -231,7 +233,7 @@ bool PlusPlayer::Play() { } return true; } - return false; + return true; } bool PlusPlayer::Activate() { @@ -285,7 +287,7 @@ bool PlusPlayer::Pause() { if (state != plusplayer::State::kPlaying) { LOG_INFO("[PlusPlayer] Player not playing."); - return false; + return true; } if (!::Pause(player_)) { @@ -339,14 +341,16 @@ bool PlusPlayer::SeekTo(int64_t position, SeekCompletedCallback callback) { return false; } - if (on_seek_completed_) { + if (is_seeking_) { LOG_ERROR("[PlusPlayer] Player is already seeking."); return false; } on_seek_completed_ = std::move(callback); + is_seeking_ = true; if (!Seek(player_, position)) { on_seek_completed_ = nullptr; + is_seeking_ = false; LOG_ERROR("[PlusPlayer] Player fail to seek."); return false; } @@ -818,25 +822,29 @@ bool PlusPlayer::StopAndClose() { return false; } + bool success = true; is_buffering_ = false; - plusplayer::State player_state = GetState(player_); - if (player_state < plusplayer::State::kReady) { - LOG_INFO("[PlusPlayer] Player already stop, nothing to do."); - return true; + if (is_seeking_) { + SendSeekCompleted(); } + on_seek_completed_ = nullptr; + is_seeking_ = false; + plusplayer::State player_state = GetState(player_); if (drm_manager_) { drm_manager_->StopDrmSession(); } - if (!::Stop(player_)) { - LOG_ERROR("[PlusPlayer] Player fail to stop."); - return false; - } + if (player_state != plusplayer::State::kNone) { + if (!::Stop(player_)) { + LOG_ERROR("[PlusPlayer] Player fail to stop."); + success = false; + } - if (!::Close(player_)) { - LOG_ERROR("[PlusPlayer] Player fail to close."); - return false; + if (!::Close(player_)) { + LOG_ERROR("[PlusPlayer] Player fail to close."); + success = false; + } } if (drm_manager_) { @@ -844,7 +852,7 @@ bool PlusPlayer::StopAndClose() { drm_manager_.reset(); } - return true; + return success; } bool PlusPlayer::Suspend() { @@ -1173,10 +1181,12 @@ void PlusPlayer::OnSeekDone(void *user_data) { LOG_INFO("[PlusPlayer] Seek completed."); PlusPlayer *self = reinterpret_cast(user_data); + self->is_seeking_ = false; if (self->on_seek_completed_) { self->on_seek_completed_(); self->on_seek_completed_ = nullptr; } + self->SendSeekCompleted(); } void PlusPlayer::OnEos(void *user_data) { diff --git a/packages/video_player_avplay/tizen/src/plus_player.h b/packages/video_player_avplay/tizen/src/plus_player.h index a510c1334..987a2e87d 100644 --- a/packages/video_player_avplay/tizen/src/plus_player.h +++ b/packages/video_player_avplay/tizen/src/plus_player.h @@ -109,6 +109,7 @@ class PlusPlayer : public VideoPlayer { bool is_buffering_ = false; bool is_prebuffer_mode_ = false; SeekCompletedCallback on_seek_completed_; + bool is_seeking_ = false; std::unique_ptr memento_ = nullptr; std::string url_; std::unique_ptr device_proxy_ = nullptr; diff --git a/packages/video_player_avplay/tizen/src/video_player.cc b/packages/video_player_avplay/tizen/src/video_player.cc index de6cbec1e..b641be380 100644 --- a/packages/video_player_avplay/tizen/src/video_player.cc +++ b/packages/video_player_avplay/tizen/src/video_player.cc @@ -145,6 +145,14 @@ void VideoPlayer::SendBufferingEnd() { PushEvent(flutter::EncodableValue(result)); } +void VideoPlayer::SendSeekCompleted() { + flutter::EncodableMap result = { + {flutter::EncodableValue("event"), + flutter::EncodableValue("seekCompleted")}, + }; + PushEvent(flutter::EncodableValue(result)); +} + void VideoPlayer::SendSubtitleUpdate(int32_t duration, flutter::EncodableList texts_info, flutter::EncodableMap picture_info) { diff --git a/packages/video_player_avplay/tizen/src/video_player.h b/packages/video_player_avplay/tizen/src/video_player.h index a9a89308e..6341d329f 100644 --- a/packages/video_player_avplay/tizen/src/video_player.h +++ b/packages/video_player_avplay/tizen/src/video_player.h @@ -81,6 +81,7 @@ class VideoPlayer { void SendBufferingStart(); void SendBufferingUpdate(int32_t value); void SendBufferingEnd(); + void SendSeekCompleted(); void SendSubtitleUpdate( int32_t duration, flutter::EncodableList texts_info, flutter::EncodableMap picture_info = flutter::EncodableMap{}); From b68023e9407a138b33e94b2667d50cddc664674a Mon Sep 17 00:00:00 2001 From: "yying.jin" Date: Thu, 17 Sep 2026 19:03:37 +0800 Subject: [PATCH 2/6] fix review issues --- .../lib/src/video_player_tizen.dart | 107 +++++++++++------- 1 file changed, 66 insertions(+), 41 deletions(-) diff --git a/packages/video_player_avplay/lib/src/video_player_tizen.dart b/packages/video_player_avplay/lib/src/video_player_tizen.dart index 3a9e6491a..8fd1741e8 100644 --- a/packages/video_player_avplay/lib/src/video_player_tizen.dart +++ b/packages/video_player_avplay/lib/src/video_player_tizen.dart @@ -18,6 +18,35 @@ class _SeekOperation { final int position; final Completer completer = Completer(); + final List> _coalesced = >[]; + + Future attach() { + final Completer c = Completer(); + _coalesced.add(c); + return c.future; + } + + void _complete() { + if (!completer.isCompleted) { + completer.complete(); + } + for (final Completer c in _coalesced) { + if (!c.isCompleted) { + c.complete(); + } + } + } + + void _completeError(Object error) { + if (!completer.isCompleted) { + completer.completeError(error); + } + for (final Completer c in _coalesced) { + if (!c.isCompleted) { + c.completeError(error); + } + } + } } /// An implementation of [VideoPlayerPlatform] that uses the @@ -27,8 +56,7 @@ class VideoPlayerTizen extends VideoPlayerPlatform { final Map _activeSeeks = {}; - final Map> _pendingSeeks = - >{}; + final Map _pendingSeeks = {}; @override Future init() { @@ -134,10 +162,17 @@ class VideoPlayerTizen extends VideoPlayerPlatform { final int targetPosition = position.inMilliseconds; if (_activeSeeks.containsKey(playerId)) { + final _SeekOperation? existing = _pendingSeeks[playerId]; + if (existing != null) { + final _SeekOperation op = _SeekOperation(targetPosition); + op._coalesced.add(existing.completer); + op._coalesced.addAll(existing._coalesced); + _pendingSeeks[playerId] = op; + return op.attach(); + } final _SeekOperation op = _SeekOperation(targetPosition); - _pendingSeeks.putIfAbsent(playerId, () => <_SeekOperation>[]); - _pendingSeeks[playerId]!.add(op); - return op.completer.future; + _pendingSeeks[playerId] = op; + return op.attach(); } return _startSeek(playerId, targetPosition); @@ -148,67 +183,57 @@ class VideoPlayerTizen extends VideoPlayerPlatform { _activeSeeks[playerId] = op; try { - await _api.seekTo( - PositionMessage(playerId: playerId, position: position), - ); + await _api + .seekTo(PositionMessage(playerId: playerId, position: position)); } catch (e) { _completeSeekWithError(playerId, e); } + + return op.completer.future; } void _handleSeekCompleted(int playerId) { final _SeekOperation? op = _activeSeeks.remove(playerId); - if (op != null && !op.completer.isCompleted) { - op.completer.complete(); + if (op != null) { + op._complete(); } _startPendingSeekIfAny(playerId); } - void _startPendingSeekIfAny(int playerId) { - 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); + Future _startPendingSeekIfAny(int playerId) async { + final _SeekOperation? pending = _pendingSeeks.remove(playerId); + if (pending != null) { + _activeSeeks[playerId] = pending; + + try { + await _api.seekTo( + PositionMessage(playerId: playerId, position: pending.position), + ); + } catch (e) { + _completeSeekWithError(playerId, e); + } } } void _completeSeekWithError(int playerId, Object error) { final _SeekOperation? op = _activeSeeks.remove(playerId); - if (op != null && !op.completer.isCompleted) { - op.completer.completeError(error); + if (op != null) { + op._completeError(error); } - final List<_SeekOperation>? pending = _pendingSeeks.remove(playerId); + final _SeekOperation? pending = _pendingSeeks.remove(playerId); if (pending != null) { - for (final _SeekOperation p in pending) { - if (!p.completer.isCompleted) { - p.completer.completeError(error); - } - } + pending._completeError(error); } } void _cancelAllSeeks(int playerId) { final _SeekOperation? op = _activeSeeks.remove(playerId); - if (op != null && !op.completer.isCompleted) { - op.completer.completeError('Player was disposed.'); + if (op != null) { + op._completeError('Player was disposed.'); } - final List<_SeekOperation>? pending = _pendingSeeks.remove(playerId); + final _SeekOperation? pending = _pendingSeeks.remove(playerId); if (pending != null) { - for (final _SeekOperation p in pending) { - if (!p.completer.isCompleted) { - p.completer.completeError('Player was disposed.'); - } - } + pending._completeError('Player was disposed.'); } } From 7399d1fb683d25a09a3534374e797018c665ee20 Mon Sep 17 00:00:00 2001 From: "yying.jin" Date: Thu, 17 Sep 2026 19:20:43 +0800 Subject: [PATCH 3/6] cancel all seeks when suspend called --- packages/video_player_avplay/lib/src/video_player_tizen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/video_player_avplay/lib/src/video_player_tizen.dart b/packages/video_player_avplay/lib/src/video_player_tizen.dart index 8fd1741e8..c5dc0ee5d 100644 --- a/packages/video_player_avplay/lib/src/video_player_tizen.dart +++ b/packages/video_player_avplay/lib/src/video_player_tizen.dart @@ -387,6 +387,7 @@ class VideoPlayerTizen extends VideoPlayerPlatform { @override Future suspend(int playerId) { + _cancelAllSeeks(playerId); return _api.suspend(playerId); } From 37fe85ff789ab1f7054ed945c06a07a8fd277c90 Mon Sep 17 00:00:00 2001 From: "yying.jin" Date: Thu, 17 Sep 2026 19:37:10 +0800 Subject: [PATCH 4/6] dart format --- .../lib/src/video_player_tizen.dart | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/video_player_avplay/lib/src/video_player_tizen.dart b/packages/video_player_avplay/lib/src/video_player_tizen.dart index ebc0ecd5d..b49d88321 100644 --- a/packages/video_player_avplay/lib/src/video_player_tizen.dart +++ b/packages/video_player_avplay/lib/src/video_player_tizen.dart @@ -21,7 +21,7 @@ class _SeekOperation { final List> _coalesced = >[]; Future attach() { - final Completer c = Completer(); + final c = Completer(); _coalesced.add(c); return c.future; } @@ -164,13 +164,13 @@ class VideoPlayerTizen extends VideoPlayerPlatform { if (_activeSeeks.containsKey(playerId)) { final _SeekOperation? existing = _pendingSeeks[playerId]; if (existing != null) { - final _SeekOperation op = _SeekOperation(targetPosition); + final op = _SeekOperation(targetPosition); op._coalesced.add(existing.completer); op._coalesced.addAll(existing._coalesced); _pendingSeeks[playerId] = op; return op.attach(); } - final _SeekOperation op = _SeekOperation(targetPosition); + final op = _SeekOperation(targetPosition); _pendingSeeks[playerId] = op; return op.attach(); } @@ -179,12 +179,11 @@ class VideoPlayerTizen extends VideoPlayerPlatform { } Future _startSeek(int playerId, int position) async { - final _SeekOperation op = _SeekOperation(position); + final op = _SeekOperation(position); _activeSeeks[playerId] = op; try { - await _api - .seekTo(PositionMessage(playerId: playerId, position: position)); + await _api.seekTo(PositionMessage(playerId: playerId, position: position)); } catch (e) { _completeSeekWithError(playerId, e); } @@ -571,10 +570,8 @@ class VideoPlayerTizen extends VideoPlayerPlatform { case 'subtitleUpdate': return VideoEvent( eventType: VideoEventType.subtitleUpdate, - subtitlesInfo: SubtitlesInfo( - map['duration'] as int?, - map['textsInfo'] as List?, - map['pictureInfo'] as Map?), + subtitlesInfo: SubtitlesInfo(map['duration'] as int?, + map['textsInfo'] as List?, map['pictureInfo'] as Map?), ); case 'isPlayingStateUpdate': return VideoEvent( @@ -635,8 +632,7 @@ class VideoPlayerTizen extends VideoPlayerPlatform { return EventChannel('tizen/video_player/video_events_$playerId'); } - static const Map _videoFormatStringMap = - { + static const Map _videoFormatStringMap = { VideoFormat.ss: 'ss', VideoFormat.hls: 'hls', VideoFormat.dash: 'dash', @@ -668,17 +664,14 @@ class VideoPlayerTizen extends VideoPlayerPlatform { StreamingPropertyType.setAbr: 'SET_ABR', }; - static const Map _bufferConfigTypeMap = - { + static const Map _bufferConfigTypeMap = { BufferConfigType.totalBufferSizeInByte: 'total_buffer_size_in_byte', BufferConfigType.totalBufferSizeInTime: 'total_buffer_size_in_time', BufferConfigType.bufferSizeInByteForPlay: 'buffer_size_in_byte_for_play', BufferConfigType.bufferSizeInSecForPlay: 'buffer_size_in_sec_for_play', - BufferConfigType.bufferSizeInByteForResume: - 'buffer_size_in_byte_for_resume', + BufferConfigType.bufferSizeInByteForResume: 'buffer_size_in_byte_for_resume', BufferConfigType.bufferSizeInSecForResume: 'buffer_size_in_sec_for_resume', - BufferConfigType.bufferingTimeoutInSecForPlay: - 'buffering_timeout_in_sec_for_play', + BufferConfigType.bufferingTimeoutInSecForPlay: 'buffering_timeout_in_sec_for_play', }; static const Map _dashPlayerPropertyMap = From c0cac066cfedc6817a927bbdd76f9a9ed4b2f0bd Mon Sep 17 00:00:00 2001 From: "yying.jin" Date: Tue, 22 Sep 2026 13:49:42 +0800 Subject: [PATCH 5/6] Fix seek completion delivery for consecutive seeks --- .../lib/src/video_player_tizen.dart | 82 ++++++++----------- .../tizen/src/media_player.cc | 2 + .../tizen/src/plus_player.cc | 12 ++- .../tizen/src/video_player_tizen_plugin.cc | 5 +- 4 files changed, 49 insertions(+), 52 deletions(-) diff --git a/packages/video_player_avplay/lib/src/video_player_tizen.dart b/packages/video_player_avplay/lib/src/video_player_tizen.dart index b49d88321..3d3ce8569 100644 --- a/packages/video_player_avplay/lib/src/video_player_tizen.dart +++ b/packages/video_player_avplay/lib/src/video_player_tizen.dart @@ -17,35 +17,30 @@ class _SeekOperation { _SeekOperation(this.position); final int position; - final Completer completer = Completer(); - final List> _coalesced = >[]; + final List> _waiters = >[]; Future attach() { - final c = Completer(); - _coalesced.add(c); - return c.future; + final completer = Completer(); + _waiters.add(completer); + return completer.future; } - void _complete() { - if (!completer.isCompleted) { - completer.complete(); - } - for (final Completer c in _coalesced) { - if (!c.isCompleted) { - c.complete(); + void completeAll() { + for (final Completer completer in _waiters) { + if (!completer.isCompleted) { + completer.complete(); } } + _waiters.clear(); } - void _completeError(Object error) { - if (!completer.isCompleted) { - completer.completeError(error); - } - for (final Completer c in _coalesced) { - if (!c.isCompleted) { - c.completeError(error); + void completeErrorAll(Object error) { + for (final Completer completer in _waiters) { + if (!completer.isCompleted) { + completer.completeError(error); } } + _waiters.clear(); } } @@ -165,8 +160,7 @@ class VideoPlayerTizen extends VideoPlayerPlatform { final _SeekOperation? existing = _pendingSeeks[playerId]; if (existing != null) { final op = _SeekOperation(targetPosition); - op._coalesced.add(existing.completer); - op._coalesced.addAll(existing._coalesced); + op._waiters.addAll(existing._waiters); _pendingSeeks[playerId] = op; return op.attach(); } @@ -178,62 +172,53 @@ class VideoPlayerTizen extends VideoPlayerPlatform { return _startSeek(playerId, targetPosition); } - Future _startSeek(int playerId, int position) async { + Future _startSeek(int playerId, int position) { final op = _SeekOperation(position); + final Future future = op.attach(); _activeSeeks[playerId] = op; + unawaited(_runNativeSeek(playerId, op)); + return future; + } + Future _runNativeSeek(int playerId, _SeekOperation op) async { try { - await _api.seekTo(PositionMessage(playerId: playerId, position: position)); - } catch (e) { - _completeSeekWithError(playerId, e); + await _api.seekTo(PositionMessage(playerId: playerId, position: op.position)); + } catch (error) { + if (identical(_activeSeeks[playerId], op)) { + _completeSeekWithError(playerId, error); + } } - - return op.completer.future; } void _handleSeekCompleted(int playerId) { final _SeekOperation? op = _activeSeeks.remove(playerId); if (op != null) { - op._complete(); + op.completeAll(); } _startPendingSeekIfAny(playerId); } - Future _startPendingSeekIfAny(int playerId) async { + void _startPendingSeekIfAny(int playerId) { final _SeekOperation? pending = _pendingSeeks.remove(playerId); if (pending != null) { _activeSeeks[playerId] = pending; - - try { - await _api.seekTo( - PositionMessage(playerId: playerId, position: pending.position), - ); - } catch (e) { - _completeSeekWithError(playerId, e); - } + unawaited(_runNativeSeek(playerId, pending)); } } void _completeSeekWithError(int playerId, Object error) { final _SeekOperation? op = _activeSeeks.remove(playerId); if (op != null) { - op._completeError(error); + op.completeErrorAll(error); } final _SeekOperation? pending = _pendingSeeks.remove(playerId); if (pending != null) { - pending._completeError(error); + pending.completeErrorAll(error); } } void _cancelAllSeeks(int playerId) { - final _SeekOperation? op = _activeSeeks.remove(playerId); - if (op != null) { - op._completeError('Player was disposed.'); - } - final _SeekOperation? pending = _pendingSeeks.remove(playerId); - if (pending != null) { - pending._completeError('Player was disposed.'); - } + _completeSeekWithError(playerId, 'Player was disposed.'); } @override @@ -594,6 +579,9 @@ class VideoPlayerTizen extends VideoPlayerPlatform { default: return VideoEvent(eventType: VideoEventType.unknown); } + }).handleError((Object error, StackTrace stackTrace) { + _completeSeekWithError(playerId, error); + Error.throwWithStackTrace(error, stackTrace); }); } diff --git a/packages/video_player_avplay/tizen/src/media_player.cc b/packages/video_player_avplay/tizen/src/media_player.cc index dac28e084..f55b3732d 100644 --- a/packages/video_player_avplay/tizen/src/media_player.cc +++ b/packages/video_player_avplay/tizen/src/media_player.cc @@ -740,6 +740,8 @@ void MediaPlayer::OnError(int error_code, void *user_data) { get_error_message(error_code)); MediaPlayer *self = static_cast(user_data); + self->on_seek_completed_ = nullptr; + self->is_seeking_ = false; self->SendError("Media Player error", std::string("Error: ") + get_error_message(error_code)); } diff --git a/packages/video_player_avplay/tizen/src/plus_player.cc b/packages/video_player_avplay/tizen/src/plus_player.cc index 72e4cd1ce..b1926ae00 100644 --- a/packages/video_player_avplay/tizen/src/plus_player.cc +++ b/packages/video_player_avplay/tizen/src/plus_player.cc @@ -837,9 +837,11 @@ bool PlusPlayer::StopAndClose() { } if (player_state != plusplayer::State::kNone) { - if (!::Stop(player_)) { - LOG_ERROR("[PlusPlayer] Player fail to stop."); - success = false; + if (player_state >= plusplayer::State::kReady) { + if (!::Stop(player_)) { + LOG_ERROR("[PlusPlayer] Player fail to stop."); + success = false; + } } if (!::Close(player_)) { @@ -1472,6 +1474,8 @@ void PlusPlayer::OnError(const plusplayer::ErrorType &error_code, LOG_ERROR("[PlusPlayer] Error code: %d", error_code); PlusPlayer *self = reinterpret_cast(user_data); + self->on_seek_completed_ = nullptr; + self->is_seeking_ = false; self->SendError("[PlusPlayer] error", std::string("Error: ") + GetErrorMessage(error_code)); } @@ -1481,6 +1485,8 @@ void PlusPlayer::OnErrorMsg(const plusplayer::ErrorType &error_code, LOG_ERROR("[PlusPlayer] Error code: %d, message: %s.", error_code, error_msg); PlusPlayer *self = reinterpret_cast(user_data); + self->on_seek_completed_ = nullptr; + self->is_seeking_ = false; self->SendError("PlusPlayer error", std::string("Error: ") + error_msg); } diff --git a/packages/video_player_avplay/tizen/src/video_player_tizen_plugin.cc b/packages/video_player_avplay/tizen/src/video_player_tizen_plugin.cc index 15a228740..ddec7705b 100644 --- a/packages/video_player_avplay/tizen/src/video_player_tizen_plugin.cc +++ b/packages/video_player_avplay/tizen/src/video_player_tizen_plugin.cc @@ -302,10 +302,11 @@ void VideoPlayerTizenPlugin::SeekTo( result(FlutterError("Invalid argument", "Player not found")); return; } - if (!player->SeekTo(msg.position(), - [result]() -> void { result(std::nullopt); })) { + if (!player->SeekTo(msg.position(), nullptr)) { result(FlutterError("SeekTo", "Player seek to failed")); + return; } + result(std::nullopt); } std::optional VideoPlayerTizenPlugin::SetDisplayGeometry( From 71be79899d0953977ecfd0f43470969155c181a2 Mon Sep 17 00:00:00 2001 From: "yying.jin" Date: Tue, 22 Sep 2026 14:43:29 +0800 Subject: [PATCH 6/6] Address remaining review comments on seek handling - Document play/pause idempotency in CHANGELOG - Cancel pending seeks in restore() as in dispose()/suspend() - Report seek cancellation as StateError with per-lifecycle wording - Drop the no-op SendSeekCompleted from PlusPlayer::StopAndClose Co-Authored-By: Cline SR --- packages/video_player_avplay/CHANGELOG.md | 1 + .../video_player_avplay/lib/src/video_player_tizen.dart | 9 +++++---- packages/video_player_avplay/tizen/src/plus_player.cc | 3 --- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/video_player_avplay/CHANGELOG.md b/packages/video_player_avplay/CHANGELOG.md index ccb9fc843..d3b820b62 100644 --- a/packages/video_player_avplay/CHANGELOG.md +++ b/packages/video_player_avplay/CHANGELOG.md @@ -5,6 +5,7 @@ * Replace Ecore pipes with GLib idle callbacks. * Apply clang-format-16 formatting. * Handle consecutive seekTo calls. +* Ignore redundant play/pause calls instead of reporting an error. ## 0.8.18 diff --git a/packages/video_player_avplay/lib/src/video_player_tizen.dart b/packages/video_player_avplay/lib/src/video_player_tizen.dart index 3d3ce8569..44891d05d 100644 --- a/packages/video_player_avplay/lib/src/video_player_tizen.dart +++ b/packages/video_player_avplay/lib/src/video_player_tizen.dart @@ -60,7 +60,7 @@ class VideoPlayerTizen extends VideoPlayerPlatform { @override Future dispose(int playerId) { - _cancelAllSeeks(playerId); + _cancelAllSeeks(playerId, 'Player was disposed.'); return _api.dispose(PlayerMessage(playerId: playerId)); } @@ -217,8 +217,8 @@ class VideoPlayerTizen extends VideoPlayerPlatform { } } - void _cancelAllSeeks(int playerId) { - _completeSeekWithError(playerId, 'Player was disposed.'); + void _cancelAllSeeks(int playerId, String reason) { + _completeSeekWithError(playerId, StateError(reason)); } @override @@ -371,7 +371,7 @@ class VideoPlayerTizen extends VideoPlayerPlatform { @override Future suspend(int playerId) { - _cancelAllSeeks(playerId); + _cancelAllSeeks(playerId, 'Player was suspended.'); return _api.suspend(playerId); } @@ -381,6 +381,7 @@ class VideoPlayerTizen extends VideoPlayerPlatform { DataSource? dataSource, int resumeTime = -1, }) async { + _cancelAllSeeks(playerId, 'Player was restored.'); final message = CreateMessage(); if (dataSource != null) { diff --git a/packages/video_player_avplay/tizen/src/plus_player.cc b/packages/video_player_avplay/tizen/src/plus_player.cc index b1926ae00..efbfe7db6 100644 --- a/packages/video_player_avplay/tizen/src/plus_player.cc +++ b/packages/video_player_avplay/tizen/src/plus_player.cc @@ -825,9 +825,6 @@ bool PlusPlayer::StopAndClose() { bool success = true; is_buffering_ = false; - if (is_seeking_) { - SendSeekCompleted(); - } on_seek_completed_ = nullptr; is_seeking_ = false; plusplayer::State player_state = GetState(player_);