From 275d57eb6f46416dbfacb31ae2867905f50eefa5 Mon Sep 17 00:00:00 2001 From: Zaggy1024 Date: Mon, 6 Feb 2023 01:24:33 -0600 Subject: [PATCH] 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. --- .../VideoPlayer/VideoPlayerWidget.cpp | 4 +- .../Libraries/LibVideo/PlaybackManager.cpp | 573 ++++++++++++------ Userland/Libraries/LibVideo/PlaybackManager.h | 115 ++-- 3 files changed, 481 insertions(+), 211 deletions(-) diff --git a/Userland/Applications/VideoPlayer/VideoPlayerWidget.cpp b/Userland/Applications/VideoPlayer/VideoPlayerWidget.cpp index fe3b11ba51..1facab92a4 100644 --- a/Userland/Applications/VideoPlayer/VideoPlayerWidget.cpp +++ b/Userland/Applications/VideoPlayer/VideoPlayerWidget.cpp @@ -56,8 +56,8 @@ ErrorOr VideoPlayerWidget::setup_interface() auto progress = value / static_cast(m_seek_slider->max()); auto duration = m_playback_manager->duration().to_milliseconds(); Time timestamp = Time::from_milliseconds(static_cast(round(progress * static_cast(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); diff --git a/Userland/Libraries/LibVideo/PlaybackManager.cpp b/Userland/Libraries/LibVideo/PlaybackManager.cpp index c60f83f582..84bfddf81d 100644 --- a/Userland/Libraries/LibVideo/PlaybackManager.cpp +++ b/Userland/Libraries/LibVideo/PlaybackManager.cpp @@ -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, \ + "Do not return a reference from a fallible expression"); \ + _fatal_expression.release_value(); \ + }) + DecoderErrorOr> PlaybackManager::from_file(Core::Object& event_handler, StringView filename) { NonnullOwnPtr demuxer = TRY(Matroska::MatroskaDemuxer::from_file(filename)); @@ -33,60 +45,30 @@ PlaybackManager::PlaybackManager(Core::Object& event_handler, NonnullOwnPtr()) + , m_playback_handler(make(*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()); - } + 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(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 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(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::max()); - dbgln_if(PLAYBACK_MANAGER_DEBUG, "Time until next frame is {}ms", frame_time_ms); - m_present_timer->start(max(static_cast(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(error)); } +void PlaybackManager::dispatch_new_frame(RefPtr frame) +{ + m_main_loop.post_event(m_event_handler, make(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, \ @@ -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 PlaybackManager::PlaybackStateHandler::seek(Time target_timestamp, SeekMode seek_mode) +{ + return replace_handler_and_delete_this(is_playing(), target_timestamp, seek_mode); +} + +ErrorOr PlaybackManager::PlaybackStateHandler::stop() +{ + return replace_handler_and_delete_this(); +} + +template +ErrorOr PlaybackManager::PlaybackStateHandler::replace_handler_and_delete_this(Args... args) +{ + auto temp_handler = TRY(adopt_nonnull_own_or_enomem(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 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 pause() override + { + manager().m_last_present_in_media_time = current_time(); + return replace_handler_and_delete_this(); + } + ErrorOr buffer() override + { + manager().m_last_present_in_media_time = current_time(); + return replace_handler_and_delete_this(true); + } + + Time current_time() const override + { + return manager().m_last_present_in_media_time + (Time::now_monotonic() - m_last_present_in_real_time); + } + + ErrorOr on_timer_callback() override + { + Optional 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::max()); + dbgln_if(PLAYBACK_MANAGER_DEBUG, "Time until next frame is {}ms", frame_time_ms); + manager().start_timer(max(static_cast(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 play() override + { + return replace_handler_and_delete_this(); + } + 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 on_enter() override + { + manager().m_decode_timer->start(0); + return {}; + } + + StringView name() override { return "Buffering"sv; } + + ErrorOr play() override + { + m_playing = true; + return {}; + } + bool is_playing() override { return m_playing; }; + ErrorOr pause() override + { + m_playing = false; + return {}; + } + + ErrorOr on_buffer_filled() override + { + if (m_playing) + return replace_handler_and_delete_this(); + return replace_handler_and_delete_this(); + } + + 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 exit_seek() + { + if (m_playing) + return replace_handler_and_delete_this(); + return replace_handler_and_delete_this(); + } + + ErrorOr 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 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 play() override + { + m_playing = true; + return {}; + } + bool is_playing() override { return m_playing; }; + ErrorOr pause() override + { + m_playing = false; + return {}; + } + ErrorOr 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 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 on_enter() override + { + return {}; + } + + StringView name() override { return "Stopped"sv; } + + ErrorOr play() override + { + manager().m_last_present_in_media_time = manager().seek_demuxer_to_most_recent_keyframe(Time::zero()); + return replace_handler_and_delete_this(); + } + bool is_playing() override { return false; }; +}; + } diff --git a/Userland/Libraries/LibVideo/PlaybackManager.h b/Userland/Libraries/LibVideo/PlaybackManager.h index c31a01b34d..2006fa6121 100644 --- a/Userland/Libraries/LibVideo/PlaybackManager.h +++ b/Userland/Libraries/LibVideo/PlaybackManager.h @@ -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, 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 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 m_demuxer; @@ -152,20 +147,65 @@ private: NonnullOwnPtr m_decoder; NonnullOwnPtr m_frame_queue; - Optional m_next_frame; RefPtr m_present_timer; unsigned m_decoding_buffer_time_ms = 16; RefPtr m_decode_timer; + NonnullOwnPtr m_playback_handler; + Optional 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 on_enter() { return {}; } + + virtual ErrorOr play() { return {}; }; + virtual bool is_playing() = 0; + virtual ErrorOr pause() { return {}; }; + virtual ErrorOr buffer() { return {}; }; + virtual ErrorOr seek(Time target_timestamp, SeekMode); + virtual ErrorOr stop(); + + virtual Time current_time() const; + + virtual ErrorOr on_timer_callback() { return {}; }; + virtual ErrorOr on_buffer_filled() { return {}; }; + + protected: + template + ErrorOr replace_handler_and_delete_this(Args... args); + + PlaybackManager& manager() const; + + PlaybackManager& manager() + { + return const_cast(const_cast(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; }; }