LibVideo/PlaybackManager: Organize playback states into virtual classes

Storing playback states in virtual classes allows the behavior to be
much more clearly written. Each `PlaybackStateHandler` subclass can
implement some event-handling functions to model their behavior, and
has functions to change its parent PlaybackManager's state to any other
state.

This will allow expanding the functionality of playback in the future,
for example to allow skipping a single frame forward/backward.
This commit is contained in:
Zaggy1024 2023-02-06 01:24:33 -06:00 committed by Andreas Kling
parent a4c7672802
commit 275d57eb6f
Notes: sideshowbarker 2024-07-17 04:01:41 +09:00
3 changed files with 481 additions and 211 deletions

View file

@ -56,8 +56,8 @@ ErrorOr<void> VideoPlayerWidget::setup_interface()
auto progress = value / static_cast<double>(m_seek_slider->max());
auto duration = m_playback_manager->duration().to_milliseconds();
Time timestamp = Time::from_milliseconds(static_cast<i64>(round(progress * static_cast<double>(duration))));
set_current_timestamp(timestamp);
m_playback_manager->seek_to_timestamp(timestamp);
set_current_timestamp(m_playback_manager->current_playback_time());
};
m_play_icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/play.png"sv));
@ -237,6 +237,8 @@ void VideoPlayerWidget::event(Core::Event& event)
} else if (event.type() == Video::EventType::PlaybackStateChange) {
update_play_pause_icon();
event.accept();
} else if (event.type() == Video::EventType::FatalPlaybackError) {
close_file();
}
Widget::event(event);

View file

