mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 20:47:45 +00:00
VideoPlayer/LibVideo: Implement the UI functionality for seeking
With these changes, the seek bar can be used, but only to seek to the start of the file. Seeking to anywhere else in the file will cause an error in the demuxer. The timestamp label that was previously invisible now has its text set according to either the playback or seek slider's position.
This commit is contained in:
parent
e216d1a65f
commit
f31621b3f2
7 changed files with 84 additions and 13 deletions
|
@ -38,6 +38,17 @@ VideoPlayerWidget::VideoPlayerWidget(GUI::Window& window)
|
|||
m_seek_slider = player_controls_widget.add<GUI::HorizontalSlider>();
|
||||
m_seek_slider->set_fixed_height(20);
|
||||
m_seek_slider->set_enabled(false);
|
||||
m_seek_slider->on_change = [&](int value) {
|
||||
if (!m_playback_manager)
|
||||
return;
|
||||
update_seek_slider_max();
|
||||
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);
|
||||
};
|
||||
m_seek_slider->set_jump_to_cursor(true);
|
||||
|
||||
auto& toolbar_container = player_controls_widget.add<GUI::ToolbarContainer>();
|
||||
m_toolbar = toolbar_container.add<GUI::Toolbar>();
|
||||
|
@ -56,7 +67,7 @@ VideoPlayerWidget::VideoPlayerWidget(GUI::Window& window)
|
|||
m_toolbar->add_action(*m_play_pause_action);
|
||||
m_toolbar->add<GUI::VerticalSeparator>();
|
||||
m_timestamp_label = m_toolbar->add<GUI::Label>();
|
||||
m_timestamp_label->set_fixed_width(50);
|
||||
m_timestamp_label->set_autosize(true);
|
||||
|
||||
m_toolbar->add<GUI::Widget>(); // Filler widget
|
||||
|
||||
|
@ -89,6 +100,7 @@ void VideoPlayerWidget::open_file(StringView filename)
|
|||
|
||||
close_file();
|
||||
m_playback_manager = load_file_result.release_value();
|
||||
update_seek_slider_max();
|
||||
resume_playback();
|
||||
}
|
||||
|
||||
|
@ -162,6 +174,46 @@ void VideoPlayerWidget::on_decoding_error(Video::DecoderError const& error)
|
|||
GUI::MessageBox::show(&m_window, String::formatted(text_format, error.string_literal()), "Video Player encountered an error"sv);
|
||||
}
|
||||
|
||||
void VideoPlayerWidget::update_seek_slider_max()
|
||||
{
|
||||
if (!m_playback_manager) {
|
||||
m_seek_slider->set_enabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
m_seek_slider->set_max(static_cast<int>(min(m_playback_manager->duration().to_milliseconds(), NumericLimits<int>::max())));
|
||||
m_seek_slider->set_enabled(true);
|
||||
}
|
||||
|
||||
void VideoPlayerWidget::set_current_timestamp(Time timestamp)
|
||||
{
|
||||
set_time_label(timestamp);
|
||||
if (!m_playback_manager)
|
||||
return;
|
||||
auto progress = static_cast<double>(timestamp.to_milliseconds()) / static_cast<double>(m_playback_manager->duration().to_milliseconds());
|
||||
m_seek_slider->set_value(static_cast<int>(round(progress * m_seek_slider->max())), GUI::AllowCallback::No);
|
||||
}
|
||||
|
||||
void VideoPlayerWidget::set_time_label(Time timestamp)
|
||||
{
|
||||
StringBuilder string_builder;
|
||||
auto append_time = [&](Time time) {
|
||||
auto seconds = time.to_seconds();
|
||||
string_builder.appendff("{:02}:{:02}:{:02}", seconds / 3600, seconds / 60, seconds % 60);
|
||||
};
|
||||
|
||||
append_time(timestamp);
|
||||
|
||||
if (m_playback_manager) {
|
||||
string_builder.append(" / "sv);
|
||||
append_time(m_playback_manager->duration());
|
||||
} else {
|
||||
string_builder.append(" / --:--:--.---"sv);
|
||||
}
|
||||
|
||||
m_timestamp_label->set_text(string_builder.string_view());
|
||||
}
|
||||
|
||||
void VideoPlayerWidget::event(Core::Event& event)
|
||||
{
|
||||
if (event.type() == Video::EventType::DecoderErrorOccurred) {
|
||||
|
@ -174,9 +226,8 @@ void VideoPlayerWidget::event(Core::Event& event)
|
|||
m_video_display->set_bitmap(frame_event.frame());
|
||||
m_video_display->repaint();
|
||||
|
||||
m_seek_slider->set_max(m_playback_manager->duration().to_milliseconds());
|
||||
m_seek_slider->set_value(m_playback_manager->current_playback_time().to_milliseconds());
|
||||
m_seek_slider->set_enabled(true);
|
||||
update_seek_slider_max();
|
||||
set_current_timestamp(m_playback_manager->current_playback_time());
|
||||
|
||||
frame_event.accept();
|
||||
} else if (event.type() == Video::EventType::PlaybackStatusChange) {
|
||||
|
|
|
@ -36,6 +36,9 @@ private:
|
|||
VideoPlayerWidget(GUI::Window&);
|
||||
|
||||
void update_play_pause_icon();
|
||||
void update_seek_slider_max();
|
||||
void set_current_timestamp(Time);
|
||||
void set_time_label(Time);
|
||||
void on_decoding_error(Video::DecoderError const&);
|
||||
void display_next_frame();
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ public:
|
|||
return sample.release_nonnull<VideoSample>();
|
||||
}
|
||||
|
||||
virtual DecoderErrorOr<void> seek_to_most_recent_keyframe(Track track, size_t timestamp) = 0;
|
||||
virtual DecoderErrorOr<void> seek_to_most_recent_keyframe(Track track, Time timestamp) = 0;
|
||||
|
||||
virtual DecoderErrorOr<Time> duration() = 0;
|
||||
|
||||
|
|
|
@ -53,9 +53,9 @@ DecoderErrorOr<MatroskaDemuxer::TrackStatus*> MatroskaDemuxer::get_track_status(
|
|||
return &m_track_statuses.get(track).release_value();
|
||||
}
|
||||
|
||||
DecoderErrorOr<void> MatroskaDemuxer::seek_to_most_recent_keyframe(Track track, size_t timestamp)
|
||||
DecoderErrorOr<void> MatroskaDemuxer::seek_to_most_recent_keyframe(Track track, Time timestamp)
|
||||
{
|
||||
if (timestamp == 0) {
|
||||
if (timestamp.is_zero()) {
|
||||
// Removing the track status will cause us to start from the beginning.
|
||||
m_track_statuses.remove(track);
|
||||
return {};
|
||||
|
|
|
@ -27,7 +27,7 @@ public:
|
|||
|
||||
DecoderErrorOr<Vector<Track>> get_tracks_for_type(TrackType type) override;
|
||||
|
||||
DecoderErrorOr<void> seek_to_most_recent_keyframe(Track track, size_t timestamp) override;
|
||||
DecoderErrorOr<void> seek_to_most_recent_keyframe(Track track, Time timestamp) override;
|
||||
|
||||
DecoderErrorOr<Time> duration() override;
|
||||
|
||||
|
|
|
@ -55,8 +55,6 @@ void PlaybackManager::set_playback_status(PlaybackStatus status)
|
|||
if (status == PlaybackStatus::Playing) {
|
||||
if (old_status == PlaybackStatus::Stopped || old_status == PlaybackStatus::Corrupted) {
|
||||
restart_playback();
|
||||
m_frame_queue->clear();
|
||||
m_skipped_frames = 0;
|
||||
}
|
||||
m_last_present_in_real_time = Time::now_monotonic();
|
||||
m_present_timer->start(0);
|
||||
|
@ -94,6 +92,8 @@ bool PlaybackManager::prepare_next_frame()
|
|||
|
||||
Time PlaybackManager::current_playback_time()
|
||||
{
|
||||
if (m_last_present_in_media_time.is_negative())
|
||||
return Time::zero();
|
||||
if (is_playing())
|
||||
return m_last_present_in_media_time + (Time::now_monotonic() - m_last_present_in_real_time);
|
||||
return m_last_present_in_media_time;
|
||||
|
@ -165,6 +165,11 @@ void PlaybackManager::update_presented_frame()
|
|||
return;
|
||||
}
|
||||
|
||||
if (m_last_present_in_media_time.is_negative()) {
|
||||
m_last_present_in_media_time = m_next_frame->timestamp();
|
||||
dbgln_if(PLAYBACK_MANAGER_DEBUG, "We've seeked, set last media time to the next frame {}ms", m_last_present_in_media_time.to_milliseconds());
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -176,15 +181,26 @@ void PlaybackManager::update_presented_frame()
|
|||
m_decode_timer->start(0);
|
||||
}
|
||||
|
||||
void PlaybackManager::restart_playback()
|
||||
void PlaybackManager::seek_to_timestamp(Time timestamp)
|
||||
{
|
||||
m_last_present_in_media_time = Time::zero();
|
||||
dbgln_if(PLAYBACK_MANAGER_DEBUG, "Seeking to {}ms", timestamp.to_milliseconds());
|
||||
m_last_present_in_media_time = Time::min();
|
||||
m_last_present_in_real_time = Time::zero();
|
||||
auto result = m_demuxer->seek_to_most_recent_keyframe(m_selected_video_track, 0);
|
||||
m_frame_queue->clear();
|
||||
m_next_frame.clear();
|
||||
m_skipped_frames = 0;
|
||||
// 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());
|
||||
}
|
||||
|
||||
void PlaybackManager::restart_playback()
|
||||
{
|
||||
seek_to_timestamp(Time::zero());
|
||||
}
|
||||
|
||||
void PlaybackManager::post_decoder_error(DecoderError error)
|
||||
{
|
||||
m_main_loop.post_event(m_event_handler, make<DecoderErrorEvent>(error));
|
||||
|
|
|
@ -101,6 +101,7 @@ public:
|
|||
void resume_playback();
|
||||
void pause_playback();
|
||||
void restart_playback();
|
||||
void seek_to_timestamp(Time);
|
||||
bool is_playing() const { return m_status == PlaybackStatus::Playing; }
|
||||
bool is_buffering() const { return m_status == PlaybackStatus::Buffering; }
|
||||
bool is_stopped() const { return m_status == PlaybackStatus::Stopped; }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue