Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/video_player_avplay/CHANGELOG.md
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.
Expand Down
101 changes: 97 additions & 4 deletions packages/video_player_avplay/lib/src/video_player_tizen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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));
}

Expand Down Expand Up @@ -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;
Comment on lines +137 to +140

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Coalesce pending seeks instead of replaying every position

When several seekTo calls 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 👍 / 👎.

}

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate native seek failures to the returned future

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 seekTo, and queued seeks enter the .then branch and also complete successfully; meanwhile the internal completer's error is unobserved. Rethrow the exception or ensure _startSeek returns the operation's completer future.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Complete the future actually returned for an active seek

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 _startSeek's Pigeon future instead. Disposal destroys the native callback, so the caller's future can remain pending indefinitely while this orphaned completeError is emitted as an unhandled asynchronous error.

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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down
43 changes: 28 additions & 15 deletions packages/video_player_avplay/tizen/src/media_player.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -705,10 +712,12 @@ void MediaPlayer::OnSeekCompleted(void *user_data) {
LOG_INFO("[MediaPlayer] Seek completed.");

MediaPlayer *self = static_cast<MediaPlayer *>(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) {
Expand Down Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions packages/video_player_avplay/tizen/src/media_player.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class MediaPlayer : public VideoPlayer {
std::unique_ptr<DrmManager> 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_;
Expand Down
40 changes: 25 additions & 15 deletions packages/video_player_avplay/tizen/src/plus_player.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -231,7 +233,7 @@ bool PlusPlayer::Play() {
}
return true;
}
return false;
return true;
}

bool PlusPlayer::Activate() {
Expand Down Expand Up @@ -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_)) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -818,33 +822,37 @@ 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_) {
drm_manager_->ReleaseDrmSession();
drm_manager_.reset();
}

return true;
return success;
}

bool PlusPlayer::Suspend() {
Expand Down Expand Up @@ -1173,10 +1181,12 @@ void PlusPlayer::OnSeekDone(void *user_data) {
LOG_INFO("[PlusPlayer] Seek completed.");
PlusPlayer *self = reinterpret_cast<PlusPlayer *>(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) {
Expand Down
1 change: 1 addition & 0 deletions packages/video_player_avplay/tizen/src/plus_player.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<plusplayer::PlayerMemento> memento_ = nullptr;
std::string url_;
std::unique_ptr<DeviceProxy> device_proxy_ = nullptr;
Expand Down
8 changes: 8 additions & 0 deletions packages/video_player_avplay/tizen/src/video_player.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading