diff --git a/packages/audioplayers/CHANGELOG.md b/packages/audioplayers/CHANGELOG.md index 737fe7bf6..25ee301d6 100644 --- a/packages/audioplayers/CHANGELOG.md +++ b/packages/audioplayers/CHANGELOG.md @@ -1,3 +1,10 @@ +## 3.1.9 + +* Serialize seeks and defer playback controls until seeking completes. +* Avoid missing network seek completion callbacks after stop by rewinding while paused. +* Reset the playback position before reporting completion in stop mode. +* Report deferred seek failures as errors. + ## 3.1.8 * Update audioplayers to 6.8.1. diff --git a/packages/audioplayers/README.md b/packages/audioplayers/README.md index 52c8cb57f..c51cfc974 100644 --- a/packages/audioplayers/README.md +++ b/packages/audioplayers/README.md @@ -11,7 +11,7 @@ This package is not an _endorsed_ implementation of `audioplayers`. Therefore, y ```yaml dependencies: audioplayers: ^6.8.1 - audioplayers_tizen: ^3.1.8 + audioplayers_tizen: ^3.1.9 ``` @@ -70,4 +70,4 @@ For detailed information on Tizen privileges, see [Tizen Docs: API Privileges](h - `onPlayerComplete` event will not be fired when `ReleaseMode` is set to loop which differs from the behavior specified in the [documentation](https://pub.dev/documentation/audioplayers/latest/audioplayers/AudioPlayer/onPlayerComplete.html). And playback rate will reset to 1.0 when audio is replayed. - `setVolume` will have no effect on TV devices. -- `setPlaybackRate` is limited to values between 0.5 and 2.0 on TV and is not supported on RPI. +- `setPlaybackRate` supports local and streaming sources between 0.5 and 2.0 on TV. On RPI, it supports local files between 0.5 and 5.0, but not streaming sources. diff --git a/packages/audioplayers/pubspec.yaml b/packages/audioplayers/pubspec.yaml index b228f55d9..e2266b556 100644 --- a/packages/audioplayers/pubspec.yaml +++ b/packages/audioplayers/pubspec.yaml @@ -2,7 +2,7 @@ name: audioplayers_tizen description: Tizen implementation of the audioplayers plugin. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/main/packages/audioplayers -version: 3.1.8 +version: 3.1.9 environment: sdk: ^3.6.0 diff --git a/packages/audioplayers/tizen/src/audio_player.cc b/packages/audioplayers/tizen/src/audio_player.cc index 3c4897b0c..cfc7ff5c1 100644 --- a/packages/audioplayers/tizen/src/audio_player.cc +++ b/packages/audioplayers/tizen/src/audio_player.cc @@ -15,6 +15,7 @@ namespace { struct IdleData { AudioPlayer *player; std::shared_ptr is_alive; + unsigned int generation; }; } // namespace @@ -24,13 +25,14 @@ AudioPlayer::AudioPlayer(const std::string &player_id, DurationListener duration_listener, SeekCompletedListener seek_completed_listener, PlayCompletedListener play_completed_listener, - LogListener log_listener) + LogListener log_listener, ErrorListener error_listener) : player_id_(player_id), prepared_listener_(prepared_listener), duration_listener_(duration_listener), seek_completed_listener_(seek_completed_listener), play_completed_listener_(play_completed_listener), - log_listener_(log_listener) { + log_listener_(log_listener), + error_listener_(error_listener) { CreatePlayer(); } @@ -50,33 +52,24 @@ AudioPlayer::~AudioPlayer() { } void AudioPlayer::Play() { + NotifyPendingCompletion(); + if (seeking_) { + pending_action_ = PendingAction::kPlay; + return; + } player_state_e state = GetPlayerState(); if (state == PLAYER_STATE_IDLE && preparing_) { // Player is preparing, play will be called in prepared callback. - should_play_ = true; + pending_action_ = PendingAction::kPlay; return; } switch (state) { case PLAYER_STATE_NONE: case PLAYER_STATE_IDLE: { - if (audio_data_.size() > 0) { - int ret = player_set_memory_buffer(player_, audio_data_.data(), - audio_data_.size()); - if (ret != PLAYER_ERROR_NONE) { - throw AudioPlayerError("player_set_memory_buffer failed", - get_error_message(ret)); - } - should_play_ = true; - PreparePlayer(); - } else if (url_.size() > 0) { - int ret = player_set_uri(player_, url_.c_str()); - if (ret != PLAYER_ERROR_NONE) { - throw AudioPlayerError("player_set_uri failed", - get_error_message(ret)); - } - should_play_ = true; - PreparePlayer(); + if (audio_data_.size() > 0 || url_.size() > 0) { + pending_action_ = PendingAction::kPlay; + PrepareSource(); } break; } @@ -86,7 +79,7 @@ void AudioPlayer::Play() { if (ret != PLAYER_ERROR_NONE) { throw AudioPlayerError("player_start failed", get_error_message(ret)); } - should_play_ = false; + pending_action_ = PendingAction::kNone; StartPositionUpdates(); break; } @@ -97,6 +90,10 @@ void AudioPlayer::Play() { } void AudioPlayer::Pause() { + if (seeking_) { + pending_action_ = PendingAction::kPause; + return; + } if (GetPlayerState() == PLAYER_STATE_PLAYING) { int ret = player_pause(player_); if (ret != PLAYER_ERROR_NONE) { @@ -104,34 +101,32 @@ void AudioPlayer::Pause() { } } - should_play_ = false; + pending_action_ = PendingAction::kNone; } void AudioPlayer::Stop() { + NotifyPendingCompletion(); + pending_action_ = PendingAction::kNone; + if (release_mode_ == ReleaseMode::kRelease) { + ReleaseMediaSource(); + return; + } + if (seeking_) { + pending_action_ = PendingAction::kPause; + should_seek_to_ = 0; + return; + } player_state_e state = GetPlayerState(); if (state == PLAYER_STATE_PLAYING || state == PLAYER_STATE_PAUSED) { - int ret = player_stop(player_); - if (ret != PLAYER_ERROR_NONE) { - throw AudioPlayerError("player_stop failed", get_error_message(ret)); - } - // Reset the play position to 0 to match other platforms, per the - // AudioPlayer.stop() contract: - // https://pub.dev/documentation/audioplayers/latest/audioplayers/AudioPlayer/stop.html - // This is best-effort: on some devices (e.g. TV with network sources) - // player_set_play_position right after stop can fail with an invalid - // state, which must not crash the app. + Pause(); try { Seek(0); } catch (const AudioPlayerError &error) { OnLog("Failed to reset position on stop: " + error.message()); } - } - - should_play_ = false; - seeking_ = false; - - if (release_mode_ == ReleaseMode::kRelease) { - ReleaseMediaSource(); + } else if (state == PLAYER_STATE_READY) { + ResetPlayer(); + PrepareSource(); } } @@ -143,6 +138,7 @@ void AudioPlayer::ReleaseMediaSource() { void AudioPlayer::Seek(int32_t position) { if (seeking_) { + should_seek_to_ = position; return; } @@ -217,7 +213,7 @@ void AudioPlayer::SetVolume(double volume) { void AudioPlayer::SetPlaybackRate(double playback_rate) { // TODO(seungsoo47): The player_set_playback_rate() API has a limitation of - // 0.5-2x on TV and is not supported on RPI. + // 0.5-2x on TV. On RPI, it supports 0.5-5x for local files only. playback_rate_ = playback_rate; player_state_e state = GetPlayerState(); if (state == PLAYER_STATE_READY || state == PLAYER_STATE_PLAYING || @@ -284,7 +280,7 @@ bool AudioPlayer::IsPlaying() { } void AudioPlayer::CreatePlayer() { - should_play_ = false; + pending_action_ = PendingAction::kNone; preparing_ = false; int ret = player_create(&player_); @@ -332,6 +328,26 @@ void AudioPlayer::PreparePlayer() { seeking_ = false; } +void AudioPlayer::PrepareSource() { + int ret; + if (audio_data_.size() > 0) { + ret = player_set_memory_buffer(player_, audio_data_.data(), + audio_data_.size()); + if (ret != PLAYER_ERROR_NONE) { + throw AudioPlayerError("player_set_memory_buffer failed", + get_error_message(ret)); + } + } else if (url_.size() > 0) { + ret = player_set_uri(player_, url_.c_str()); + if (ret != PLAYER_ERROR_NONE) { + throw AudioPlayerError("player_set_uri failed", get_error_message(ret)); + } + } else { + return; + } + PreparePlayer(); +} + void AudioPlayer::ResetPlayer() { player_state_e state = GetPlayerState(); switch (state) { @@ -359,6 +375,23 @@ void AudioPlayer::ResetPlayer() { } break; } + bool seek_cancelled = seeking_ || should_seek_to_ >= 0; + ++generation_; + preparing_ = false; + NotifyPendingCompletion(); + seeking_ = false; + should_seek_to_ = -1; + pending_action_ = PendingAction::kNone; + if (seek_cancelled) { + seek_completed_listener_(player_id_); + } +} + +void AudioPlayer::NotifyPendingCompletion() { + if (completing_) { + completing_ = false; + play_completed_listener_(player_id_); + } } player_state_e AudioPlayer::GetPlayerState() { @@ -381,11 +414,12 @@ void AudioPlayer::OnPrepared(void *data) { G_PRIORITY_DEFAULT_IDLE, [](gpointer data) -> gboolean { auto *idle = static_cast(data); - if (!*idle->is_alive) { + if (!*idle->is_alive || idle->generation != idle->player->generation_) { return G_SOURCE_REMOVE; } auto *player = idle->player; player->preparing_ = false; + player->NotifyPendingCompletion(); try { player->duration_listener_(player->player_id_, player->GetDuration()); @@ -396,32 +430,34 @@ void AudioPlayer::OnPrepared(void *data) { } player_set_playback_rate(player->player_, player->playback_rate_); - if (player->should_play_) { - int ret = player_start(player->player_); + if (player->should_seek_to_ >= 0) { + int position = player->should_seek_to_; + player->should_seek_to_ = -1; + player->seeking_ = true; + int ret = player_set_play_position(player->player_, position, true, + OnSeekCompleted, player); if (ret != PLAYER_ERROR_NONE) { - player->log_listener_(player->player_id_, "player_start failed."); + player->seeking_ = false; + player->error_listener_(player->player_id_, + "player_set_play_position failed", + get_error_message(ret)); + } else { return G_SOURCE_REMOVE; } - player->StartPositionUpdates(); - player->should_play_ = false; } - if (player->should_seek_to_ > 0) { - player->seeking_ = true; - int ret = - player_set_play_position(player->player_, player->should_seek_to_, - true, OnSeekCompleted, player); + if (player->pending_action_ == PendingAction::kPlay) { + int ret = player_start(player->player_); if (ret != PLAYER_ERROR_NONE) { - player->seeking_ = false; - player->log_listener_(player->player_id_, - "player_set_play_position failed."); + player->log_listener_(player->player_id_, "player_start failed."); return G_SOURCE_REMOVE; } - player->should_seek_to_ = -1; + player->StartPositionUpdates(); } + player->pending_action_ = PendingAction::kNone; return G_SOURCE_REMOVE; }, - new IdleData{self, self->is_alive_}, + new IdleData{self, self->is_alive_, self->generation_}, [](gpointer data) { delete static_cast(data); }); } @@ -434,15 +470,47 @@ void AudioPlayer::OnSeekCompleted(void *data) { G_PRIORITY_DEFAULT_IDLE, [](gpointer data) -> gboolean { auto *idle = static_cast(data); - if (!*idle->is_alive) { + if (!*idle->is_alive || idle->generation != idle->player->generation_) { return G_SOURCE_REMOVE; } auto *player = idle->player; - player->seek_completed_listener_(player->player_id_); player->seeking_ = false; + if (player->pending_action_ == PendingAction::kPause) { + player->pending_action_ = PendingAction::kNone; + try { + player->Pause(); + } catch (const AudioPlayerError &error) { + player->OnLog(error.code() + ": " + error.message()); + } + } + bool seek_failed = false; + if (player->should_seek_to_ >= 0) { + try { + int position = player->should_seek_to_; + player->should_seek_to_ = -1; + player->Seek(position); + return G_SOURCE_REMOVE; + } catch (const AudioPlayerError &error) { + seek_failed = true; + player->error_listener_(player->player_id_, error.code(), + error.message()); + } + } + auto action = player->pending_action_; + player->pending_action_ = PendingAction::kNone; + try { + if (action == PendingAction::kPlay) { + player->Play(); + } + } catch (const AudioPlayerError &error) { + player->OnLog(error.code() + ": " + error.message()); + } + if (!seek_failed) { + player->seek_completed_listener_(player->player_id_); + } return G_SOURCE_REMOVE; }, - new IdleData{self, self->is_alive_}, + new IdleData{self, self->is_alive_, self->generation_}, [](gpointer data) { delete static_cast(data); }); } @@ -455,64 +523,70 @@ void AudioPlayer::OnPlayCompleted(void *data) { G_PRIORITY_DEFAULT_IDLE, [](gpointer data) -> gboolean { auto *idle = static_cast(data); - if (!*idle->is_alive) { + if (!*idle->is_alive || idle->generation != idle->player->generation_) { return G_SOURCE_REMOVE; } auto *player = idle->player; try { - player->Seek(0); - player->Stop(); - player->play_completed_listener_(player->player_id_); + if (player->release_mode_ == ReleaseMode::kRelease) { + player->Stop(); + player->play_completed_listener_(player->player_id_); + } else { + player->ResetPlayer(); + player->completing_ = true; + player->PrepareSource(); + } } catch (const AudioPlayerError &error) { + player->completing_ = false; + player->play_completed_listener_(player->player_id_); player->log_listener_(player->player_id_, error.code()); } return G_SOURCE_REMOVE; }, - new IdleData{self, self->is_alive_}, + new IdleData{self, self->is_alive_, self->generation_}, [](gpointer data) { delete static_cast(data); }); } void AudioPlayer::OnInterrupted(player_interrupted_code_e code, void *data) { auto *self = reinterpret_cast(data); - // On TV devices, callbacks are not executed on the main loop. Transfer to - // the main loop so the log event is sent on the platform thread. g_idle_add_full( G_PRIORITY_DEFAULT_IDLE, [](gpointer data) -> gboolean { auto *idle = static_cast(data); - if (!*idle->is_alive) { + if (!*idle->is_alive || idle->generation != idle->player->generation_) { return G_SOURCE_REMOVE; } idle->player->log_listener_(idle->player->player_id_, "Player interrupted."); return G_SOURCE_REMOVE; }, - new IdleData{self, self->is_alive_}, + new IdleData{self, self->is_alive_, self->generation_}, [](gpointer data) { delete static_cast(data); }); } void AudioPlayer::OnError(int code, void *data) { auto *self = reinterpret_cast(data); - // On TV devices, callbacks are not executed on the main loop. Transfer to - // the main loop so the log event is sent on the platform thread. The error - // message is resolved here and carried via a heap-allocated context. struct ErrorData { AudioPlayer *player; std::shared_ptr is_alive; + unsigned int generation; std::string message; }; g_idle_add_full( G_PRIORITY_DEFAULT_IDLE, [](gpointer data) -> gboolean { auto *error_data = static_cast(data); - if (!*error_data->is_alive) { + if (!*error_data->is_alive || + error_data->generation != error_data->player->generation_) { return G_SOURCE_REMOVE; } error_data->player->log_listener_(error_data->player->player_id_, error_data->message); + error_data->player->NotifyPendingCompletion(); return G_SOURCE_REMOVE; }, - new ErrorData{self, self->is_alive_, get_error_message(code)}, + new ErrorData{self, self->is_alive_, self->generation_, + get_error_message(code)}, [](gpointer data) { delete static_cast(data); }); } diff --git a/packages/audioplayers/tizen/src/audio_player.h b/packages/audioplayers/tizen/src/audio_player.h index 9d634bede..e911d8df8 100644 --- a/packages/audioplayers/tizen/src/audio_player.h +++ b/packages/audioplayers/tizen/src/audio_player.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -25,6 +26,9 @@ using SeekCompletedListener = std::function; using PlayCompletedListener = std::function; using LogListener = std::function; +using ErrorListener = + std::function; class AudioPlayer { public: @@ -32,7 +36,7 @@ class AudioPlayer { DurationListener duration_listener, SeekCompletedListener seek_completed_listener, PlayCompletedListener play_completed_listener, - LogListener log_listener); + LogListener log_listener, ErrorListener error_listener); ~AudioPlayer(); @@ -58,11 +62,15 @@ class AudioPlayer { bool IsSourcePrepared(); private: + enum class PendingAction { kNone, kPlay, kPause }; // The player state should be none before calling this function. void CreatePlayer(); // The player state should be idle before calling this function. void PreparePlayer(); + void PrepareSource(); void ResetPlayer(); + // Reports a completion deferred while re-preparing in stop mode. + void NotifyPendingCompletion(); void StartPositionUpdates(); player_state_e GetPlayerState(); @@ -84,8 +92,10 @@ class AudioPlayer { ReleaseMode release_mode_ = ReleaseMode::kRelease; int should_seek_to_ = -1; bool preparing_ = false; + bool completing_ = false; bool seeking_ = false; - bool should_play_ = false; + PendingAction pending_action_ = PendingAction::kNone; + std::atomic generation_{0}; guint timer_id_ = 0; std::shared_ptr is_alive_ = std::make_shared(true); @@ -94,6 +104,7 @@ class AudioPlayer { SeekCompletedListener seek_completed_listener_; PlayCompletedListener play_completed_listener_; LogListener log_listener_; + ErrorListener error_listener_; }; #endif // FLUTTER_PLUGIN_AUDIO_PLAYER_H_ diff --git a/packages/audioplayers/tizen/src/audioplayers_tizen_plugin.cc b/packages/audioplayers/tizen/src/audioplayers_tizen_plugin.cc index f9723bf29..509cfe628 100644 --- a/packages/audioplayers/tizen/src/audioplayers_tizen_plugin.cc +++ b/packages/audioplayers/tizen/src/audioplayers_tizen_plugin.cc @@ -393,9 +393,16 @@ class AudioplayersTizenPlugin : public flutter::Plugin { event_sinks_[player_id]->Success(flutter::EncodableValue(map)); }; + ErrorListener error_listener = [this](const std::string &player_id, + const std::string &code, + const std::string &message) { + event_sinks_[player_id]->Error(code, message, flutter::EncodableValue()); + }; + auto player = std::make_unique( player_id, prepared_listener, duration_listener, - seek_completed_listener, play_completed_listener, log_listener); + seek_completed_listener, play_completed_listener, log_listener, + error_listener); audio_players_[player_id] = std::move(player); }