mirror of
https://github.com/RGBCube/serenity
synced 2025-05-28 13:05:09 +00:00

Fast seeking does not work correctly when seeking in small increments, so it is necessary to use accurate seeking when using certain actions. The PlaybackManager has been changed to accept the seek mode as a parameter to `seek_to_timestamp` to facilitate this. This now means that it no longer has to track a seek mode preference.
310 lines
10 KiB
C++
310 lines
10 KiB
C++
/*
|
|
* Copyright (c) 2022, Gregory Bertilson <zaggy1024@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibGUI/Action.h>
|
|
#include <LibGUI/BoxLayout.h>
|
|
#include <LibGUI/FilePicker.h>
|
|
#include <LibGUI/ImageWidget.h>
|
|
#include <LibGUI/Label.h>
|
|
#include <LibGUI/MessageBox.h>
|
|
#include <LibGUI/SeparatorWidget.h>
|
|
#include <LibGUI/Slider.h>
|
|
#include <LibGUI/Toolbar.h>
|
|
#include <LibGUI/ToolbarContainer.h>
|
|
#include <LibGUI/Window.h>
|
|
#include <Userland/Applications/VideoPlayer/VideoPlayerWindowGML.h>
|
|
|
|
#include "VideoPlayerWidget.h"
|
|
|
|
namespace VideoPlayer {
|
|
|
|
ErrorOr<NonnullRefPtr<VideoPlayerWidget>> VideoPlayerWidget::try_create()
|
|
{
|
|
auto main_widget = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) VideoPlayerWidget()));
|
|
TRY(main_widget->load_from_gml(videoplayer_window_gml));
|
|
|
|
TRY(main_widget->setup_interface());
|
|
|
|
return main_widget;
|
|
}
|
|
|
|
ErrorOr<void> VideoPlayerWidget::setup_interface()
|
|
{
|
|
m_video_display = find_descendant_of_type_named<VideoPlayer::VideoFrameWidget>("video_frame");
|
|
m_video_display->on_click = [&]() { toggle_pause(); };
|
|
|
|
m_seek_slider = find_descendant_of_type_named<GUI::HorizontalSlider>("seek_slider");
|
|
m_seek_slider->on_drag_start = [&]() {
|
|
if (!m_playback_manager)
|
|
return;
|
|
m_was_playing_before_seek = m_playback_manager->is_playing();
|
|
m_playback_manager->pause_playback();
|
|
};
|
|
m_seek_slider->on_drag_end = [&]() {
|
|
if (!m_playback_manager || !m_was_playing_before_seek)
|
|
return;
|
|
m_was_playing_before_seek = false;
|
|
m_playback_manager->resume_playback();
|
|
};
|
|
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))));
|
|
auto seek_mode_to_use = m_seek_slider->knob_dragging() ? seek_mode() : Video::PlaybackManager::SeekMode::Accurate;
|
|
m_playback_manager->seek_to_timestamp(timestamp, seek_mode_to_use);
|
|
set_current_timestamp(m_playback_manager->current_playback_time());
|
|
};
|
|
|
|
m_play_icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/play.png"sv));
|
|
m_pause_icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/pause.png"sv));
|
|
|
|
m_play_pause_action = GUI::Action::create("Play", { Key_Space }, m_play_icon, [&](auto&) {
|
|
toggle_pause();
|
|
});
|
|
|
|
m_cycle_sizing_modes_action = GUI::Action::create("Sizing", [&](auto&) {
|
|
cycle_sizing_modes();
|
|
});
|
|
|
|
m_timestamp_label = find_descendant_of_type_named<GUI::Label>("timestamp");
|
|
m_volume_slider = find_descendant_of_type_named<GUI::HorizontalSlider>("volume_slider");
|
|
find_descendant_of_type_named<GUI::Button>("playback")->set_action(*m_play_pause_action);
|
|
find_descendant_of_type_named<GUI::Button>("sizing")->set_action(*m_cycle_sizing_modes_action);
|
|
|
|
return {};
|
|
}
|
|
|
|
void VideoPlayerWidget::close_file()
|
|
{
|
|
if (m_playback_manager)
|
|
m_playback_manager = nullptr;
|
|
}
|
|
|
|
void VideoPlayerWidget::open_file(StringView filename)
|
|
{
|
|
auto load_file_result = Video::PlaybackManager::from_file(*this, filename);
|
|
|
|
if (load_file_result.is_error()) {
|
|
on_decoding_error(load_file_result.release_error());
|
|
return;
|
|
}
|
|
|
|
m_path = filename;
|
|
update_title();
|
|
|
|
close_file();
|
|
m_playback_manager = load_file_result.release_value();
|
|
update_seek_slider_max();
|
|
resume_playback();
|
|
}
|
|
|
|
void VideoPlayerWidget::update_play_pause_icon()
|
|
{
|
|
if (!m_playback_manager) {
|
|
m_play_pause_action->set_enabled(false);
|
|
m_play_pause_action->set_icon(m_play_icon);
|
|
m_play_pause_action->set_text("Play"sv);
|
|
return;
|
|
}
|
|
|
|
m_play_pause_action->set_enabled(true);
|
|
|
|
if (m_playback_manager->is_playing() || m_was_playing_before_seek) {
|
|
m_play_pause_action->set_icon(m_pause_icon);
|
|
m_play_pause_action->set_text("Pause"sv);
|
|
} else {
|
|
m_play_pause_action->set_icon(m_play_icon);
|
|
m_play_pause_action->set_text("Play"sv);
|
|
}
|
|
}
|
|
|
|
void VideoPlayerWidget::resume_playback()
|
|
{
|
|
if (!m_playback_manager || m_seek_slider->knob_dragging())
|
|
return;
|
|
m_playback_manager->resume_playback();
|
|
update_play_pause_icon();
|
|
}
|
|
|
|
void VideoPlayerWidget::pause_playback()
|
|
{
|
|
if (!m_playback_manager || m_seek_slider->knob_dragging())
|
|
return;
|
|
m_playback_manager->pause_playback();
|
|
update_play_pause_icon();
|
|
}
|
|
|
|
void VideoPlayerWidget::toggle_pause()
|
|
{
|
|
if (!m_playback_manager)
|
|
return;
|
|
if (m_playback_manager->is_playing())
|
|
pause_playback();
|
|
else
|
|
resume_playback();
|
|
}
|
|
|
|
void VideoPlayerWidget::on_decoding_error(Video::DecoderError const& error)
|
|
{
|
|
StringView text_format;
|
|
|
|
switch (error.category()) {
|
|
case Video::DecoderErrorCategory::IO:
|
|
text_format = "Error while reading video:\n{}"sv;
|
|
break;
|
|
case Video::DecoderErrorCategory::Memory:
|
|
text_format = "Ran out of memory:\n{}"sv;
|
|
break;
|
|
case Video::DecoderErrorCategory::Corrupted:
|
|
text_format = "Video was corrupted:\n{}"sv;
|
|
break;
|
|
case Video::DecoderErrorCategory::Invalid:
|
|
text_format = "Invalid call:\n{}"sv;
|
|
break;
|
|
case Video::DecoderErrorCategory::NotImplemented:
|
|
text_format = "Video feature is not yet implemented:\n{}"sv;
|
|
break;
|
|
default:
|
|
text_format = "Unexpected error:\n{}"sv;
|
|
break;
|
|
}
|
|
|
|
GUI::MessageBox::show(window(), DeprecatedString::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) {
|
|
auto& error_event = static_cast<Video::DecoderErrorEvent&>(event);
|
|
on_decoding_error(error_event.error());
|
|
error_event.accept();
|
|
} else if (event.type() == Video::EventType::VideoFramePresent) {
|
|
auto& frame_event = static_cast<Video::VideoFramePresentEvent&>(event);
|
|
|
|
m_video_display->set_bitmap(frame_event.frame());
|
|
m_video_display->repaint();
|
|
|
|
update_seek_slider_max();
|
|
set_current_timestamp(m_playback_manager->current_playback_time());
|
|
|
|
frame_event.accept();
|
|
} 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);
|
|
}
|
|
|
|
void VideoPlayerWidget::cycle_sizing_modes()
|
|
{
|
|
auto sizing_mode = m_video_display->sizing_mode();
|
|
sizing_mode = static_cast<VideoSizingMode>((to_underlying(sizing_mode) + 1) % to_underlying(VideoSizingMode::Sentinel));
|
|
m_video_display->set_sizing_mode(sizing_mode);
|
|
m_video_display->update();
|
|
}
|
|
|
|
void VideoPlayerWidget::update_title()
|
|
{
|
|
StringBuilder string_builder;
|
|
if (m_path.is_empty()) {
|
|
string_builder.append("No video"sv);
|
|
} else {
|
|
string_builder.append(m_path.view());
|
|
}
|
|
|
|
string_builder.append("[*] - Video Player"sv);
|
|
window()->set_title(string_builder.to_deprecated_string());
|
|
}
|
|
|
|
Video::PlaybackManager::SeekMode VideoPlayerWidget::seek_mode()
|
|
{
|
|
if (m_use_fast_seeking->is_checked())
|
|
return Video::PlaybackManager::SeekMode::Fast;
|
|
return Video::PlaybackManager::SeekMode::Accurate;
|
|
}
|
|
|
|
void VideoPlayerWidget::set_seek_mode(Video::PlaybackManager::SeekMode seek_mode)
|
|
{
|
|
m_use_fast_seeking->set_checked(seek_mode == Video::PlaybackManager::SeekMode::Fast);
|
|
}
|
|
|
|
ErrorOr<void> VideoPlayerWidget::initialize_menubar(GUI::Window& window)
|
|
{
|
|
// File menu
|
|
auto file_menu = TRY(window.try_add_menu("&File"));
|
|
TRY(file_menu->try_add_action(GUI::CommonActions::make_open_action([&](auto&) {
|
|
Optional<DeprecatedString> path = GUI::FilePicker::get_open_filepath(&window, "Open video file...");
|
|
if (path.has_value())
|
|
open_file(path.value());
|
|
})));
|
|
TRY(file_menu->try_add_separator());
|
|
TRY(file_menu->try_add_action(GUI::CommonActions::make_quit_action([&](auto&) {
|
|
window.close();
|
|
})));
|
|
|
|
// Playback menu
|
|
auto playback_menu = TRY(window.try_add_menu("&Playback"));
|
|
|
|
// FIXME: Maybe seek mode should be in an options dialog instead. The playback menu may get crowded.
|
|
// For now, leave it here for convenience.
|
|
m_use_fast_seeking = GUI::Action::create_checkable("&Fast Seeking", [&](auto&) {});
|
|
TRY(playback_menu->try_add_action(*m_use_fast_seeking));
|
|
set_seek_mode(Video::PlaybackManager::DEFAULT_SEEK_MODE);
|
|
|
|
// Help menu
|
|
auto help_menu = TRY(window.try_add_menu("&Help"));
|
|
TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("Video Player", TRY(GUI::Icon::try_create_default_icon("app-video-player"sv)), &window)));
|
|
|
|
return {};
|
|
}
|
|
|
|
}
|