@ -13,6 +13,18 @@
namespace Video {
#define TRY_OR_FATAL_ERROR(expression) \
({ \
auto _fatal_expression = (expression); \
if (_fatal_expression.is_error()) { \
dispatch_fatal_error(_fatal_expression.release_error()); \
return; \
} \
static_assert(!::AK::Detail::IsLvalueReference<decltype(_fatal_expression.release_value())>, \
"Do not return a reference from a fallible expression"); \
_fatal_expression.release_value(); \
})
DecoderErrorOr<NonnullOwnPtr<PlaybackManager>> PlaybackManager::from_file(Core::Object& event_handler, StringView filename)
{
NonnullOwnPtr<Demuxer> demuxer = TRY(Matroska::MatroskaDemuxer::from_file(filename));
@ -33,60 +45,30 @@ PlaybackManager::PlaybackManager(Core::Object& event_handler, NonnullOwnPtr<Demu
, m_selected_video_track(video_track)
, m_decoder(move(decoder))
, m_frame_queue(make<VideoFrameQueue>())
, m_playback_handler(make<StoppedStateHandler>(*this))
{
m_present_timer = Core::Timer::create_single_shot(0, [&] { update_presented_frame(); }).release_value_but_fixme_should_propagate_errors();
m_present_timer = Core::Timer::create_single_shot(0, [&] { timer_callback(); }).release_value_but_fixme_should_propagate_errors();
m_decode_timer = Core::Timer::create_single_shot(0, [&] { on_decode_timer(); }).release_value_but_fixme_should_propagate_errors();
}
void PlaybackManager::set_playback_status(PlaybackStatus status)
{
if (status != m_status) {
bool was_stopped = is_stopped();
auto old_status = m_status;
m_status = status;
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Set playback status from {} to {}", playback_status_to_string(old_status), playback_status_to_string(m_status));
if (m_status == PlaybackStatus::Playing) {
if (was_stopped)
restart_playback();
m_last_present_in_real_time = Time::now_monotonic();
m_present_timer->start(0);
} else if (!is_seeking()) {
m_last_present_in_media_time = current_playback_time();
m_last_present_in_real_time = Time::zero();
m_present_timer->stop();
}
m_main_loop.post_event(m_event_handler, make<PlaybackStateChangeEvent>());
}
TRY_OR_FATAL_ERROR(m_playback_handler->on_enter());
}
void PlaybackManager::resume_playback()
{
if (is_seeking()) {
set_playback_status(PlaybackStatus::SeekingPlaying);
return;
}
set_playback_status(PlaybackStatus::Playing);
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Resuming playback.");
TRY_OR_FATAL_ERROR(m_playback_handler->play());
}
void PlaybackManager::pause_playback()
{
if (is_seeking()) {
set_playback_status(PlaybackStatus::SeekingPaused);
return;
}
set_playback_status(PlaybackStatus::Paused);
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Pausing playback.");
if (!m_playback_handler->is_playing())
warnln("Cannot pause.");
TRY_OR_FATAL_ERROR(m_playback_handler->pause());
}
Time PlaybackManager::current_playback_time()
{
if (is_seeking())
return m_seek_to_media_time;
VERIFY(!m_last_present_in_media_time.is_negative());
if (m_status == PlaybackStatus::Playing)
return m_last_present_in_media_time + (Time::now_monotonic() - m_last_present_in_real_time);
return m_last_present_in_media_time;
return m_playback_handler->current_time();
}
Time PlaybackManager::duration()
@ -97,151 +79,49 @@ Time PlaybackManager::duration()
return duration_result.release_value();
}
void PlaybackManager::dispatch_fatal_error(Error error)
{
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Encountered fatal error: {}", error.string_literal());
// FIXME: For threading, this will have to use a pre-allocated event to send to the main loop
// to be able to gracefully handle OOM.
VERIFY(&m_main_loop == &Core::EventLoop::current());
FatalPlaybackErrorEvent event { error };
m_event_handler.dispatch_event(event);
}
void PlaybackManager::on_decoder_error(DecoderError error)
{
// If we don't switch to playing/paused before stopping/becoming corrupted, the player will crash
// due to the invalid playback time.
if (is_seeking())
end_seek();
switch (error.category()) {
case DecoderErrorCategory::EndOfStream:
dbgln_if(PLAYBACK_MANAGER_DEBUG, "{}", error.string_literal());
set_playback_status(PlaybackStatus::Stopped);
TRY_OR_FATAL_ERROR(m_playback_handler->stop());
break;
default:
dbgln("Playback error encountered: {}", error.string_literal());
set_playback_status(PlaybackStatus::Corrupted);
TRY_OR_FATAL_ERROR(m_playback_handler->stop());
m_main_loop.post_event(m_event_handler, make<DecoderErrorEvent>(move(error)));
break;
}
}
void PlaybackManager::end_seek()
void PlaybackManager::timer_callback()
{
dbgln_if(PLAYBACK_MANAGER_DEBUG, "We've finished seeking, set media time to seek time at {}ms and change status", m_seek_to_media_time.to_milliseconds());
VERIFY(!m_seek_to_media_time.is_negative());
m_last_present_in_media_time = m_seek_to_media_time;
m_seek_to_media_time = Time::min();
if (m_status == PlaybackStatus::SeekingPlaying) {
set_playback_status(PlaybackStatus::Playing);
return;
}
VERIFY(m_status == PlaybackStatus::SeekingPaused);
set_playback_status(PlaybackStatus::Paused);
TRY_OR_FATAL_ERROR(m_playback_handler->on_timer_callback());
}
void PlaybackManager::update_presented_frame()
void PlaybackManager::seek_to_timestamp(Time target_timestamp)
{
Optional<FrameQueueItem> future_frame_item;
bool should_present_frame = false;
// Skip frames until we find a frame past the current playback time, and keep the one that precedes it to display.
while ((m_status == PlaybackStatus::Playing || is_seeking()) && !m_frame_queue->is_empty()) {
future_frame_item.emplace(m_frame_queue->dequeue());
m_decode_timer->start(0);
if (future_frame_item->is_error() || future_frame_item->timestamp() >= current_playback_time()) {
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Should present frame, future {} is after {}ms", future_frame_item->debug_string(), current_playback_time().to_milliseconds());
should_present_frame = true;
break;
}
if (m_next_frame.has_value() && !is_seeking()) {
dbgln_if(PLAYBACK_MANAGER_DEBUG, "At {}ms: Dropped {} in favor of {}", current_playback_time().to_milliseconds(), m_next_frame->debug_string(), future_frame_item->debug_string());
m_skipped_frames++;
}
m_next_frame.emplace(future_frame_item.release_value());
}
// If we don't have both of these items, we can't present, since we need to set a timer for
// the next frame. Check if we need to buffer based on the current state.
if (!m_next_frame.has_value() || !future_frame_item.has_value()) {
#if PLAYBACK_MANAGER_DEBUG
StringBuilder debug_string_builder;
debug_string_builder.append("We don't have "sv);
if (!m_next_frame.has_value()) {
debug_string_builder.append("a frame to present"sv);
if (!future_frame_item.has_value())
debug_string_builder.append(" or a future frame"sv);
} else {
debug_string_builder.append("a future frame"sv);
}
debug_string_builder.append(", checking for error and buffering"sv);
dbgln_if(PLAYBACK_MANAGER_DEBUG, debug_string_builder.to_deprecated_string());
#endif
if (future_frame_item.has_value()) {
if (future_frame_item->is_error()) {
on_decoder_error(future_frame_item.release_value().release_error());
return;
}
m_next_frame.emplace(future_frame_item.release_value());
}
if (m_status == PlaybackStatus::Playing)
set_playback_status(PlaybackStatus::Buffering);
m_decode_timer->start(0);
return;
}
// If we have a frame, send it for presentation.
if (should_present_frame) {
if (is_seeking())
end_seek();
else
m_last_present_in_media_time = current_playback_time();
m_last_present_in_real_time = Time::now_monotonic();
m_main_loop.post_event(m_event_handler, make<VideoFramePresentEvent>(m_next_frame.value().bitmap()));
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Sent frame for presentation");
}
// Now that we've presented the current frame, we can throw whatever error is next in queue.
// This way, we always display a frame before the stream ends, and should also show any frames
// we already had when a real error occurs.
if (future_frame_item->is_error()) {
on_decoder_error(future_frame_item.release_value().release_error());
return;
}
// The future frame item becomes the next one to present.
m_next_frame.emplace(future_frame_item.release_value());
if (m_status != PlaybackStatus::Playing) {
dbgln_if(PLAYBACK_MANAGER_DEBUG, "We're not playing! Starting the decode timer");
m_decode_timer->start(0);
return;
}
auto frame_time_ms = (m_next_frame->timestamp() - current_playback_time()).to_milliseconds();
VERIFY(frame_time_ms <= NumericLimits<int>::max());
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Time until next frame is {}ms", frame_time_ms);
m_present_timer->start(max(static_cast<int>(frame_time_ms), 0));
TRY_OR_FATAL_ERROR(m_playback_handler->seek(target_timestamp, m_seek_mode));
}
void PlaybackManager::seek_to_timestamp(Time timestamp)
Time PlaybackManager::seek_demuxer_to_most_recent_keyframe(Time timestamp)
{
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Seeking to {}ms", timestamp.to_milliseconds());
// FIXME: When the demuxer is getting samples off the main thread in the future, this needs to
// mutex so that seeking can't happen while that thread is getting a sample.
auto result = m_demuxer->seek_to_most_recent_keyframe(m_selected_video_track, timestamp);
if (result.is_error())
on_decoder_error(result.release_error());
if (is_playing())
set_playback_status(PlaybackStatus::SeekingPlaying);
else
set_playback_status(PlaybackStatus::SeekingPaused);
m_frame_queue->clear();
m_next_frame.clear();
m_skipped_frames = 0;
if (m_seek_mode == SeekMode::Accurate)
m_seek_to_media_time = timestamp;
else
m_seek_to_media_time = result.release_value();
m_last_present_in_media_time = Time::min();
m_last_present_in_real_time = Time::zero();
m_present_timer->stop();
m_decode_timer->start(0);
return result.release_value();
}
void PlaybackManager::restart_playback()
@ -249,11 +129,21 @@ void PlaybackManager::restart_playback()
seek_to_timestamp(Time::zero());
}
void PlaybackManager::post_decoder_error(DecoderError error)
void PlaybackManager::dispatch_decoder_error(DecoderError error)
{
m_main_loop.post_event(m_event_handler, make<DecoderErrorEvent>(error));
}
void PlaybackManager::dispatch_new_frame(RefPtr<Gfx::Bitmap> frame)
{
m_main_loop.post_event(m_event_handler, make<VideoFramePresentEvent>(frame));
}
void PlaybackManager::start_timer(int milliseconds)
{
m_present_timer->start(milliseconds);
}
bool PlaybackManager::decode_and_queue_one_sample()
{
if (m_frame_queue->size() >= FRAME_BUFFER_COUNT) {
@ -270,7 +160,6 @@ bool PlaybackManager::decode_and_queue_one_sample()
if (_temporary_result.is_error()) { \
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Enqueued decoder error: {}", _temporary_result.error().string_literal()); \
m_frame_queue->enqueue(FrameQueueItem::error_marker(_temporary_result.release_error())); \
m_present_timer->start(0); \
return false; \
} \
static_assert(!::AK::Detail::IsLvalueReference<decltype(_temporary_result.release_value())>, \
@ -290,7 +179,7 @@ bool PlaybackManager::decode_and_queue_one_sample()
if (frame_result.error().category() == DecoderErrorCategory::NeedsMoreInput)
break;
post_decoder_error(frame_result.release_error());
dispatch_decoder_error(frame_result.release_error());
return false;
}
@ -320,11 +209,10 @@ bool PlaybackManager::decode_and_queue_one_sample()
auto bitmap = TRY_OR_ENQUEUE_ERROR(decoded_frame->to_bitmap());
m_frame_queue->enqueue(FrameQueueItem::frame(bitmap, frame_sample->timestamp()));
m_present_timer->start(0);
#if PLAYBACK_MANAGER_DEBUG
auto end_time = Time::now_monotonic();
dbgln("Decoding took {}ms, queue is {} items", (end_time - start_time).to_milliseconds(), m_frame_queue->size());
dbgln("Decoding sample at {}ms took {}ms, queue contains {} items", frame_sample->timestamp().to_milliseconds(), (end_time - start_time).to_milliseconds(), m_frame_queue->size());
#endif
return true;
@ -332,14 +220,361 @@ bool PlaybackManager::decode_and_queue_one_sample()
void PlaybackManager::on_decode_timer()
{
if (!decode_and_queue_one_sample() && is_buffering()) {
set_playback_status(PlaybackStatus::Playing);
if (!decode_and_queue_one_sample()) {
// Note: When threading is implemented, this must be dispatched via an event loop.
TRY_OR_FATAL_ERROR(m_playback_handler->on_buffer_filled());
return;
}
// Continually decode until buffering is complete
if (is_buffering() || is_seeking())
m_decode_timer->start(0);
m_decode_timer->start(0);
}
Time PlaybackManager::PlaybackStateHandler::current_time() const
{
return m_manager.m_last_present_in_media_time;
}
ErrorOr<void> PlaybackManager::PlaybackStateHandler::seek(Time target_timestamp, SeekMode seek_mode)
{
return replace_handler_and_delete_this<SeekingStateHandler>(is_playing(), target_timestamp, seek_mode);
}
ErrorOr<void> PlaybackManager::PlaybackStateHandler::stop()
{
return replace_handler_and_delete_this<StoppedStateHandler>();
}
template<class T, class... Args>
ErrorOr<void> PlaybackManager::PlaybackStateHandler::replace_handler_and_delete_this(Args... args)
{
auto temp_handler = TRY(adopt_nonnull_own_or_enomem<PlaybackStateHandler>(new (nothrow) T(m_manager, args...)));
m_manager.m_playback_handler.swap(temp_handler);
#if PLAYBACK_MANAGER_DEBUG
m_has_exited = true;
dbgln("Changing state from {} to {}", temp_handler->name(), m_manager.m_playback_handler->name());
#endif
TRY(m_manager.m_playback_handler->on_enter());
return {};
}
PlaybackManager& PlaybackManager::PlaybackStateHandler::manager() const
{
#if PLAYBACK_MANAGER_DEBUG
VERIFY(!m_has_exited);
#endif
return m_manager;
}
class PlaybackManager::PlayingStateHandler : public PlaybackManager::PlaybackStateHandler {
public:
PlayingStateHandler(PlaybackManager& manager)
: PlaybackStateHandler(manager)
{
}
~PlayingStateHandler() override = default;
private:
ErrorOr<void> on_enter() override
{
m_last_present_in_real_time = Time::now_monotonic();
return on_timer_callback();
}
StringView name() override { return "Playing"sv; }
bool is_playing() override { return true; };
ErrorOr<void> pause() override
{
manager().m_last_present_in_media_time = current_time();
return replace_handler_and_delete_this<PausedStateHandler>();
}
ErrorOr<void> buffer() override
{
manager().m_last_present_in_media_time = current_time();
return replace_handler_and_delete_this<BufferingStateHandler>(true);
}
Time current_time() const override
{
return manager().m_last_present_in_media_time + (Time::now_monotonic() - m_last_present_in_real_time);
}
ErrorOr<void> on_timer_callback() override
{
Optional<FrameQueueItem> future_frame_item;
bool should_present_frame = false;
// Skip frames until we find a frame past the current playback time, and keep the one that precedes it to display.
while (!manager().m_frame_queue->is_empty()) {
future_frame_item.emplace(manager().m_frame_queue->dequeue());
manager().m_decode_timer->start(0);
if (future_frame_item->is_error() || future_frame_item->timestamp() >= current_time()) {
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Should present frame, future {} is error or after {}ms", future_frame_item->debug_string(), current_time().to_milliseconds());
should_present_frame = true;
break;
}
if (manager().m_next_frame.has_value()) {
dbgln_if(PLAYBACK_MANAGER_DEBUG, "At {}ms: Dropped {} in favor of {}", current_time().to_milliseconds(), manager().m_next_frame->debug_string(), future_frame_item->debug_string());
manager().m_skipped_frames++;
}
manager().m_next_frame.emplace(future_frame_item.release_value());
}
// If we don't have both of these items, we can't present, since we need to set a timer for
// the next frame. Check if we need to buffer based on the current state.
if (!manager().m_next_frame.has_value() || !future_frame_item.has_value()) {
#if PLAYBACK_MANAGER_DEBUG
StringBuilder debug_string_builder;
debug_string_builder.append("We don't have "sv);
if (!manager().m_next_frame.has_value()) {
debug_string_builder.append("a frame to present"sv);
if (!future_frame_item.has_value())
debug_string_builder.append(" or a future frame"sv);
} else {
debug_string_builder.append("a future frame"sv);
}
debug_string_builder.append(", checking for error and buffering"sv);
dbgln_if(PLAYBACK_MANAGER_DEBUG, debug_string_builder.to_deprecated_string());
#endif
if (future_frame_item.has_value()) {
if (future_frame_item->is_error()) {
manager().on_decoder_error(future_frame_item.release_value().release_error());
return {};
}
manager().m_next_frame.emplace(future_frame_item.release_value());
}
TRY(buffer());
return {};
}
// If we have a frame, send it for presentation.
if (should_present_frame) {
auto now = Time::now_monotonic();
manager().m_last_present_in_media_time += now - m_last_present_in_real_time;
m_last_present_in_real_time = now;
manager().dispatch_new_frame(manager().m_next_frame.value().bitmap());
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Sent frame for presentation");
}
// Now that we've presented the current frame, we can throw whatever error is next in queue.
// This way, we always display a frame before the stream ends, and should also show any frames
// we already had when a real error occurs.
if (future_frame_item->is_error()) {
manager().on_decoder_error(future_frame_item.release_value().release_error());
return {};
}
// The future frame item becomes the next one to present.
manager().m_next_frame.emplace(future_frame_item.release_value());
auto frame_time_ms = (manager().m_next_frame->timestamp() - current_time()).to_milliseconds();
VERIFY(frame_time_ms <= NumericLimits<int>::max());
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Time until next frame is {}ms", frame_time_ms);
manager().start_timer(max(static_cast<int>(frame_time_ms), 0));
return {};
}
Time m_last_present_in_real_time = Time::zero();
};
class PlaybackManager::PausedStateHandler : public PlaybackManager::PlaybackStateHandler {
public:
PausedStateHandler(PlaybackManager& manager)
: PlaybackStateHandler(manager)
{
}
~PausedStateHandler() override = default;
private:
StringView name() override { return "Paused"sv; }
ErrorOr<void> play() override
{
return replace_handler_and_delete_this<PlayingStateHandler>();
}
bool is_playing() override { return false; };
};
class PlaybackManager::BufferingStateHandler : public PlaybackManager::PlaybackStateHandler {
public:
BufferingStateHandler(PlaybackManager& manager, bool playing)
: PlaybackStateHandler(manager)
, m_playing(playing)
{
}
~BufferingStateHandler() override = default;
private:
ErrorOr<void> on_enter() override
{
manager().m_decode_timer->start(0);
return {};
}
StringView name() override { return "Buffering"sv; }
ErrorOr<void> play() override
{
m_playing = true;
return {};
}
bool is_playing() override { return m_playing; };
ErrorOr<void> pause() override
{
m_playing = false;
return {};
}
ErrorOr<void> on_buffer_filled() override
{
if (m_playing)
return replace_handler_and_delete_this<PlayingStateHandler>();
return replace_handler_and_delete_this<PausedStateHandler>();
}
bool m_playing { false };
};
class PlaybackManager::SeekingStateHandler : public PlaybackManager::PlaybackStateHandler {
public:
SeekingStateHandler(PlaybackManager& manager, bool playing, Time target_timestamp, SeekMode seek_mode)
: PlaybackStateHandler(manager)
, m_playing(playing)
, m_target_timestamp(target_timestamp)
, m_seek_mode(seek_mode)
{
}
~SeekingStateHandler() override = default;
private:
ErrorOr<void> exit_seek()
{
if (m_playing)
return replace_handler_and_delete_this<PlayingStateHandler>();
return replace_handler_and_delete_this<PausedStateHandler>();
}
ErrorOr<void> on_enter() override
{
auto keyframe_timestamp = manager().seek_demuxer_to_most_recent_keyframe(m_target_timestamp);
dbgln_if(PLAYBACK_MANAGER_DEBUG, "{} seeking to timestamp target {}ms, selected keyframe at {}ms", m_seek_mode == SeekMode::Accurate ? "Accurate"sv : "Fast"sv, m_target_timestamp.to_milliseconds(), keyframe_timestamp.to_milliseconds());
if (m_seek_mode == SeekMode::Fast) {
m_target_timestamp = keyframe_timestamp;
}
if (m_target_timestamp < manager().m_last_present_in_media_time) {
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Timestamp is earlier than current media time, clearing queue");
manager().m_frame_queue->clear();
manager().m_next_frame.clear();
} else if (manager().m_next_frame.has_value() && manager().m_next_frame.value().timestamp() > m_target_timestamp) {
manager().m_last_present_in_media_time = m_target_timestamp;
return exit_seek();
}
return skip_samples_until_timestamp();
}
ErrorOr<void> skip_samples_until_timestamp()
{
while (!manager().m_frame_queue->is_empty()) {
auto item = manager().m_frame_queue->dequeue();
manager().m_decode_timer->start(0);
if (item.is_error()) {
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Encountered error while seeking: {}", item.error().description());
manager().on_decoder_error(item.release_error());
return {};
}
if (item.is_frame()) {
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Dequeuing frame at {}ms and comparing to seek target {}ms", item.timestamp().to_milliseconds(), m_target_timestamp.to_milliseconds());
if (item.timestamp() > m_target_timestamp) {
// Fast seeking will result in an equal timestamp, so we can exit as soon as we see the next frame.
if (manager().m_next_frame.has_value()) {
manager().dispatch_new_frame(manager().m_next_frame.release_value().bitmap());
manager().m_last_present_in_media_time = m_target_timestamp;
}
manager().m_next_frame.emplace(item);
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Exiting seek to {} state at {}ms", m_playing ? "Playing" : "Paused", manager().m_last_present_in_media_time.to_milliseconds());
return exit_seek();
}
manager().m_next_frame.emplace(item);
continue;
}
VERIFY_NOT_REACHED();
}
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Frame queue is empty while seeking, waiting for buffer fill.");
manager().m_decode_timer->start(0);
return {};
}
StringView name() override { return "Seeking"sv; }
ErrorOr<void> play() override
{
m_playing = true;
return {};
}
bool is_playing() override { return m_playing; };
ErrorOr<void> pause() override
{
m_playing = false;
return {};
}
ErrorOr<void> seek(Time target_timestamp, SeekMode seek_mode) override
{
m_target_timestamp = target_timestamp;
m_seek_mode = seek_mode;
return on_enter();
}
Time current_time() const override
{
return m_target_timestamp;
}
// We won't need this override when threaded, the queue can pause us in on_enter().
ErrorOr<void> on_buffer_filled() override
{
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Buffer filled while seeking, dequeuing until timestamp.");
return skip_samples_until_timestamp();
}
bool m_playing { false };
Time m_target_timestamp { Time::zero() };
SeekMode m_seek_mode { SeekMode::Accurate };
};
class PlaybackManager::StoppedStateHandler : public PlaybackManager::PlaybackStateHandler {
public:
StoppedStateHandler(PlaybackManager& manager)
: PlaybackStateHandler(manager)
{
}
~StoppedStateHandler() override = default;
private:
ErrorOr<void> on_enter() override
{
return {};
}
StringView name() override { return "Stopped"sv; }
ErrorOr<void> play() override
{
manager().m_last_present_in_media_time = manager().seek_demuxer_to_most_recent_keyframe(Time::zero());
return replace_handler_and_delete_this<PlayingStateHandler>();
}
bool is_playing() override { return false; };
};
}

View file

@ -24,16 +24,6 @@
namespace Video {
enum class PlaybackStatus {
Playing,
Paused,
Buffering,
SeekingPlaying,
SeekingPaused,
Stopped,
Corrupted,
};
struct FrameQueueItem {
enum class Type {
Frame,
@ -109,10 +99,10 @@ public:
void pause_playback();
void restart_playback();
void seek_to_timestamp(Time);
bool is_playing() const { return m_status == PlaybackStatus::Playing || m_status == PlaybackStatus::SeekingPlaying || m_status == PlaybackStatus::Buffering; }
bool is_seeking() const { return m_status == PlaybackStatus::SeekingPlaying || m_status == PlaybackStatus::SeekingPaused; }
bool is_buffering() const { return m_status == PlaybackStatus::Buffering; }
bool is_stopped() const { return m_status == PlaybackStatus::Stopped || m_status == PlaybackStatus::Corrupted; }
bool is_playing() const
{
return m_playback_handler->is_playing();
}
SeekMode seek_mode() { return m_seek_mode; }
void set_seek_mode(SeekMode mode) { m_seek_mode = mode; }
@ -127,24 +117,29 @@ public:
Function<void(NonnullRefPtr<Gfx::Bitmap>, Time)> on_frame_present;
private:
void set_playback_status(PlaybackStatus status);
class PlaybackStateHandler;
class PlayingStateHandler;
class PausedStateHandler;
class BufferingStateHandler;
class SeekingStateHandler;
class StoppedStateHandler;
void end_seek();
void update_presented_frame();
void start_timer(int milliseconds);
void timer_callback();
Time seek_demuxer_to_most_recent_keyframe(Time timestamp);
// May run off the main thread
void post_decoder_error(DecoderError error);
bool decode_and_queue_one_sample();
void on_decode_timer();
void dispatch_decoder_error(DecoderError error);
void dispatch_new_frame(RefPtr<Gfx::Bitmap> frame);
void dispatch_fatal_error(Error);
Core::Object& m_event_handler;
Core::EventLoop& m_main_loop;
PlaybackStatus m_status { PlaybackStatus::Stopped };
Time m_last_present_in_media_time = Time::zero();
Time m_last_present_in_real_time = Time::zero();
Time m_seek_to_media_time = Time::min();
SeekMode m_seek_mode = DEFAULT_SEEK_MODE;
NonnullOwnPtr<Demuxer> m_demuxer;
@ -152,20 +147,65 @@ private:
NonnullOwnPtr<VideoDecoder> m_decoder;
NonnullOwnPtr<VideoFrameQueue> m_frame_queue;
Optional<FrameQueueItem> m_next_frame;
RefPtr<Core::Timer> m_present_timer;
unsigned m_decoding_buffer_time_ms = 16;
RefPtr<Core::Timer> m_decode_timer;
NonnullOwnPtr<PlaybackStateHandler> m_playback_handler;
Optional<FrameQueueItem> m_next_frame;
u64 m_skipped_frames;
// This is a nested class to allow private access.
class PlaybackStateHandler {
public:
PlaybackStateHandler(PlaybackManager& manager)
: m_manager(manager)
{
}
virtual ~PlaybackStateHandler() = default;
virtual StringView name() = 0;
virtual ErrorOr<void> on_enter() { return {}; }
virtual ErrorOr<void> play() { return {}; };
virtual bool is_playing() = 0;
virtual ErrorOr<void> pause() { return {}; };
virtual ErrorOr<void> buffer() { return {}; };
virtual ErrorOr<void> seek(Time target_timestamp, SeekMode);
virtual ErrorOr<void> stop();
virtual Time current_time() const;
virtual ErrorOr<void> on_timer_callback() { return {}; };
virtual ErrorOr<void> on_buffer_filled() { return {}; };
protected:
template<class T, class... Args>
ErrorOr<void> replace_handler_and_delete_this(Args... args);
PlaybackManager& manager() const;
PlaybackManager& manager()
{
return const_cast<PlaybackManager&>(const_cast<PlaybackStateHandler const*>(this)->manager());
}
private:
PlaybackManager& m_manager;
#if PLAYBACK_MANAGER_DEBUG
bool m_has_exited { false };
#endif
};
};
enum EventType : unsigned {
DecoderErrorOccurred = (('v' << 2) | ('i' << 1) | 'd') << 4,
VideoFramePresent,
PlaybackStateChange,
FatalPlaybackError,
};
class DecoderErrorEvent : public Core::Event {
@ -208,25 +248,18 @@ public:
virtual ~PlaybackStateChangeEvent() = default;
};
inline StringView playback_status_to_string(PlaybackStatus status)
{
switch (status) {
case PlaybackStatus::Playing:
return "Playing"sv;
case PlaybackStatus::Paused:
return "Paused"sv;
case PlaybackStatus::Buffering:
return "Buffering"sv;
case PlaybackStatus::SeekingPlaying:
return "SeekingPlaying"sv;
case PlaybackStatus::SeekingPaused:
return "SeekingPaused"sv;
case PlaybackStatus::Stopped:
return "Stopped"sv;
case PlaybackStatus::Corrupted:
return "Corrupted"sv;
class FatalPlaybackErrorEvent : public Core::Event {
public:
explicit FatalPlaybackErrorEvent(Error error)
: Core::Event(FatalPlaybackError)
, m_error(error)
{
}
return "Unknown"sv;
virtual ~FatalPlaybackErrorEvent() = default;
Error error() { return m_error; }
private:
Error m_error;
};
}