diff --git a/packages/video_player_avplay/CHANGELOG.md b/packages/video_player_avplay/CHANGELOG.md index 1e3b9a830..75c88aa80 100644 --- a/packages/video_player_avplay/CHANGELOG.md +++ b/packages/video_player_avplay/CHANGELOG.md @@ -1,10 +1,12 @@ -## NEXT +## 0.8.19 * Omit obvious local variable types. * Reformat with a line length of 100. * Replace Ecore pipes with GLib idle callbacks. * Apply clang-format-16 formatting. * Use wrapper api. +* Handle consecutive seekTo calls. +* Ignore redundant play/pause calls instead of reporting an error. ## 0.8.18 diff --git a/packages/video_player_avplay/README.md b/packages/video_player_avplay/README.md index 3773429df..a363e9b0c 100644 --- a/packages/video_player_avplay/README.md +++ b/packages/video_player_avplay/README.md @@ -12,7 +12,7 @@ To use this package, add `video_player_avplay` as a dependency in your `pubspec. ```yaml dependencies: - video_player_avplay: ^0.8.18 + video_player_avplay: ^0.8.19 ``` Then you can import `video_player_avplay` in your Dart code: 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 90b7afd89..44891d05d 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,46 @@ import '../video_player_platform_interface.dart'; import 'messages.g.dart'; import 'tracks.dart'; +class _SeekOperation { + _SeekOperation(this.position); + + final int position; + final List> _waiters = >[]; + + Future attach() { + final completer = Completer(); + _waiters.add(completer); + return completer.future; + } + + void completeAll() { + for (final Completer completer in _waiters) { + if (!completer.isCompleted) { + completer.complete(); + } + } + _waiters.clear(); + } + + void completeErrorAll(Object error) { + for (final Completer completer in _waiters) { + if (!completer.isCompleted) { + completer.completeError(error); + } + } + _waiters.clear(); + } +} + /// 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 +60,7 @@ class VideoPlayerTizen extends VideoPlayerPlatform { @override Future dispose(int playerId) { + _cancelAllSeeks(playerId, 'Player was disposed.'); return _api.dispose(PlayerMessage(playerId: playerId)); } @@ -116,9 +154,71 @@ 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? existing = _pendingSeeks[playerId]; + if (existing != null) { + final op = _SeekOperation(targetPosition); + op._waiters.addAll(existing._waiters); + _pendingSeeks[playerId] = op; + return op.attach(); + } + final op = _SeekOperation(targetPosition); + _pendingSeeks[playerId] = op; + return op.attach(); + } + + return _startSeek(playerId, targetPosition); + } + + 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: op.position)); + } catch (error) { + if (identical(_activeSeeks[playerId], op)) { + _completeSeekWithError(playerId, error); + } + } + } + + void _handleSeekCompleted(int playerId) { + final _SeekOperation? op = _activeSeeks.remove(playerId); + if (op != null) { + op.completeAll(); + } + _startPendingSeekIfAny(playerId); + } + + void _startPendingSeekIfAny(int playerId) { + final _SeekOperation? pending = _pendingSeeks.remove(playerId); + if (pending != null) { + _activeSeeks[playerId] = pending; + unawaited(_runNativeSeek(playerId, pending)); + } + } + + void _completeSeekWithError(int playerId, Object error) { + final _SeekOperation? op = _activeSeeks.remove(playerId); + if (op != null) { + op.completeErrorAll(error); + } + final _SeekOperation? pending = _pendingSeeks.remove(playerId); + if (pending != null) { + pending.completeErrorAll(error); + } + } + + void _cancelAllSeeks(int playerId, String reason) { + _completeSeekWithError(playerId, StateError(reason)); } @override @@ -271,6 +371,7 @@ class VideoPlayerTizen extends VideoPlayerPlatform { @override Future suspend(int playerId) { + _cancelAllSeeks(playerId, 'Player was suspended.'); return _api.suspend(playerId); } @@ -280,6 +381,7 @@ class VideoPlayerTizen extends VideoPlayerPlatform { DataSource? dataSource, int resumeTime = -1, }) async { + _cancelAllSeeks(playerId, 'Player was restored.'); final message = CreateMessage(); if (dataSource != null) { @@ -443,7 +545,6 @@ class VideoPlayerTizen extends VideoPlayerPlatform { return VideoEvent(eventType: VideoEventType.completed); case 'bufferingUpdate': final value = map['value']! as int; - return VideoEvent( buffered: value, eventType: VideoEventType.bufferingUpdate, @@ -473,9 +574,15 @@ 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); } + }).handleError((Object error, StackTrace stackTrace) { + _completeSeekWithError(playerId, error); + Error.throwWithStackTrace(error, stackTrace); }); } diff --git a/packages/video_player_avplay/pubspec.yaml b/packages/video_player_avplay/pubspec.yaml index 4e5554ddc..727779ae0 100644 --- a/packages/video_player_avplay/pubspec.yaml +++ b/packages/video_player_avplay/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avplay description: Flutter plugin for displaying inline video on Tizen TV devices. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/main/packages/video_player_avplay -version: 0.8.18 +version: 0.8.19 environment: sdk: ">=3.1.0 <4.0.0" diff --git a/packages/video_player_avplay/tizen/src/media_player.cc b/packages/video_player_avplay/tizen/src/media_player.cc index f7690a1bf..9bf9d4d6f 100644 --- a/packages/video_player_avplay/tizen/src/media_player.cc +++ b/packages/video_player_avplay/tizen/src/media_player.cc @@ -226,7 +226,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) { @@ -251,7 +251,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) { @@ -301,11 +301,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; @@ -700,10 +707,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) { @@ -726,6 +735,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)); } @@ -804,36 +815,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 85fcd2cd7..97de570e0 100644 --- a/packages/video_player_avplay/tizen/src/media_player.h +++ b/packages/video_player_avplay/tizen/src/media_player.h @@ -76,6 +76,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 8f5e0f716..affd40077 100644 --- a/packages/video_player_avplay/tizen/src/plus_player.cc +++ b/packages/video_player_avplay/tizen/src/plus_player.cc @@ -195,6 +195,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(); } @@ -232,7 +234,7 @@ bool PlusPlayer::Play() { } return true; } - return false; + return true; } bool PlusPlayer::Activate() { @@ -286,7 +288,7 @@ bool PlusPlayer::Pause() { if (state != plusplayer::State::kPlaying) { LOG_INFO("[PlusPlayer] Player not playing."); - return false; + return true; } if (!::Pause(player_)) { @@ -340,14 +342,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; } @@ -819,25 +823,28 @@ bool PlusPlayer::StopAndClose() { return false; } + bool success = true; is_buffering_ = false; + on_seek_completed_ = nullptr; + is_seeking_ = 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 (drm_manager_) { drm_manager_->StopDrmSession(); } - if (!::Stop(player_)) { - LOG_ERROR("[PlusPlayer] Player fail to stop."); - return false; - } + if (player_state != plusplayer::State::kNone) { + if (player_state >= plusplayer::State::kReady) { + 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_) { @@ -845,7 +852,7 @@ bool PlusPlayer::StopAndClose() { drm_manager_.reset(); } - return true; + return success; } bool PlusPlayer::Suspend() { @@ -1172,10 +1179,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) { @@ -1460,6 +1469,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)); } @@ -1469,6 +1480,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/plus_player.h b/packages/video_player_avplay/tizen/src/plus_player.h index bc110bbc6..8cdd185a9 100644 --- a/packages/video_player_avplay/tizen/src/plus_player.h +++ b/packages/video_player_avplay/tizen/src/plus_player.h @@ -108,6 +108,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_; CreateMessage create_message_; diff --git a/packages/video_player_avplay/tizen/src/video_player.cc b/packages/video_player_avplay/tizen/src/video_player.cc index 6e1c71c6d..d80f59ff7 100644 --- a/packages/video_player_avplay/tizen/src/video_player.cc +++ b/packages/video_player_avplay/tizen/src/video_player.cc @@ -155,6 +155,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 460209e5f..f43d59d7f 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{}); 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(