diff options
| author | Carl Hetherington <cth@carlh.net> | 2026-01-25 01:02:02 +0100 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2026-02-16 01:20:38 +0100 |
| commit | 5d522d0b3648d8b248efc16c0238af2cb6958cdf (patch) | |
| tree | d05b15b8178b0c5c0478ebc6b098be3c0773dc51 | |
| parent | f9e61eb0bf8b51a6ffd30857d6d4cced5479f19e (diff) | |
Move player frame (DOMFrame) class into wx/ as PlayerFrame.
| -rw-r--r-- | src/lib/dcp_examiner.h | 1 | ||||
| -rw-r--r-- | src/tools/dcpomatic_player.cc | 1252 | ||||
| -rw-r--r-- | src/wx/player_frame.cc | 1335 | ||||
| -rw-r--r-- | src/wx/player_frame.h | 161 | ||||
| -rw-r--r-- | src/wx/wscript | 1 |
5 files changed, 1501 insertions, 1249 deletions
diff --git a/src/lib/dcp_examiner.h b/src/lib/dcp_examiner.h index 12cd32500..c3a916bff 100644 --- a/src/lib/dcp_examiner.h +++ b/src/lib/dcp_examiner.h @@ -28,6 +28,7 @@ #include "dcp_text_track.h" #include "dcpomatic_assert.h" #include "video_examiner.h" +#include <dcp/content_kind.h> #include <dcp/dcp_time.h> #include <dcp/rating.h> diff --git a/src/tools/dcpomatic_player.cc b/src/tools/dcpomatic_player.cc index d73f6dfe2..e120048fa 100644 --- a/src/tools/dcpomatic_player.cc +++ b/src/tools/dcpomatic_player.cc @@ -18,75 +18,36 @@ */ -#include "wx/about_dialog.h" -#include "wx/audio_dialog.h" -#include "wx/file_dialog.h" -#include "wx/film_viewer.h" #include "wx/i18n_setup.h" #include "wx/id.h" -#include "wx/nag_dialog.h" -#include "wx/player_config_dialog.h" -#include "wx/player_information.h" -#include "wx/player_stress_tester.h" -#include "wx/playlist_controls.h" -#include "wx/report_problem_dialog.h" -#include "wx/standard_controls.h" -#include "wx/system_information_dialog.h" -#include "wx/timer_display.h" -#include "wx/update_dialog.h" -#include "wx/verify_dcp_dialog.h" +#include "wx/player_frame.h" #include "wx/wx_ptr.h" #include "wx/wx_signal_manager.h" #include "wx/wx_util.h" #include "wx/wx_variant.h" -#include "lib/audio_content.h" -#include "lib/config.h" #include "lib/constants.h" -#include "lib/copy_dcp_details_to_film.h" #include "lib/cross.h" -#include "lib/dcp_content.h" #include "lib/dcp_examiner.h" -#include "lib/dcpomatic_log.h" #include "lib/dcpomatic_socket.h" -#include "lib/examine_content_job.h" #include "lib/ffmpeg_content.h" -#include "lib/file_log.h" #include "lib/film.h" -#include "lib/font_config.h" -#include "lib/http_server.h" -#include "lib/image.h" -#include "lib/image_jpeg.h" -#include "lib/image_png.h" -#include "lib/internal_player_server.h" #include "lib/internet.h" #include "lib/job.h" -#include "lib/job_manager.h" -#include "lib/null_log.h" #include "lib/player.h" #include "lib/player_video.h" #include "lib/ratio.h" #include "lib/scoped_temporary.h" #include "lib/server.h" -#include "lib/text_content.h" #include "lib/update_checker.h" -#include "lib/variant.h" #include "lib/verify_dcp_job.h" -#include "lib/video_content.h" -#include <dcp/cpl.h> #include <dcp/dcp.h> #include <dcp/exceptions.h> #include <dcp/filesystem.h> -#include <dcp/scope_guard.h> -#include <dcp/search.h> #include <dcp/warnings.h> LIBDCP_DISABLE_WARNINGS #include <wx/cmdline.h> -#include <wx/display.h> -#include <wx/dnd.h> -#include <wx/preferences.h> #include <wx/progdlg.h> #include <wx/splash.h> -#include <wx/stdpaths.h> #include <wx/wx.h> LIBDCP_ENABLE_WARNINGS #ifdef __WXGTK__ @@ -101,1224 +62,17 @@ LIBDCP_ENABLE_WARNINGS #endif -#define MAX_CPLS 32 - - using std::cout; -using std::dynamic_pointer_cast; using std::exception; using std::list; -using std::make_shared; -using std::shared_ptr; using std::string; -using std::vector; -using std::weak_ptr; -using boost::bind; -using boost::optional; using boost::scoped_array; -using boost::thread; #if BOOST_VERSION >= 106100 using namespace boost::placeholders; #endif using namespace dcpomatic; -enum { - ID_file_open = DCPOMATIC_MAIN_MENU, - ID_file_add_ov, - ID_file_add_kdm, - ID_file_save_frame, - ID_file_history, - /* Allow spare IDs after _history for the recent files list */ - ID_file_close = DCPOMATIC_MAIN_MENU + 100, - ID_view_cpl, - /* Allow spare IDs for CPLs */ - ID_view_full_screen = DCPOMATIC_MAIN_MENU + 200, - ID_view_dual_screen, - ID_view_closed_captions, - ID_view_eye, - ID_view_eye_left, - ID_view_eye_right, - ID_view_scale_appropriate, - ID_view_scale_full, - ID_view_scale_half, - ID_view_scale_quarter, - ID_help_report_a_problem, - ID_tools_verify, - ID_tools_audio_graph, - ID_tools_check_for_updates, - ID_tools_timing, - ID_tools_system_information, - /* IDs for shortcuts (with no associated menu item) */ - ID_start_stop, - ID_go_back_frame, - ID_go_forward_frame, - ID_go_back_small_amount, - ID_go_forward_small_amount, - ID_go_back_medium_amount, - ID_go_forward_medium_amount, - ID_go_back_large_amount, - ID_go_forward_large_amount, - ID_go_to_start, - ID_go_to_end -}; - - -class DOMFrame : public wxFrame -{ -public: - - class DCPDropTarget : public wxFileDropTarget - { - public: - DCPDropTarget(DOMFrame* owner) - : _frame(owner) - {} - - bool OnDropFiles(wxCoord, wxCoord, wxArrayString const& filenames) override - { - if (filenames.GetCount() == 1) { - /* Try to load a directory */ - auto path = boost::filesystem::path(wx_to_std(filenames[0])); - if (dcp::filesystem::is_directory(path)) { - _frame->load_dcp(wx_to_std(filenames[0])); - return true; - } - } - - if (filenames.GetCount() >= 1) { - /* Try to load the parent if we drop some files, one if which is an asset map */ - for (size_t i = 0; i < filenames.GetCount(); ++i) { - auto path = boost::filesystem::path(wx_to_std(filenames[i])); - if (path.filename() == "ASSETMAP" || path.filename() == "ASSETMAP.xml") { - _frame->load_dcp(path.parent_path()); - return true; - } - } - } - - return false; - } - - private: - DOMFrame* _frame; - }; - - - DOMFrame() - : wxFrame(nullptr, -1, variant::wx::dcpomatic_player()) - , _mode(Config::instance()->player_mode()) - /* Use a panel as the only child of the Frame so that we avoid - the dark-grey background on Windows. - */ - , _overall_panel(new wxPanel(this, wxID_ANY)) - , _viewer(_overall_panel, true) - , _main_sizer(new wxBoxSizer(wxVERTICAL)) - { - dcpomatic_log = make_shared<NullLog>(); - -#if defined(DCPOMATIC_WINDOWS) - maybe_open_console(); - cout << variant::dcpomatic_player() << " is starting." << "\n"; -#endif - - auto bar = new wxMenuBar; - setup_menu(bar); - set_menu_sensitivity(); - SetMenuBar(bar); - -#ifdef DCPOMATIC_WINDOWS - SetIcon(wxIcon(std_to_wx("id"))); -#endif - - _config_changed_connection = Config::instance()->Changed.connect(boost::bind(&DOMFrame::config_changed, this, _1)); - update_from_config(Config::PLAYER_DEBUG_LOG); - - Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_open, this), ID_file_open); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_add_ov, this), ID_file_add_ov); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_add_kdm, this), ID_file_add_kdm); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_save_frame, this), ID_file_save_frame); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_history, this, _1), ID_file_history, ID_file_history + HISTORY_SIZE); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_close, this), ID_file_close); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_exit, this), wxID_EXIT); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::edit_preferences, this), wxID_PREFERENCES); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::view_full_screen, this), ID_view_full_screen); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::view_dual_screen, this), ID_view_dual_screen); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::view_closed_captions, this), ID_view_closed_captions); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::view_cpl, this, _1), ID_view_cpl, ID_view_cpl + MAX_CPLS); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::view_eye_changed, this, _1), ID_view_eye_left); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::view_eye_changed, this, _1), ID_view_eye_right); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::set_decode_reduction, this, optional<int>(0)), ID_view_scale_full); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::set_decode_reduction, this, optional<int>(1)), ID_view_scale_half); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::set_decode_reduction, this, optional<int>(2)), ID_view_scale_quarter); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::help_about, this), wxID_ABOUT); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::help_report_a_problem, this), ID_help_report_a_problem); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::tools_verify, this), ID_tools_verify); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::tools_audio_graph, this), ID_tools_audio_graph); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::tools_check_for_updates, this), ID_tools_check_for_updates); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::tools_timing, this), ID_tools_timing); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::tools_system_information, this), ID_tools_system_information); - - Bind(wxEVT_CLOSE_WINDOW, boost::bind(&DOMFrame::close, this, _1)); - - if (Config::instance()->enable_player_http_server()) { - update_content_store(); - } - - if (Config::instance()->player_mode() == Config::PlayerMode::DUAL) { - auto pc = new PlaylistControls(_overall_panel, _viewer); - _controls = pc; - pc->ResetFilm.connect(bind(&DOMFrame::reset_film_weak, this, _1, _2)); - } else { - _controls = new StandardControls(_overall_panel, _viewer, false); - } - _controls->set_film(_viewer.film()); - _viewer.set_dcp_decode_reduction(Config::instance()->decode_reduction()); - _viewer.PlaybackPermitted.connect(bind(&DOMFrame::playback_permitted, this)); - _viewer.TooManyDropped.connect(bind(&DOMFrame::too_many_frames_dropped, this)); - _info = new PlayerInformation(_overall_panel, _viewer); - setup_main_sizer(Config::instance()->player_mode()); -#ifdef __WXOSX__ - int accelerators = 12; -#else - int accelerators = 11; -#endif - - _stress.setup(this, _controls); - - std::vector<wxAcceleratorEntry> accel(accelerators); - accel[0].Set(wxACCEL_NORMAL, WXK_SPACE, ID_start_stop); - accel[1].Set(wxACCEL_NORMAL, WXK_LEFT, ID_go_back_frame); - accel[2].Set(wxACCEL_NORMAL, WXK_RIGHT, ID_go_forward_frame); - accel[3].Set(wxACCEL_SHIFT, WXK_LEFT, ID_go_back_small_amount); - accel[4].Set(wxACCEL_SHIFT, WXK_RIGHT, ID_go_forward_small_amount); - accel[5].Set(wxACCEL_CTRL, WXK_LEFT, ID_go_back_medium_amount); - accel[6].Set(wxACCEL_CTRL, WXK_RIGHT, ID_go_forward_medium_amount); - accel[7].Set(wxACCEL_SHIFT | wxACCEL_CTRL, WXK_LEFT, ID_go_back_large_amount); - accel[8].Set(wxACCEL_SHIFT | wxACCEL_CTRL, WXK_RIGHT, ID_go_forward_large_amount); - accel[9].Set(wxACCEL_NORMAL, WXK_HOME, ID_go_to_start); - accel[10].Set(wxACCEL_NORMAL, WXK_END, ID_go_to_end); -#ifdef __WXOSX__ - accel[11].Set(wxACCEL_CTRL, static_cast<int>('W'), ID_file_close); -#endif - wxAcceleratorTable accel_table(accelerators, accel.data()); - SetAcceleratorTable(accel_table); - - Bind(wxEVT_MENU, boost::bind(&DOMFrame::start_stop_pressed, this), ID_start_stop); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::go_back_frame, this), ID_go_back_frame); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::go_forward_frame, this), ID_go_forward_frame); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::go_seconds, this, -60), ID_go_back_small_amount); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::go_seconds, this, 60), ID_go_forward_small_amount); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::go_seconds, this, -600), ID_go_back_medium_amount); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::go_seconds, this, 600), ID_go_forward_medium_amount); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::go_seconds, this, -3600), ID_go_back_large_amount); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::go_seconds, this, 3600), ID_go_forward_large_amount); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::go_to_start, this), ID_go_to_start); - Bind(wxEVT_MENU, boost::bind(&DOMFrame::go_to_end, this), ID_go_to_end); - - reset_film(); - - UpdateChecker::instance()->StateChanged.connect(boost::bind(&DOMFrame::update_checker_state_changed, this)); - setup_screen(); - - _stress.LoadDCP.connect(boost::bind(&DOMFrame::load_dcp, this, _1)); - - setup_internal_player_server(); - setup_http_server(); - - SetDropTarget(new DCPDropTarget(this)); - } - - ~DOMFrame() - { - try { - stop_http_server(); - /* It's important that this is stopped before our frame starts destroying its children, - * otherwise UI elements that it depends on will disappear from under it. - */ - _viewer.stop(); - } catch (std::exception& e) { - LOG_ERROR("Destructor threw {}", e.what()); - } catch (...) { - LOG_ERROR("Destructor threw"); - } - } - - void close(wxCloseEvent& ev) - { - FontConfig::drop(); - ev.Skip(); - } - - void setup_main_sizer(Config::PlayerMode mode) - { - _main_sizer->Detach(_viewer.panel()); - _main_sizer->Detach(_controls); - _main_sizer->Detach(_info); - if (mode != Config::PlayerMode::DUAL) { - _main_sizer->Add(_viewer.panel(), 1, wxEXPAND); - } - _main_sizer->Add(_controls, mode == Config::PlayerMode::DUAL ? 1 : 0, wxEXPAND | wxALL, 6); - _main_sizer->Add(_info, 0, wxEXPAND | wxALL, 6); - _overall_panel->SetSizer(_main_sizer); - _overall_panel->Layout(); - } - - bool playback_permitted() - { - if (!_film || !Config::instance()->respect_kdm_validity_periods()) { - return true; - } - - bool ok = true; - for (auto i: _film->content()) { - auto d = dynamic_pointer_cast<DCPContent>(i); - if (d && !d->kdm_timing_window_valid()) { - ok = false; - } - } - - if (!ok) { - error_dialog(this, _("The KDM does not allow playback of this content at this time.")); - } - - return ok; - } - - - void too_many_frames_dropped() - { - if (!Config::instance()->nagged(Config::NAG_TOO_MANY_DROPPED_FRAMES)) { - _viewer.stop(); - } - - NagDialog::maybe_nag( - this, - Config::NAG_TOO_MANY_DROPPED_FRAMES, - wxGetTranslation( - wxString::FromUTF8( - "The player is dropping a lot of frames, so playback may not be accurate.\n\n" - "<b>This does not necessarily mean that the DCP you are playing is defective!</b>\n\n" - "You may be able to improve player performance by:\n" - "• choosing 'decode at half resolution' or 'decode at quarter resolution' from the View menu\n" - "• using a more powerful computer.\n" - ) - ) - ); - } - - void set_decode_reduction(optional<int> reduction) - { - _viewer.set_dcp_decode_reduction(reduction); - _info->triggered_update(); - Config::instance()->set_decode_reduction(reduction); - } - - void load_dcp(boost::filesystem::path dir) - { - DCPOMATIC_ASSERT(_film); - - auto film = std::make_shared<Film>(optional<boost::filesystem::path>()); - - try { - _stress.set_suspended(true); - - /* Handler to set things up once the DCP has been examined */ - auto setup = [this](weak_ptr<Film> weak_film, weak_ptr<Job> weak_job, weak_ptr<Content> weak_content) - { - auto job = weak_job.lock(); - if (!job || !job->finished_ok()) { - return; - } - - auto content = weak_content.lock(); - if (!content) { - return; - } - - auto film = weak_film.lock(); - if (!film) { - return; - } - - film->add_content({content}); - _stress.set_suspended(false); - reset_film(film); - }; - - auto dcp = make_shared<DCPContent>(dir); - auto job = make_shared<ExamineContentJob>(vector<shared_ptr<Content>>{dcp}, true); - _examine_job_connection = job->Finished.connect(boost::bind<void>(setup, weak_ptr<Film>(film), weak_ptr<Job>(job), weak_ptr<Content>(dcp))); - JobManager::instance()->add(job); - bool const ok = display_progress(variant::wx::dcpomatic_player(), _("Loading content")); - if (ok && report_errors_from_last_job(this)) { - Config::instance()->add_to_player_history(dir); - } - } catch (ProjectFolderError &) { - error_dialog( - this, - wxString::Format(_("Could not load a DCP from %s"), std_to_wx(dir.string())), - wxString::Format( - _("This looks like a %s project folder, which cannot be loaded into the player. " - "Choose the DCP folder inside the %s project folder if that's what you want to play."), - variant::wx::dcpomatic(), - variant::wx::dcpomatic() - ) - ); - } catch (dcp::ReadError& e) { - error_dialog(this, wxString::Format(_("Could not load a DCP from %s"), std_to_wx(dir.string())), std_to_wx(e.what())); - } catch (DCPError& e) { - error_dialog(this, wxString::Format(_("Could not load a DCP from %s"), std_to_wx(dir.string())), std_to_wx(e.what())); - } - } - - void reset_film_weak(weak_ptr<Film> weak_film, optional<float> crop_to_ratio) - { - if (auto film = weak_film.lock()) { - reset_film(film, crop_to_ratio); - } - } - - void reset_film(shared_ptr<Film> film = std::make_shared<Film>(boost::none), optional<float> crop_to_ratio = {}) - { - _film = film; - - if (!crop_to_ratio) { - crop_to_ratio = Config::instance()->player_crop_output_ratio(); - } - - prepare_to_play_film(crop_to_ratio); - } - - /* _film is now something new: set up to play it */ - void prepare_to_play_film(optional<float> crop_to_ratio) - { - if (_viewer.playing()) { - _viewer.stop(); - } - - /* Start off as Flat */ - auto auto_ratio = Ratio::from_id("185"); - - _film->set_audio_channels(MAX_DCP_AUDIO_CHANNELS); - - for (auto i: _film->content()) { - auto dcp = dynamic_pointer_cast<DCPContent>(i); - - copy_dcp_markers_to_film(dcp, _film); - - for (auto j: i->text) { - j->set_use(true); - } - - if (i->video && i->video->size()) { - auto const r = Ratio::nearest_from_ratio(i->video->size()->ratio()); - if (r.id() == "239") { - /* Any scope content means we use scope */ - auto_ratio = r; - } - } - - /* Any 3D content means we use 3D mode */ - if (i->video && i->video->frame_type() != VideoFrameType::TWO_D) { - _film->set_three_d(true); - } - - if (dcp->video_frame_rate()) { - _film->set_video_frame_rate(dcp->video_frame_rate().get()); - } - - switch (dcp->video_encoding().get_value_or(VideoEncoding::JPEG2000)) { - case VideoEncoding::JPEG2000: - _viewer.set_optimisation(Optimisation::JPEG2000); - break; - case VideoEncoding::MPEG2: - _viewer.set_optimisation(Optimisation::MPEG2); - break; - case VideoEncoding::COUNT: - DCPOMATIC_ASSERT(false); - } - } - - set_audio_delay_from_config(); - - auto old = _cpl_menu->GetMenuItems(); - for (auto const& i: old) { - _cpl_menu->Remove(i); - } - - if (_film->content().size() == 1) { - /* Offer a CPL menu */ - if (auto first = dynamic_pointer_cast<DCPContent>(_film->content().front())) { - int id = ID_view_cpl; - for (auto i: dcp::find_and_resolve_cpls(first->directories(), true)) { - auto j = _cpl_menu->AppendRadioItem( - id, - wxString::Format(char_to_wx("%s (%s)"), std_to_wx(i->content_title_text()).data(), std_to_wx(i->id()).data()) - ); - j->Check(!first->cpl() || i->id() == *first->cpl()); - ++id; - } - } - - if (crop_to_ratio) { - auto size = _film->content()[0]->video->size().get_value_or({1998, 1080}); - int pixels = 0; - if (*crop_to_ratio > (2048.0 / 1080.0)) { - pixels = (size.height - (size.width / *crop_to_ratio)) / 2; - _film->content()[0]->video->set_crop(Crop{0, 0, std::max(0, pixels), std::max(0, pixels)}); - } else { - pixels = (size.width - (size.height * *crop_to_ratio)) / 2; - _film->content()[0]->video->set_crop(Crop{std::max(0, pixels), std::max(0, pixels), 0, 0}); - } - } - } - - if (crop_to_ratio) { - _film->set_container(Ratio(*crop_to_ratio, "custom", "custom", {}, "custom")); - } else { - _film->set_container(auto_ratio); - } - - _viewer.set_film(_film); - _viewer.seek(DCPTime(), true); - _viewer.set_eyes(_view_eye_left->IsChecked() ? Eyes::LEFT : Eyes::RIGHT); - _info->triggered_update(); - set_menu_sensitivity(); - - _controls->set_film(_film); - } - - void set_audio_delay_from_config() - { - if (!_film) { - return; - } - - for (auto i: _film->content()) { - if (i->audio) { - i->audio->set_delay(Config::instance()->player_audio_delay()); - } - } - } - - void load_stress_script(boost::filesystem::path path) - { - _stress.load_script(path); - } - - void idle() - { - if (_http_server) { - struct timeval now; - gettimeofday(&now, 0); - auto time_since_last_update = (now.tv_sec + now.tv_usec / 1e6) - (_last_http_server_update.tv_sec + _last_http_server_update.tv_usec / 1e6); - if (time_since_last_update > 0.25) { - _http_server->set_playing(_viewer.playing()); - if (auto dcp = _viewer.dcp()) { - _http_server->set_dcp_name(dcp->name()); - } else { - _http_server->set_dcp_name(""); - } - _http_server->set_position(_viewer.position()); - _last_http_server_update = now; - } - } - } - -private: - - void examine_content() - { - DCPOMATIC_ASSERT(_film); - auto dcp = dynamic_pointer_cast<DCPContent>(_film->content().front()); - DCPOMATIC_ASSERT(dcp); - dcp->examine({}, true); - - /* Examining content re-creates the TextContent objects, so we must re-enable them */ - for (auto i: dcp->text) { - i->set_use(true); - } - } - - bool report_errors_from_last_job(wxWindow* parent) const - { - auto jm = JobManager::instance(); - - DCPOMATIC_ASSERT(!jm->get().empty()); - - auto last = jm->get().back(); - if (last->finished_in_error()) { - error_dialog(parent, wxString::Format(_("Could not load DCP.\n\n%s."), std_to_wx(last->error_summary()).data()), std_to_wx(last->error_details())); - return false; - } - - return true; - } - - void setup_menu(wxMenuBar* m) - { - _file_menu = new wxMenu; - _file_menu->Append(ID_file_open, _("&Open...\tCtrl-O")); - _file_add_ov = _file_menu->Append(ID_file_add_ov, _("&Add OV...")); - _file_add_kdm = _file_menu->Append(ID_file_add_kdm, _("Add &KDM...")); - _file_menu->AppendSeparator(); - _file_save_frame = _file_menu->Append(ID_file_save_frame, _("&Save frame to file...\tCtrl-S")); - - _history_position = _file_menu->GetMenuItems().GetCount(); - - _file_menu->AppendSeparator(); - _file_menu->Append(ID_file_close, _("&Close")); - _file_menu->AppendSeparator(); - -#ifdef __WXOSX__ - _file_menu->Append(wxID_EXIT, _("&Exit")); -#else - _file_menu->Append(wxID_EXIT, _("&Quit")); -#endif - -#ifdef __WXOSX__ - auto prefs = _file_menu->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-,")); -#else - auto edit = new wxMenu; - auto prefs = edit->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-P")); -#endif - - prefs->Enable(Config::instance()->have_write_permission()); - - _cpl_menu = new wxMenu; - - auto view = new wxMenu; - auto c = Config::instance()->decode_reduction(); - _view_cpl = view->Append(ID_view_cpl, _("CPL"), _cpl_menu); - view->AppendSeparator(); - _view_full_screen = view->AppendCheckItem(ID_view_full_screen, _("Full screen\tF11")); - _view_dual_screen = view->AppendCheckItem(ID_view_dual_screen, _("Dual screen\tShift+F11")); - setup_menu(); - view->AppendSeparator(); - view->Append(ID_view_closed_captions, _("Closed captions...")); - _view_eye_menu = new wxMenu; - _view_eye_left = _view_eye_menu->AppendRadioItem(ID_view_eye_left, _("Left")); - _view_eye_menu->AppendRadioItem(ID_view_eye_right, _("Right")); - _view_eye = view->Append(ID_view_eye, _("Eye"), _view_eye_menu); - view->AppendSeparator(); - view->AppendRadioItem(ID_view_scale_appropriate, _("Set decode resolution to match display"))->Check(!static_cast<bool>(c)); - view->AppendRadioItem(ID_view_scale_full, _("Decode at full resolution"))->Check(c && c.get() == 0); - view->AppendRadioItem(ID_view_scale_half, _("Decode at half resolution"))->Check(c && c.get() == 1); - view->AppendRadioItem(ID_view_scale_quarter, _("Decode at quarter resolution"))->Check(c && c.get() == 2); - - auto tools = new wxMenu; - _tools_verify = tools->Append(ID_tools_verify, _("Verify DCP...")); - _tools_audio_graph = tools->Append(ID_tools_audio_graph, _("Audio graph...")); - tools->AppendSeparator(); - tools->Append(ID_tools_check_for_updates, _("Check for updates")); - tools->Append(ID_tools_timing, _("Timing...")); - tools->Append(ID_tools_system_information, _("System information...")); - - auto help = new wxMenu; -#ifdef __WXOSX__ - help->Append(wxID_ABOUT, variant::wx::insert_dcpomatic_player(_("About %s"))); -#else - help->Append(wxID_ABOUT, _("About")); -#endif - if (variant::show_report_a_problem()) { - help->Append(ID_help_report_a_problem, _("Report a problem...")); - } - - m->Append (_file_menu, _("&File")); - if (!Config::instance()->player_restricted_menus()) { -#ifndef __WXOSX__ - m->Append(edit, _("&Edit")); -#endif - m->Append(view, _("&View")); - m->Append(tools, _("&Tools")); - m->Append(help, _("&Help")); - } - } - - void file_open() - { - auto d = wxStandardPaths::Get().GetDocumentsDir(); - if (Config::instance()->last_player_load_directory()) { - d = std_to_wx(Config::instance()->last_player_load_directory()->string()); - } - - wxDirDialog dialog(this, _("Select DCP to open"), d, wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST); - - int r; - while (true) { - r = dialog.ShowModal(); - if (r == wxID_OK && dialog.GetPath() == wxStandardPaths::Get().GetDocumentsDir()) { - error_dialog(this, _("You did not select a folder. Make sure that you select a folder before clicking Open.")); - } else { - break; - } - } - - if (r == wxID_OK) { - boost::filesystem::path const dcp(wx_to_std(dialog.GetPath())); - load_dcp(dcp); - Config::instance()->set_last_player_load_directory(dcp.parent_path()); - } - } - - void file_add_ov() - { - auto initial_dir = wxStandardPaths::Get().GetDocumentsDir(); - if (Config::instance()->last_player_load_directory()) { - initial_dir = std_to_wx(Config::instance()->last_player_load_directory()->string()); - } - - wxDirDialog dialog( - this, - _("Select DCP to open as OV"), - initial_dir, - wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST - ); - - int r; - while (true) { - r = dialog.ShowModal(); - if (r == wxID_OK && dialog.GetPath() == wxStandardPaths::Get().GetDocumentsDir()) { - error_dialog(this, _("You did not select a folder. Make sure that you select a folder before clicking Open.")); - } else { - break; - } - } - - if (r == wxID_OK) { - DCPOMATIC_ASSERT(_film); - DCPOMATIC_ASSERT(!_film->content().empty()); - auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front()); - DCPOMATIC_ASSERT(dcp); - - try { - dcp->add_ov(wx_to_std(dialog.GetPath())); - } catch (DCPError& e) { - error_dialog(this, char_to_wx(e.what())); - return; - } - - auto job = make_shared<ExamineContentJob>(vector<shared_ptr<Content>>{dcp}, true); - _examine_job_connection = job->Finished.connect(boost::bind(&DOMFrame::prepare_to_play_film, this, Config::instance()->player_crop_output_ratio())); - JobManager::instance()->add(job); - - display_progress(variant::wx::dcpomatic_player(), _("Loading content")); - report_errors_from_last_job(this); - } - } - - void file_add_kdm() - { - FileDialog dialog(this, _("Select KDM"), char_to_wx("XML files|*.xml|All files|*.*"), wxFD_MULTIPLE, "AddKDMPath"); - - if (dialog.show()) { - DCPOMATIC_ASSERT(_film); - auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front()); - DCPOMATIC_ASSERT(dcp); - try { - if (dcp) { - dcp::ScopeGuard sg([this]() { - _viewer.set_coalesce_player_changes(false); - }); - _viewer.set_coalesce_player_changes(true); - for (auto path: dialog.paths()) { - dcp->add_kdm(dcp::EncryptedKDM(dcp::file_to_string(path))); - _kdms.push_back(path); - } - examine_content(); - } - } catch (exception& e) { - error_dialog(this, wxString::Format(_("Could not load KDM.")), std_to_wx(e.what())); - return; - } - } - - _info->triggered_update(); - set_menu_sensitivity(); - } - - void file_save_frame() - { - wxFileDialog dialog(this, _("Save frame to file"), {}, {}, char_to_wx("PNG files (*.png)|*.png|JPEG files (*.jpg;*.jpeg)|*.jpg;*.jpeg"), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (dialog.ShowModal() == wxID_CANCEL) { - return; - } - - auto path = boost::filesystem::path(wx_to_std(dialog.GetPath())); - - auto player = make_shared<Player>(_film, Image::Alignment::PADDED, true); - player->seek(_viewer.position(), true); - - bool done = false; - player->Video.connect([path, &done, this](shared_ptr<PlayerVideo> video, DCPTime) { - auto ext = boost::algorithm::to_lower_copy(path.extension().string()); - if (ext == ".png") { - auto image = video->image(force(AV_PIX_FMT_RGBA), VideoRange::FULL, false); - image_as_png(image).write(path); - } else if (ext == ".jpg" || ext == ".jpeg") { - auto image = video->image(force(AV_PIX_FMT_RGB24), VideoRange::FULL, false); - image_as_jpeg(image, 80).write(path); - } else { - error_dialog(this, wxString::Format(_("Unrecognised file extension %s (use .jpg, .jpeg or .png)"), std_to_wx(ext))); - } - done = true; - }); - - int tries_left = 50; - while (!done && tries_left >= 0) { - player->pass(); - --tries_left; - } - - DCPOMATIC_ASSERT(tries_left >= 0); - } - - void file_history(wxCommandEvent& event) - { - auto history = Config::instance()->player_history(); - int n = event.GetId() - ID_file_history; - if (n >= 0 && n < static_cast<int>(history.size())) { - try { - load_dcp(history[n]); - } catch (exception& e) { - error_dialog(nullptr, wxString::Format(_("Could not load DCP %s."), std_to_wx(history[n].string()))), std_to_wx(e.what()); - } - } - } - - void file_close() - { - reset_film(); - _info->triggered_update(); - set_menu_sensitivity(); - } - - void file_exit() - { - Close(); - } - - void edit_preferences() - { - if (!Config::instance()->have_write_permission()) { - return; - } - - if (!_config_dialog) { - _config_dialog = create_player_config_dialog(); - } - _config_dialog->Show(this); - } - - void view_cpl(wxCommandEvent& ev) - { - auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front()); - DCPOMATIC_ASSERT(dcp); - auto cpls = dcp->cpls(); - int id = ev.GetId() - ID_view_cpl; - DCPOMATIC_ASSERT(id >= 0); - DCPOMATIC_ASSERT(id < int(cpls.size())); - auto i = cpls.begin(); - while (id > 0) { - ++i; - --id; - } - - _viewer.set_coalesce_player_changes(true); - dcp->set_cpl(*i); - examine_content(); - _viewer.set_coalesce_player_changes(false); - - _info->triggered_update(); - } - - void view_eye_changed(wxCommandEvent& ev) - { - _viewer.set_eyes(ev.GetId() == ID_view_eye_left ? Eyes::LEFT : Eyes::RIGHT); - } - - void view_full_screen() - { - if (_mode == Config::PlayerMode::FULL) { - _mode = Config::PlayerMode::WINDOW; - } else { - _mode = Config::PlayerMode::FULL; - } - setup_screen(); - setup_menu(); - } - - void view_dual_screen() - { - if (_mode == Config::PlayerMode::DUAL) { - _mode = Config::PlayerMode::WINDOW; - } else { - _mode = Config::PlayerMode::DUAL; - } - setup_screen(); - setup_menu(); - } - - void setup_menu() - { - if (_view_full_screen) { - _view_full_screen->Check(_mode == Config::PlayerMode::FULL); - } - if (_view_dual_screen) { - _view_dual_screen->Check(_mode == Config::PlayerMode::DUAL); - } - } - - void setup_screen() - { - _controls->Show(_mode != Config::PlayerMode::FULL); - _info->Show(_mode != Config::PlayerMode::FULL); - _overall_panel->SetBackgroundColour(_mode == Config::PlayerMode::FULL ? wxColour(0, 0, 0) : wxNullColour); - ShowFullScreen(_mode == Config::PlayerMode::FULL); - _viewer.set_pad_black(_mode != Config::PlayerMode::WINDOW); - - if (_mode == Config::PlayerMode::DUAL) { - _dual_screen = new wxFrame(this, wxID_ANY, {}); - _dual_screen->SetBackgroundColour(wxColour(0, 0, 0)); - _dual_screen->ShowFullScreen(true); - _viewer.panel()->Reparent(_dual_screen); - _viewer.panel()->SetFocus(); - _dual_screen->Show(); - LOG_DEBUG_PLAYER("Setting up dual screen mode with {} displays", wxDisplay::GetCount()); - for (auto index = 0U; index < wxDisplay::GetCount(); ++index) { - wxDisplay display(index); - auto client = display.GetClientArea(); - auto mode = display.GetCurrentMode(); - auto geometry = display.GetGeometry(); - LOG_DEBUG_PLAYER("Display {}", index); - LOG_DEBUG_PLAYER(" ClientArea position=({}, {}) size=({}, {})", client.GetX(), client.GetY(), client.GetWidth(), client.GetHeight()); - LOG_DEBUG_PLAYER(" Geometry position=({}, {}) size=({}, {})", geometry.GetX(), geometry.GetY(), geometry.GetWidth(), geometry.GetHeight()); - LOG_DEBUG_PLAYER(" Mode size=({}, {})", mode.GetWidth(), mode.GetHeight()); - LOG_DEBUG_PLAYER(" Primary? {}", static_cast<int>(display.IsPrimary())); - } - if (wxDisplay::GetCount() > 1) { - wxRect geometry[2] = { - wxDisplay(0U).GetGeometry(), - wxDisplay(1U).GetGeometry() - }; - auto const image_display = Config::instance()->image_display(); - _dual_screen->Move(geometry[image_display].GetX(), geometry[image_display].GetY()); - _viewer.panel()->SetSize(geometry[image_display].GetWidth(), geometry[image_display].GetHeight()); - Move(geometry[1 - image_display].GetX(), geometry[1 - image_display].GetY()); - } - _dual_screen->Bind(wxEVT_CHAR_HOOK, boost::bind(&DOMFrame::dual_screen_key_press, this, _1)); - } else { - if (_dual_screen) { - _viewer.panel()->Reparent(_overall_panel); - _dual_screen->Destroy(); - _dual_screen = 0; - } - } - - setup_main_sizer(_mode); - } - - void dual_screen_key_press(wxKeyEvent& ev) - { - if (ev.GetKeyCode() == WXK_F11) { - if (ev.ShiftDown()) { - view_dual_screen(); - } else if (!ev.HasAnyModifiers()) { - view_full_screen(); - } - } - } - - void view_closed_captions() - { - _viewer.show_closed_captions(); - } - - void tools_verify() - { - DCPOMATIC_ASSERT(!_film->content().empty()); - auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front()); - DCPOMATIC_ASSERT(dcp); - - VerifyDCPDialog dialog(this, _("Verify DCP"), dcp->directories(), _kdms); - dialog.ShowModal(); - } - - void tools_audio_graph() - { - DCPOMATIC_ASSERT(!_film->content().empty()); - auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front()); - DCPOMATIC_ASSERT(dcp); - - _audio_dialog.reset(this, _film, dcp); - _audio_dialog->Seek.connect(boost::bind(&FilmViewer::seek, &_viewer, _1, true)); - _audio_dialog->Show(); - } - - void tools_check_for_updates() - { - UpdateChecker::instance()->run(); - _update_news_requested = true; - } - - void tools_timing() - { - TimerDisplay dialog(this, _viewer.state_timer(), _viewer.gets()); - dialog.ShowModal(); - } - - void tools_system_information() - { - if (!_system_information_dialog) { - _system_information_dialog.reset(this, _viewer); - } - - _system_information_dialog->Show(); - } - - void help_about() - { - AboutDialog dialog(this); - dialog.ShowModal(); - } - - void help_report_a_problem() - { - ReportProblemDialog dialog(this); - if (dialog.ShowModal() == wxID_OK) { - dialog.report(); - } - } - - void update_checker_state_changed() - { - auto uc = UpdateChecker::instance(); - - bool const announce = - _update_news_requested || - (uc->stable() && Config::instance()->check_for_updates()) || - (uc->test() && Config::instance()->check_for_updates() && Config::instance()->check_for_test_updates()); - - _update_news_requested = false; - - if (!announce) { - return; - } - - if (uc->state() == UpdateChecker::State::YES) { - UpdateDialog dialog(this, uc->stable(), uc->test()); - dialog.ShowModal(); - } else if (uc->state() == UpdateChecker::State::FAILED) { - error_dialog(this, variant::wx::insert_dcpomatic(_("The %s download server could not be contacted."))); - } else { - error_dialog(this, variant::wx::insert_dcpomatic(_("There are no new versions of %s available."))); - } - - _update_news_requested = false; - } - - void config_changed(Config::Property prop) - { - /* Instantly save any config changes when using the player GUI */ - try { - Config::instance()->write_config(); - } catch (FileError& e) { - if (prop != Config::HISTORY) { - error_dialog( - this, - wxString::Format( - _("Could not write to config file at %s. Your changes have not been saved."), - std_to_wx(e.file().string()) - ) - ); - } - } catch (exception& e) { - error_dialog( - this, - _("Could not write to config file. Your changes have not been saved.") - ); - } - - update_from_config(prop); - - setup_http_server(); - } - - void stop_http_server() - { - if (_http_server) { - _http_server->stop(); - _http_server_thread.join(); - _http_server.reset(); - } - } - - void setup_http_server() - { - stop_http_server(); - - auto config = Config::instance(); - try { - if (config->enable_player_http_server()) { - _http_server.reset(new HTTPServer(config->player_http_server_port())); - _http_server->Play.connect(boost::bind(&FilmViewer::start, &_viewer)); - _http_server->Stop.connect(boost::bind(&FilmViewer::stop, &_viewer)); - _http_server_thread = boost::thread(boost::bind(&HTTPServer::run, _http_server.get())); - } - } catch (std::exception& e) { - LOG_DEBUG_PLAYER("Failed to start player HTTP server ({})", e.what()); - } - } - - void setup_internal_player_server() - { - try { - auto server = new InternalPlayerServer(); - server->LoadDCP.connect(boost::bind(&DOMFrame::load_dcp, this, _1)); - new thread(boost::bind(&InternalPlayerServer::run, server)); - } catch (std::exception& e) { - /* This is not the end of the world; probably a failure to bind the server socket - * because there's already another player running. - */ - LOG_DEBUG_PLAYER("Failed to start internal player server ({})", e.what()); - } - } - - void update_from_config(Config::Property prop) - { - for (int i = 0; i < _history_items; ++i) { - delete _file_menu->Remove(ID_file_history + i); - } - - if (_history_separator) { - _file_menu->Remove(_history_separator); - } - delete _history_separator; - _history_separator = nullptr; - - int pos = _history_position; - - /* Clear out non-existent history items before we re-build the menu */ - Config::instance()->clean_player_history(); - auto history = Config::instance()->player_history(); - - if (!history.empty()) { - _history_separator = _file_menu->InsertSeparator(pos++); - } - - for (size_t i = 0; i < history.size(); ++i) { - string s; - if (i < 9) { - s = fmt::format("&{} {}", i + 1, history[i].string()); - } else { - s = history[i].string(); - } - _file_menu->Insert(pos++, ID_file_history + i, std_to_wx(s)); - } - - _history_items = history.size(); - - if (prop == Config::PLAYER_DEBUG_LOG) { - auto p = Config::instance()->player_debug_log_file(); - if (p) { - dcpomatic_log = make_shared<FileLog>(*p); - } else { - dcpomatic_log = make_shared<NullLog>(); - } - } - - dcpomatic_log->set_types(Config::instance()->log_types()); - - set_audio_delay_from_config(); - } - - void set_menu_sensitivity() - { - auto const have_content = _film && !_film->content().empty(); - auto const dcp = _viewer.dcp(); - auto const playable = dcp && !dcp->needs_assets() && !dcp->needs_kdm(); - _tools_verify->Enable(have_content); - _tools_audio_graph->Enable(playable); - _file_add_ov->Enable(have_content); - _file_add_kdm->Enable(have_content); - _file_save_frame->Enable(playable); - _view_cpl->Enable(have_content); - _view_eye->Enable(have_content && _film->three_d()); - } - - void start_stop_pressed() - { - if (_viewer.playing()) { - _viewer.stop(); - } else { - _viewer.start(); - } - } - - void go_back_frame() - { - _viewer.seek_by(-_viewer.one_video_frame(), true); - } - - void go_forward_frame() - { - _viewer.seek_by(_viewer.one_video_frame(), true); - } - - void go_seconds(int s) - { - _viewer.seek_by(DCPTime::from_seconds(s), true); - } - - void go_to_start() - { - _viewer.seek(DCPTime(), true); - } - - void go_to_end() - { - _viewer.seek(_film->length() - _viewer.one_video_frame(), true); - } - - wxFrame* _dual_screen = nullptr; - bool _update_news_requested = false; - PlayerInformation* _info = nullptr; - Config::PlayerMode _mode; - wxPreferencesEditor* _config_dialog = nullptr; - wxPanel* _overall_panel = nullptr; - wxMenu* _file_menu = nullptr; - wxMenuItem* _view_cpl = nullptr; - wxMenu* _cpl_menu = nullptr; - wxMenuItem* _view_eye = nullptr; - wxMenuItem* _view_eye_left = nullptr; - wxMenu* _view_eye_menu = nullptr; - int _history_items = 0; - int _history_position = 0; - wxMenuItem* _history_separator = nullptr; - FilmViewer _viewer; - Controls* _controls; - wx_ptr<SystemInformationDialog> _system_information_dialog; - std::shared_ptr<Film> _film; - boost::signals2::scoped_connection _config_changed_connection; - boost::signals2::scoped_connection _examine_job_connection; - wxMenuItem* _file_add_ov = nullptr; - wxMenuItem* _file_add_kdm = nullptr; - wxMenuItem* _file_save_frame = nullptr; - wxMenuItem* _tools_verify = nullptr; - wxMenuItem* _tools_audio_graph = nullptr; - wxMenuItem* _view_full_screen = nullptr; - wxMenuItem* _view_dual_screen = nullptr; - wxSizer* _main_sizer = nullptr; - PlayerStressTester _stress; - /** KDMs that have been loaded, so that we can pass them to the verifier */ - std::vector<boost::filesystem::path> _kdms; - boost::thread _http_server_thread; - std::unique_ptr<HTTPServer> _http_server; - struct timeval _last_http_server_update = { 0, 0 }; - wx_ptr<AudioDialog> _audio_dialog; -}; - static const wxCmdLineEntryDesc command_line_description[] = { { wxCMD_LINE_PARAM, 0, 0, "DCP to load or create", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, { wxCMD_LINE_OPTION, "c", "config", "Directory containing config.xml", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, @@ -1390,7 +144,7 @@ private: signal_manager = new wxSignalManager(this); - _frame = new DOMFrame(); + _frame = new dcpomatic::ui::PlayerFrame(); SetTopWindow(_frame); _frame->Maximize(); if (splash) { @@ -1525,7 +279,7 @@ private: message_dialog(_frame, std_to_wx(m)); } - DOMFrame* _frame = nullptr; + dcpomatic::ui::PlayerFrame* _frame = nullptr; boost::optional<boost::filesystem::path> _dcp_to_load; boost::optional<string> _stress; }; diff --git a/src/wx/player_frame.cc b/src/wx/player_frame.cc new file mode 100644 index 000000000..f88a579a5 --- /dev/null +++ b/src/wx/player_frame.cc @@ -0,0 +1,1335 @@ +/* + Copyright (C) 2017-2026 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "about_dialog.h" +#include "id.h" +#include "file_dialog.h" +#include "nag_dialog.h" +#include "standard_controls.h" +#include "player_config_dialog.h" +#include "player_information.h" +#include "player_frame.h" +#include "playlist_controls.h" +#include "report_problem_dialog.h" +#include "timer_display.h" +#include "update_dialog.h" +#include "verify_dcp_dialog.h" +#include "wx_util.h" +#include "wx_variant.h" +#include "lib/audio_content.h" +#include "lib/copy_dcp_details_to_film.h" +#include "lib/dcp_content.h" +#include "lib/dcpomatic_log.h" +#include "lib/examine_content_job.h" +#include "lib/image.h" +#include "lib/image_jpeg.h" +#include "lib/image_png.h" +#include "lib/internal_player_server.h" +#include "lib/file_log.h" +#include "lib/film.h" +#include "lib/font_config.h" +#include "lib/job_manager.h" +#include "lib/null_log.h" +#include "lib/text_content.h" +#include "lib/update_checker.h" +#include "lib/variant.h" +#include "lib/video_content.h" +#include <dcp/cpl.h> +#include <dcp/scope_guard.h> +#include <dcp/search.h> +#include <dcp/warnings.h> +LIBDCP_DISABLE_WARNINGS +#include <wx/display.h> +#include <wx/preferences.h> +#include <wx/stdpaths.h> +LIBDCP_ENABLE_WARNINGS + + +#define MAX_CPLS 32 + + +using std::dynamic_pointer_cast; +using std::exception; +using std::make_shared; +using std::shared_ptr; +using std::string; +using std::weak_ptr; +using std::vector; +using boost::bind; +using boost::optional; +using boost::thread; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif +using namespace dcpomatic::ui; + + +enum { + ID_file_open = DCPOMATIC_MAIN_MENU, + ID_file_add_ov, + ID_file_add_kdm, + ID_file_save_frame, + ID_file_history, + /* Allow spare IDs after _history for the recent files list */ + ID_file_close = DCPOMATIC_MAIN_MENU + 100, + ID_view_cpl, + /* Allow spare IDs for CPLs */ + ID_view_full_screen = DCPOMATIC_MAIN_MENU + 200, + ID_view_dual_screen, + ID_view_closed_captions, + ID_view_eye, + ID_view_eye_left, + ID_view_eye_right, + ID_view_scale_appropriate, + ID_view_scale_full, + ID_view_scale_half, + ID_view_scale_quarter, + ID_help_report_a_problem, + ID_tools_verify, + ID_tools_audio_graph, + ID_tools_check_for_updates, + ID_tools_timing, + ID_tools_system_information, + /* IDs for shortcuts (with no associated menu item) */ + ID_start_stop, + ID_go_back_frame, + ID_go_forward_frame, + ID_go_back_small_amount, + ID_go_forward_small_amount, + ID_go_back_medium_amount, + ID_go_forward_medium_amount, + ID_go_back_large_amount, + ID_go_forward_large_amount, + ID_go_to_start, + ID_go_to_end +}; + + +PlayerFrame::DCPDropTarget::DCPDropTarget(PlayerFrame* owner) + : _frame(owner) +{ + +} + + +bool +PlayerFrame::DCPDropTarget::OnDropFiles(wxCoord, wxCoord, wxArrayString const& filenames) +{ + if (filenames.GetCount() == 1) { + /* Try to load a directory */ + auto path = boost::filesystem::path(wx_to_std(filenames[0])); + if (dcp::filesystem::is_directory(path)) { + _frame->load_dcp(wx_to_std(filenames[0])); + return true; + } + } + + if (filenames.GetCount() >= 1) { + /* Try to load the parent if we drop some files, one if which is an asset map */ + for (size_t i = 0; i < filenames.GetCount(); ++i) { + auto path = boost::filesystem::path(wx_to_std(filenames[i])); + if (path.filename() == "ASSETMAP" || path.filename() == "ASSETMAP.xml") { + _frame->load_dcp(path.parent_path()); + return true; + } + } + } + + return false; +} + + + +PlayerFrame::PlayerFrame() + : wxFrame(nullptr, -1, variant::wx::dcpomatic_player()) + , _mode(Config::instance()->player_mode()) + /* Use a panel as the only child of the Frame so that we avoid + the dark-grey background on Windows. + */ + , _overall_panel(new wxPanel(this, wxID_ANY)) + , _viewer(_overall_panel, true) + , _main_sizer(new wxBoxSizer(wxVERTICAL)) +{ + dcpomatic_log = make_shared<NullLog>(); + +#if defined(DCPOMATIC_WINDOWS) + maybe_open_console(); + cout << variant::dcpomatic_player() << " is starting." << "\n"; +#endif + + auto bar = new wxMenuBar; + setup_menu(bar); + set_menu_sensitivity(); + SetMenuBar(bar); + +#ifdef DCPOMATIC_WINDOWS + SetIcon(wxIcon(std_to_wx("id"))); +#endif + + _config_changed_connection = Config::instance()->Changed.connect(boost::bind(&PlayerFrame::config_changed, this, _1)); + update_from_config(Config::PLAYER_DEBUG_LOG); + + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::file_open, this), ID_file_open); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::file_add_ov, this), ID_file_add_ov); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::file_add_kdm, this), ID_file_add_kdm); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::file_save_frame, this), ID_file_save_frame); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::file_history, this, _1), ID_file_history, ID_file_history + HISTORY_SIZE); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::file_close, this), ID_file_close); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::file_exit, this), wxID_EXIT); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::edit_preferences, this), wxID_PREFERENCES); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::view_full_screen, this), ID_view_full_screen); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::view_dual_screen, this), ID_view_dual_screen); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::view_closed_captions, this), ID_view_closed_captions); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::view_cpl, this, _1), ID_view_cpl, ID_view_cpl + MAX_CPLS); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::view_eye_changed, this, _1), ID_view_eye_left); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::view_eye_changed, this, _1), ID_view_eye_right); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::set_decode_reduction, this, optional<int>(0)), ID_view_scale_full); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::set_decode_reduction, this, optional<int>(1)), ID_view_scale_half); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::set_decode_reduction, this, optional<int>(2)), ID_view_scale_quarter); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::help_about, this), wxID_ABOUT); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::help_report_a_problem, this), ID_help_report_a_problem); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::tools_verify, this), ID_tools_verify); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::tools_audio_graph, this), ID_tools_audio_graph); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::tools_check_for_updates, this), ID_tools_check_for_updates); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::tools_timing, this), ID_tools_timing); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::tools_system_information, this), ID_tools_system_information); + + Bind(wxEVT_CLOSE_WINDOW, boost::bind(&PlayerFrame::close, this, _1)); + + if (Config::instance()->enable_player_http_server()) { + update_content_store(); + } + + if (Config::instance()->player_mode() == Config::PlayerMode::DUAL) { + auto pc = new PlaylistControls(_overall_panel, _viewer); + _controls = pc; + pc->ResetFilm.connect(bind(&PlayerFrame::reset_film_weak, this, _1, _2)); + } else { + _controls = new StandardControls(_overall_panel, _viewer, false); + } + _controls->set_film(_viewer.film()); + _viewer.set_dcp_decode_reduction(Config::instance()->decode_reduction()); + _viewer.PlaybackPermitted.connect(bind(&PlayerFrame::playback_permitted, this)); + _viewer.TooManyDropped.connect(bind(&PlayerFrame::too_many_frames_dropped, this)); + _info = new PlayerInformation(_overall_panel, _viewer); + setup_main_sizer(Config::instance()->player_mode()); +#ifdef __WXOSX__ + int accelerators = 12; +#else + int accelerators = 11; +#endif + + _stress.setup(this, _controls); + + std::vector<wxAcceleratorEntry> accel(accelerators); + accel[0].Set(wxACCEL_NORMAL, WXK_SPACE, ID_start_stop); + accel[1].Set(wxACCEL_NORMAL, WXK_LEFT, ID_go_back_frame); + accel[2].Set(wxACCEL_NORMAL, WXK_RIGHT, ID_go_forward_frame); + accel[3].Set(wxACCEL_SHIFT, WXK_LEFT, ID_go_back_small_amount); + accel[4].Set(wxACCEL_SHIFT, WXK_RIGHT, ID_go_forward_small_amount); + accel[5].Set(wxACCEL_CTRL, WXK_LEFT, ID_go_back_medium_amount); + accel[6].Set(wxACCEL_CTRL, WXK_RIGHT, ID_go_forward_medium_amount); + accel[7].Set(wxACCEL_SHIFT | wxACCEL_CTRL, WXK_LEFT, ID_go_back_large_amount); + accel[8].Set(wxACCEL_SHIFT | wxACCEL_CTRL, WXK_RIGHT, ID_go_forward_large_amount); + accel[9].Set(wxACCEL_NORMAL, WXK_HOME, ID_go_to_start); + accel[10].Set(wxACCEL_NORMAL, WXK_END, ID_go_to_end); +#ifdef __WXOSX__ + accel[11].Set(wxACCEL_CTRL, static_cast<int>('W'), ID_file_close); +#endif + wxAcceleratorTable accel_table(accelerators, accel.data()); + SetAcceleratorTable(accel_table); + + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::start_stop_pressed, this), ID_start_stop); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_back_frame, this), ID_go_back_frame); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_forward_frame, this), ID_go_forward_frame); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_seconds, this, -60), ID_go_back_small_amount); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_seconds, this, 60), ID_go_forward_small_amount); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_seconds, this, -600), ID_go_back_medium_amount); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_seconds, this, 600), ID_go_forward_medium_amount); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_seconds, this, -3600), ID_go_back_large_amount); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_seconds, this, 3600), ID_go_forward_large_amount); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_to_start, this), ID_go_to_start); + Bind(wxEVT_MENU, boost::bind(&PlayerFrame::go_to_end, this), ID_go_to_end); + + reset_film(); + + UpdateChecker::instance()->StateChanged.connect(boost::bind(&PlayerFrame::update_checker_state_changed, this)); + setup_screen(); + + _stress.LoadDCP.connect(boost::bind(&PlayerFrame::load_dcp, this, _1)); + + setup_internal_player_server(); + setup_http_server(); + + SetDropTarget(new DCPDropTarget(this)); +} + + +PlayerFrame::~PlayerFrame() +{ + try { + stop_http_server(); + /* It's important that this is stopped before our frame starts destroying its children, + * otherwise UI elements that it depends on will disappear from under it. + */ + _viewer.stop(); + } catch (std::exception& e) { + LOG_ERROR("Destructor threw {}", e.what()); + } catch (...) { + LOG_ERROR("Destructor threw"); + } +} + + +void +PlayerFrame::close(wxCloseEvent& ev) +{ + FontConfig::drop(); + ev.Skip(); +} + + +void +PlayerFrame::setup_main_sizer(Config::PlayerMode mode) +{ + _main_sizer->Detach(_viewer.panel()); + _main_sizer->Detach(_controls); + _main_sizer->Detach(_info); + if (mode != Config::PlayerMode::DUAL) { + _main_sizer->Add(_viewer.panel(), 1, wxEXPAND); + } + _main_sizer->Add(_controls, mode == Config::PlayerMode::DUAL ? 1 : 0, wxEXPAND | wxALL, 6); + _main_sizer->Add(_info, 0, wxEXPAND | wxALL, 6); + _overall_panel->SetSizer(_main_sizer); + _overall_panel->Layout(); +} + + +bool +PlayerFrame::playback_permitted() +{ + if (!_film || !Config::instance()->respect_kdm_validity_periods()) { + return true; + } + + bool ok = true; + for (auto i: _film->content()) { + auto d = dynamic_pointer_cast<DCPContent>(i); + if (d && !d->kdm_timing_window_valid()) { + ok = false; + } + } + + if (!ok) { + error_dialog(this, _("The KDM does not allow playback of this content at this time.")); + } + + return ok; +} + + +void +PlayerFrame::too_many_frames_dropped() +{ + if (!Config::instance()->nagged(Config::NAG_TOO_MANY_DROPPED_FRAMES)) { + _viewer.stop(); + } + + NagDialog::maybe_nag( + this, + Config::NAG_TOO_MANY_DROPPED_FRAMES, + wxGetTranslation( + wxString::FromUTF8( + "The player is dropping a lot of frames, so playback may not be accurate.\n\n" + "<b>This does not necessarily mean that the DCP you are playing is defective!</b>\n\n" + "You may be able to improve player performance by:\n" + "• choosing 'decode at half resolution' or 'decode at quarter resolution' from the View menu\n" + "• using a more powerful computer.\n" + ) + ) + ); +} + + +void +PlayerFrame::set_decode_reduction(optional<int> reduction) +{ + _viewer.set_dcp_decode_reduction(reduction); + _info->triggered_update(); + Config::instance()->set_decode_reduction(reduction); +} + + +void +PlayerFrame::load_dcp(boost::filesystem::path dir) +{ + DCPOMATIC_ASSERT(_film); + + auto film = std::make_shared<Film>(optional<boost::filesystem::path>()); + + try { + _stress.set_suspended(true); + + /* Handler to set things up once the DCP has been examined */ + auto setup = [this](weak_ptr<Film> weak_film, weak_ptr<Job> weak_job, weak_ptr<Content> weak_content) + { + auto job = weak_job.lock(); + if (!job || !job->finished_ok()) { + return; + } + + auto content = weak_content.lock(); + if (!content) { + return; + } + + auto film = weak_film.lock(); + if (!film) { + return; + } + + film->add_content({content}); + _stress.set_suspended(false); + reset_film(film); + }; + + auto dcp = make_shared<DCPContent>(dir); + auto job = make_shared<ExamineContentJob>(vector<shared_ptr<Content>>{dcp}, true); + _examine_job_connection = job->Finished.connect(boost::bind<void>(setup, weak_ptr<Film>(film), weak_ptr<Job>(job), weak_ptr<Content>(dcp))); + JobManager::instance()->add(job); + bool const ok = display_progress(variant::wx::dcpomatic_player(), _("Loading content")); + if (ok && report_errors_from_last_job(this)) { + Config::instance()->add_to_player_history(dir); + } + } catch (ProjectFolderError &) { + error_dialog( + this, + wxString::Format(_("Could not load a DCP from %s"), std_to_wx(dir.string())), + wxString::Format( + _("This looks like a %s project folder, which cannot be loaded into the player. " + "Choose the DCP folder inside the %s project folder if that's what you want to play."), + variant::wx::dcpomatic(), + variant::wx::dcpomatic() + ) + ); + } catch (dcp::ReadError& e) { + error_dialog(this, wxString::Format(_("Could not load a DCP from %s"), std_to_wx(dir.string())), std_to_wx(e.what())); + } catch (DCPError& e) { + error_dialog(this, wxString::Format(_("Could not load a DCP from %s"), std_to_wx(dir.string())), std_to_wx(e.what())); + } +} + + +void +PlayerFrame::reset_film_weak(weak_ptr<Film> weak_film, optional<float> crop_to_ratio) +{ + if (auto film = weak_film.lock()) { + reset_film(film, crop_to_ratio); + } +} + + +void +PlayerFrame::reset_film(shared_ptr<Film> film, optional<float> crop_to_ratio) +{ + _film = film; + + if (!crop_to_ratio) { + crop_to_ratio = Config::instance()->player_crop_output_ratio(); + } + + prepare_to_play_film(crop_to_ratio); +} + + +/* _film is now something new: set up to play it */ +void +PlayerFrame::prepare_to_play_film(optional<float> crop_to_ratio) +{ + if (_viewer.playing()) { + _viewer.stop(); + } + + /* Start off as Flat */ + auto auto_ratio = Ratio::from_id("185"); + + _film->set_audio_channels(MAX_DCP_AUDIO_CHANNELS); + + for (auto i: _film->content()) { + auto dcp = dynamic_pointer_cast<DCPContent>(i); + + copy_dcp_markers_to_film(dcp, _film); + + for (auto j: i->text) { + j->set_use(true); + } + + if (i->video && i->video->size()) { + auto const r = Ratio::nearest_from_ratio(i->video->size()->ratio()); + if (r.id() == "239") { + /* Any scope content means we use scope */ + auto_ratio = r; + } + } + + /* Any 3D content means we use 3D mode */ + if (i->video && i->video->frame_type() != VideoFrameType::TWO_D) { + _film->set_three_d(true); + } + + if (dcp->video_frame_rate()) { + _film->set_video_frame_rate(dcp->video_frame_rate().get()); + } + + switch (dcp->video_encoding().get_value_or(VideoEncoding::JPEG2000)) { + case VideoEncoding::JPEG2000: + _viewer.set_optimisation(Optimisation::JPEG2000); + break; + case VideoEncoding::MPEG2: + _viewer.set_optimisation(Optimisation::MPEG2); + break; + case VideoEncoding::COUNT: + DCPOMATIC_ASSERT(false); + } + } + + set_audio_delay_from_config(); + + auto old = _cpl_menu->GetMenuItems(); + for (auto const& i: old) { + _cpl_menu->Remove(i); + } + + if (_film->content().size() == 1) { + /* Offer a CPL menu */ + if (auto first = dynamic_pointer_cast<DCPContent>(_film->content().front())) { + int id = ID_view_cpl; + for (auto i: dcp::find_and_resolve_cpls(first->directories(), true)) { + auto j = _cpl_menu->AppendRadioItem( + id, + wxString::Format(char_to_wx("%s (%s)"), std_to_wx(i->content_title_text()).data(), std_to_wx(i->id()).data()) + ); + j->Check(!first->cpl() || i->id() == *first->cpl()); + ++id; + } + } + + if (crop_to_ratio) { + auto size = _film->content()[0]->video->size().get_value_or({1998, 1080}); + int pixels = 0; + if (*crop_to_ratio > (2048.0 / 1080.0)) { + pixels = (size.height - (size.width / *crop_to_ratio)) / 2; + _film->content()[0]->video->set_crop(Crop{0, 0, std::max(0, pixels), std::max(0, pixels)}); + } else { + pixels = (size.width - (size.height * *crop_to_ratio)) / 2; + _film->content()[0]->video->set_crop(Crop{std::max(0, pixels), std::max(0, pixels), 0, 0}); + } + } + } + + if (crop_to_ratio) { + _film->set_container(Ratio(*crop_to_ratio, "custom", "custom", {}, "custom")); + } else { + _film->set_container(auto_ratio); + } + + _viewer.set_film(_film); + _viewer.seek(DCPTime(), true); + _viewer.set_eyes(_view_eye_left->IsChecked() ? Eyes::LEFT : Eyes::RIGHT); + _info->triggered_update(); + set_menu_sensitivity(); + + _controls->set_film(_film); +} + + +void +PlayerFrame::set_audio_delay_from_config() +{ + if (!_film) { + return; + } + + for (auto i: _film->content()) { + if (i->audio) { + i->audio->set_delay(Config::instance()->player_audio_delay()); + } + } +} + + +void +PlayerFrame::load_stress_script(boost::filesystem::path path) +{ + _stress.load_script(path); +} + + +void +PlayerFrame::idle() +{ + if (_http_server) { + struct timeval now; + gettimeofday(&now, 0); + auto time_since_last_update = (now.tv_sec + now.tv_usec / 1e6) - (_last_http_server_update.tv_sec + _last_http_server_update.tv_usec / 1e6); + if (time_since_last_update > 0.25) { + _http_server->set_playing(_viewer.playing()); + if (auto dcp = _viewer.dcp()) { + _http_server->set_dcp_name(dcp->name()); + } else { + _http_server->set_dcp_name(""); + } + _http_server->set_position(_viewer.position()); + _last_http_server_update = now; + } + } +} + + +void +PlayerFrame::examine_content() +{ + DCPOMATIC_ASSERT(_film); + auto dcp = dynamic_pointer_cast<DCPContent>(_film->content().front()); + DCPOMATIC_ASSERT(dcp); + dcp->examine({}, true); + + /* Examining content re-creates the TextContent objects, so we must re-enable them */ + for (auto i: dcp->text) { + i->set_use(true); + } +} + + +bool +PlayerFrame::report_errors_from_last_job(wxWindow* parent) const +{ + auto jm = JobManager::instance(); + + DCPOMATIC_ASSERT(!jm->get().empty()); + + auto last = jm->get().back(); + if (last->finished_in_error()) { + error_dialog(parent, wxString::Format(_("Could not load DCP.\n\n%s."), std_to_wx(last->error_summary()).data()), std_to_wx(last->error_details())); + return false; + } + + return true; +} + + +void +PlayerFrame::setup_menu(wxMenuBar* m) +{ + _file_menu = new wxMenu; + _file_menu->Append(ID_file_open, _("&Open...\tCtrl-O")); + _file_add_ov = _file_menu->Append(ID_file_add_ov, _("&Add OV...")); + _file_add_kdm = _file_menu->Append(ID_file_add_kdm, _("Add &KDM...")); + _file_menu->AppendSeparator(); + _file_save_frame = _file_menu->Append(ID_file_save_frame, _("&Save frame to file...\tCtrl-S")); + + _history_position = _file_menu->GetMenuItems().GetCount(); + + _file_menu->AppendSeparator(); + _file_menu->Append(ID_file_close, _("&Close")); + _file_menu->AppendSeparator(); + +#ifdef __WXOSX__ + _file_menu->Append(wxID_EXIT, _("&Exit")); +#else + _file_menu->Append(wxID_EXIT, _("&Quit")); +#endif + +#ifdef __WXOSX__ + auto prefs = _file_menu->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-,")); +#else + auto edit = new wxMenu; + auto prefs = edit->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-P")); +#endif + + prefs->Enable(Config::instance()->have_write_permission()); + + _cpl_menu = new wxMenu; + + auto view = new wxMenu; + auto c = Config::instance()->decode_reduction(); + _view_cpl = view->Append(ID_view_cpl, _("CPL"), _cpl_menu); + view->AppendSeparator(); + _view_full_screen = view->AppendCheckItem(ID_view_full_screen, _("Full screen\tF11")); + _view_dual_screen = view->AppendCheckItem(ID_view_dual_screen, _("Dual screen\tShift+F11")); + setup_menu(); + view->AppendSeparator(); + view->Append(ID_view_closed_captions, _("Closed captions...")); + _view_eye_menu = new wxMenu; + _view_eye_left = _view_eye_menu->AppendRadioItem(ID_view_eye_left, _("Left")); + _view_eye_menu->AppendRadioItem(ID_view_eye_right, _("Right")); + _view_eye = view->Append(ID_view_eye, _("Eye"), _view_eye_menu); + view->AppendSeparator(); + view->AppendRadioItem(ID_view_scale_appropriate, _("Set decode resolution to match display"))->Check(!static_cast<bool>(c)); + view->AppendRadioItem(ID_view_scale_full, _("Decode at full resolution"))->Check(c && c.get() == 0); + view->AppendRadioItem(ID_view_scale_half, _("Decode at half resolution"))->Check(c && c.get() == 1); + view->AppendRadioItem(ID_view_scale_quarter, _("Decode at quarter resolution"))->Check(c && c.get() == 2); + + auto tools = new wxMenu; + _tools_verify = tools->Append(ID_tools_verify, _("Verify DCP...")); + _tools_audio_graph = tools->Append(ID_tools_audio_graph, _("Audio graph...")); + tools->AppendSeparator(); + tools->Append(ID_tools_check_for_updates, _("Check for updates")); + tools->Append(ID_tools_timing, _("Timing...")); + tools->Append(ID_tools_system_information, _("System information...")); + + auto help = new wxMenu; +#ifdef __WXOSX__ + help->Append(wxID_ABOUT, variant::wx::insert_dcpomatic_player(_("About %s"))); +#else + help->Append(wxID_ABOUT, _("About")); +#endif + if (variant::show_report_a_problem()) { + help->Append(ID_help_report_a_problem, _("Report a problem...")); + } + + m->Append (_file_menu, _("&File")); + if (!Config::instance()->player_restricted_menus()) { +#ifndef __WXOSX__ + m->Append(edit, _("&Edit")); +#endif + m->Append(view, _("&View")); + m->Append(tools, _("&Tools")); + m->Append(help, _("&Help")); + } +} + + +void +PlayerFrame::file_open() +{ + auto d = wxStandardPaths::Get().GetDocumentsDir(); + if (Config::instance()->last_player_load_directory()) { + d = std_to_wx(Config::instance()->last_player_load_directory()->string()); + } + + wxDirDialog dialog(this, _("Select DCP to open"), d, wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST); + + int r; + while (true) { + r = dialog.ShowModal(); + if (r == wxID_OK && dialog.GetPath() == wxStandardPaths::Get().GetDocumentsDir()) { + error_dialog(this, _("You did not select a folder. Make sure that you select a folder before clicking Open.")); + } else { + break; + } + } + + if (r == wxID_OK) { + boost::filesystem::path const dcp(wx_to_std(dialog.GetPath())); + load_dcp(dcp); + Config::instance()->set_last_player_load_directory(dcp.parent_path()); + } +} + + +void +PlayerFrame::file_add_ov() +{ + auto initial_dir = wxStandardPaths::Get().GetDocumentsDir(); + if (Config::instance()->last_player_load_directory()) { + initial_dir = std_to_wx(Config::instance()->last_player_load_directory()->string()); + } + + wxDirDialog dialog( + this, + _("Select DCP to open as OV"), + initial_dir, + wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST + ); + + int r; + while (true) { + r = dialog.ShowModal(); + if (r == wxID_OK && dialog.GetPath() == wxStandardPaths::Get().GetDocumentsDir()) { + error_dialog(this, _("You did not select a folder. Make sure that you select a folder before clicking Open.")); + } else { + break; + } + } + + if (r == wxID_OK) { + DCPOMATIC_ASSERT(_film); + DCPOMATIC_ASSERT(!_film->content().empty()); + auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front()); + DCPOMATIC_ASSERT(dcp); + + try { + dcp->add_ov(wx_to_std(dialog.GetPath())); + } catch (DCPError& e) { + error_dialog(this, char_to_wx(e.what())); + return; + } + + auto job = make_shared<ExamineContentJob>(vector<shared_ptr<Content>>{dcp}, true); + _examine_job_connection = job->Finished.connect(boost::bind(&PlayerFrame::prepare_to_play_film, this, Config::instance()->player_crop_output_ratio())); + JobManager::instance()->add(job); + + display_progress(variant::wx::dcpomatic_player(), _("Loading content")); + report_errors_from_last_job(this); + } +} + + +void +PlayerFrame::file_add_kdm() +{ + FileDialog dialog(this, _("Select KDM"), char_to_wx("XML files|*.xml|All files|*.*"), wxFD_MULTIPLE, "AddKDMPath"); + + if (dialog.show()) { + DCPOMATIC_ASSERT(_film); + auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front()); + DCPOMATIC_ASSERT(dcp); + try { + if (dcp) { + dcp::ScopeGuard sg([this]() { + _viewer.set_coalesce_player_changes(false); + }); + _viewer.set_coalesce_player_changes(true); + for (auto path: dialog.paths()) { + dcp->add_kdm(dcp::EncryptedKDM(dcp::file_to_string(path))); + _kdms.push_back(path); + } + examine_content(); + } + } catch (exception& e) { + error_dialog(this, wxString::Format(_("Could not load KDM.")), std_to_wx(e.what())); + return; + } + } + + _info->triggered_update(); + set_menu_sensitivity(); +} + + +void +PlayerFrame::file_save_frame() +{ + wxFileDialog dialog(this, _("Save frame to file"), {}, {}, char_to_wx("PNG files (*.png)|*.png|JPEG files (*.jpg;*.jpeg)|*.jpg;*.jpeg"), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if (dialog.ShowModal() == wxID_CANCEL) { + return; + } + + auto path = boost::filesystem::path(wx_to_std(dialog.GetPath())); + + auto player = make_shared<Player>(_film, Image::Alignment::PADDED, true); + player->seek(_viewer.position(), true); + + bool done = false; + player->Video.connect([path, &done, this](shared_ptr<PlayerVideo> video, DCPTime) { + auto ext = boost::algorithm::to_lower_copy(path.extension().string()); + if (ext == ".png") { + auto image = video->image(force(AV_PIX_FMT_RGBA), VideoRange::FULL, false); + image_as_png(image).write(path); + } else if (ext == ".jpg" || ext == ".jpeg") { + auto image = video->image(force(AV_PIX_FMT_RGB24), VideoRange::FULL, false); + image_as_jpeg(image, 80).write(path); + } else { + error_dialog(this, wxString::Format(_("Unrecognised file extension %s (use .jpg, .jpeg or .png)"), std_to_wx(ext))); + } + done = true; + }); + + int tries_left = 50; + while (!done && tries_left >= 0) { + player->pass(); + --tries_left; + } + + DCPOMATIC_ASSERT(tries_left >= 0); +} + + +void +PlayerFrame::file_history(wxCommandEvent& event) +{ + auto history = Config::instance()->player_history(); + int n = event.GetId() - ID_file_history; + if (n >= 0 && n < static_cast<int>(history.size())) { + try { + load_dcp(history[n]); + } catch (exception& e) { + error_dialog(nullptr, wxString::Format(_("Could not load DCP %s."), std_to_wx(history[n].string()))), std_to_wx(e.what()); + } + } +} + + +void +PlayerFrame::file_close() +{ + reset_film(); + _info->triggered_update(); + set_menu_sensitivity(); +} + + +void +PlayerFrame::file_exit() +{ + Close(); +} + + +void +PlayerFrame::edit_preferences() +{ + if (!Config::instance()->have_write_permission()) { + return; + } + + if (!_config_dialog) { + _config_dialog = create_player_config_dialog(); + } + _config_dialog->Show(this); +} + + +void +PlayerFrame::view_cpl(wxCommandEvent& ev) +{ + auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front()); + DCPOMATIC_ASSERT(dcp); + auto cpls = dcp->cpls(); + int id = ev.GetId() - ID_view_cpl; + DCPOMATIC_ASSERT(id >= 0); + DCPOMATIC_ASSERT(id < int(cpls.size())); + auto i = cpls.begin(); + while (id > 0) { + ++i; + --id; + } + + _viewer.set_coalesce_player_changes(true); + dcp->set_cpl(*i); + examine_content(); + _viewer.set_coalesce_player_changes(false); + + _info->triggered_update(); +} + + +void +PlayerFrame::view_eye_changed(wxCommandEvent& ev) +{ + _viewer.set_eyes(ev.GetId() == ID_view_eye_left ? Eyes::LEFT : Eyes::RIGHT); +} + + +void +PlayerFrame::view_full_screen() +{ + if (_mode == Config::PlayerMode::FULL) { + _mode = Config::PlayerMode::WINDOW; + } else { + _mode = Config::PlayerMode::FULL; + } + setup_screen(); + setup_menu(); +} + + +void +PlayerFrame::view_dual_screen() +{ + if (_mode == Config::PlayerMode::DUAL) { + _mode = Config::PlayerMode::WINDOW; + } else { + _mode = Config::PlayerMode::DUAL; + } + setup_screen(); + setup_menu(); +} + + +void +PlayerFrame::setup_menu() +{ + if (_view_full_screen) { + _view_full_screen->Check(_mode == Config::PlayerMode::FULL); + } + if (_view_dual_screen) { + _view_dual_screen->Check(_mode == Config::PlayerMode::DUAL); + } +} + + +void +PlayerFrame::setup_screen() +{ + _controls->Show(_mode != Config::PlayerMode::FULL); + _info->Show(_mode != Config::PlayerMode::FULL); + _overall_panel->SetBackgroundColour(_mode == Config::PlayerMode::FULL ? wxColour(0, 0, 0) : wxNullColour); + ShowFullScreen(_mode == Config::PlayerMode::FULL); + _viewer.set_pad_black(_mode != Config::PlayerMode::WINDOW); + + if (_mode == Config::PlayerMode::DUAL) { + _dual_screen = new wxFrame(this, wxID_ANY, {}); + _dual_screen->SetBackgroundColour(wxColour(0, 0, 0)); + _dual_screen->ShowFullScreen(true); + _viewer.panel()->Reparent(_dual_screen); + _viewer.panel()->SetFocus(); + _dual_screen->Show(); + LOG_DEBUG_PLAYER("Setting up dual screen mode with {} displays", wxDisplay::GetCount()); + for (auto index = 0U; index < wxDisplay::GetCount(); ++index) { + wxDisplay display(index); + auto client = display.GetClientArea(); + auto mode = display.GetCurrentMode(); + auto geometry = display.GetGeometry(); + LOG_DEBUG_PLAYER("Display {}", index); + LOG_DEBUG_PLAYER(" ClientArea position=({}, {}) size=({}, {})", client.GetX(), client.GetY(), client.GetWidth(), client.GetHeight()); + LOG_DEBUG_PLAYER(" Geometry position=({}, {}) size=({}, {})", geometry.GetX(), geometry.GetY(), geometry.GetWidth(), geometry.GetHeight()); + LOG_DEBUG_PLAYER(" Mode size=({}, {})", mode.GetWidth(), mode.GetHeight()); + LOG_DEBUG_PLAYER(" Primary? {}", static_cast<int>(display.IsPrimary())); + } + if (wxDisplay::GetCount() > 1) { + wxRect geometry[2] = { + wxDisplay(0U).GetGeometry(), + wxDisplay(1U).GetGeometry() + }; + auto const image_display = Config::instance()->image_display(); + _dual_screen->Move(geometry[image_display].GetX(), geometry[image_display].GetY()); + _viewer.panel()->SetSize(geometry[image_display].GetWidth(), geometry[image_display].GetHeight()); + Move(geometry[1 - image_display].GetX(), geometry[1 - image_display].GetY()); + } + _dual_screen->Bind(wxEVT_CHAR_HOOK, boost::bind(&PlayerFrame::dual_screen_key_press, this, _1)); + } else { + if (_dual_screen) { + _viewer.panel()->Reparent(_overall_panel); + _dual_screen->Destroy(); + _dual_screen = 0; + } + } + + setup_main_sizer(_mode); +} + + +void +PlayerFrame::dual_screen_key_press(wxKeyEvent& ev) +{ + if (ev.GetKeyCode() == WXK_F11) { + if (ev.ShiftDown()) { + view_dual_screen(); + } else if (!ev.HasAnyModifiers()) { + view_full_screen(); + } + } +} + + +void +PlayerFrame::view_closed_captions() +{ + _viewer.show_closed_captions(); +} + + +void +PlayerFrame::tools_verify() +{ + DCPOMATIC_ASSERT(!_film->content().empty()); + auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front()); + DCPOMATIC_ASSERT(dcp); + + VerifyDCPDialog dialog(this, _("Verify DCP"), dcp->directories(), _kdms); + dialog.ShowModal(); +} + + +void +PlayerFrame::tools_audio_graph() +{ + DCPOMATIC_ASSERT(!_film->content().empty()); + auto dcp = std::dynamic_pointer_cast<DCPContent>(_film->content().front()); + DCPOMATIC_ASSERT(dcp); + + _audio_dialog.reset(this, _film, dcp); + _audio_dialog->Seek.connect(boost::bind(&FilmViewer::seek, &_viewer, _1, true)); + _audio_dialog->Show(); +} + + +void +PlayerFrame::tools_check_for_updates() +{ + UpdateChecker::instance()->run(); + _update_news_requested = true; +} + + +void +PlayerFrame::tools_timing() +{ + TimerDisplay dialog(this, _viewer.state_timer(), _viewer.gets()); + dialog.ShowModal(); +} + + +void +PlayerFrame::tools_system_information() +{ + if (!_system_information_dialog) { + _system_information_dialog.reset(this, _viewer); + } + + _system_information_dialog->Show(); +} + + +void +PlayerFrame::help_about() +{ + AboutDialog dialog(this); + dialog.ShowModal(); +} + + +void +PlayerFrame::help_report_a_problem() +{ + ReportProblemDialog dialog(this); + if (dialog.ShowModal() == wxID_OK) { + dialog.report(); + } +} + + +void +PlayerFrame::update_checker_state_changed() +{ + auto uc = UpdateChecker::instance(); + + bool const announce = + _update_news_requested || + (uc->stable() && Config::instance()->check_for_updates()) || + (uc->test() && Config::instance()->check_for_updates() && Config::instance()->check_for_test_updates()); + + _update_news_requested = false; + + if (!announce) { + return; + } + + if (uc->state() == UpdateChecker::State::YES) { + UpdateDialog dialog(this, uc->stable(), uc->test()); + dialog.ShowModal(); + } else if (uc->state() == UpdateChecker::State::FAILED) { + error_dialog(this, variant::wx::insert_dcpomatic(_("The %s download server could not be contacted."))); + } else { + error_dialog(this, variant::wx::insert_dcpomatic(_("There are no new versions of %s available."))); + } + + _update_news_requested = false; +} + + +void +PlayerFrame::config_changed(Config::Property prop) +{ + /* Instantly save any config changes when using the player GUI */ + try { + Config::instance()->write_config(); + } catch (FileError& e) { + if (prop != Config::HISTORY) { + error_dialog( + this, + wxString::Format( + _("Could not write to config file at %s. Your changes have not been saved."), + std_to_wx(e.file().string()) + ) + ); + } + } catch (exception& e) { + error_dialog( + this, + _("Could not write to config file. Your changes have not been saved.") + ); + } + + update_from_config(prop); + + setup_http_server(); +} + + +void +PlayerFrame::stop_http_server() +{ + if (_http_server) { + _http_server->stop(); + _http_server_thread.join(); + _http_server.reset(); + } +} + + +void +PlayerFrame::setup_http_server() +{ + stop_http_server(); + + auto config = Config::instance(); + try { + if (config->enable_player_http_server()) { + _http_server.reset(new HTTPServer(config->player_http_server_port())); + _http_server->Play.connect(boost::bind(&FilmViewer::start, &_viewer)); + _http_server->Stop.connect(boost::bind(&FilmViewer::stop, &_viewer)); + _http_server_thread = boost::thread(boost::bind(&HTTPServer::run, _http_server.get())); + } + } catch (std::exception& e) { + LOG_DEBUG_PLAYER("Failed to start player HTTP server ({})", e.what()); + } +} + + +void +PlayerFrame::setup_internal_player_server() +{ + try { + auto server = new InternalPlayerServer(); + server->LoadDCP.connect(boost::bind(&PlayerFrame::load_dcp, this, _1)); + new thread(boost::bind(&InternalPlayerServer::run, server)); + } catch (std::exception& e) { + /* This is not the end of the world; probably a failure to bind the server socket + * because there's already another player running. + */ + LOG_DEBUG_PLAYER("Failed to start internal player server ({})", e.what()); + } +} + + +void +PlayerFrame::update_from_config(Config::Property prop) +{ + for (int i = 0; i < _history_items; ++i) { + delete _file_menu->Remove(ID_file_history + i); + } + + if (_history_separator) { + _file_menu->Remove(_history_separator); + } + delete _history_separator; + _history_separator = nullptr; + + int pos = _history_position; + + /* Clear out non-existent history items before we re-build the menu */ + Config::instance()->clean_player_history(); + auto history = Config::instance()->player_history(); + + if (!history.empty()) { + _history_separator = _file_menu->InsertSeparator(pos++); + } + + for (size_t i = 0; i < history.size(); ++i) { + string s; + if (i < 9) { + s = fmt::format("&{} {}", i + 1, history[i].string()); + } else { + s = history[i].string(); + } + _file_menu->Insert(pos++, ID_file_history + i, std_to_wx(s)); + } + + _history_items = history.size(); + + if (prop == Config::PLAYER_DEBUG_LOG) { + auto p = Config::instance()->player_debug_log_file(); + if (p) { + dcpomatic_log = make_shared<FileLog>(*p); + } else { + dcpomatic_log = make_shared<NullLog>(); + } + } + + dcpomatic_log->set_types(Config::instance()->log_types()); + + set_audio_delay_from_config(); +} + + +void +PlayerFrame::set_menu_sensitivity() +{ + auto const have_content = _film && !_film->content().empty(); + auto const dcp = _viewer.dcp(); + auto const playable = dcp && !dcp->needs_assets() && !dcp->needs_kdm(); + _tools_verify->Enable(have_content); + _tools_audio_graph->Enable(playable); + _file_add_ov->Enable(have_content); + _file_add_kdm->Enable(have_content); + _file_save_frame->Enable(playable); + _view_cpl->Enable(have_content); + _view_eye->Enable(have_content && _film->three_d()); +} + + +void +PlayerFrame::start_stop_pressed() +{ + if (_viewer.playing()) { + _viewer.stop(); + } else { + _viewer.start(); + } +} + + +void +PlayerFrame::go_back_frame() +{ + _viewer.seek_by(-_viewer.one_video_frame(), true); +} + + +void +PlayerFrame::go_forward_frame() +{ + _viewer.seek_by(_viewer.one_video_frame(), true); +} + + +void +PlayerFrame::go_seconds(int s) +{ + _viewer.seek_by(DCPTime::from_seconds(s), true); +} + + +void +PlayerFrame::go_to_start() +{ + _viewer.seek(DCPTime(), true); +} + + +void +PlayerFrame::go_to_end() +{ + _viewer.seek(_film->length() - _viewer.one_video_frame(), true); +} diff --git a/src/wx/player_frame.h b/src/wx/player_frame.h new file mode 100644 index 000000000..43f3cf300 --- /dev/null +++ b/src/wx/player_frame.h @@ -0,0 +1,161 @@ +/* + Copyright (C) 2017-2026 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "audio_dialog.h" +#include "film_viewer.h" +#include "player_stress_tester.h" +#include "system_information_dialog.h" +#include "wx_ptr.h" +#include "lib/config.h" +#include "lib/http_server.h" +#include <dcp/warnings.h> +LIBDCP_DISABLE_WARNINGS +#include <wx/dnd.h> +#include <wx/wx.h> +LIBDCP_ENABLE_WARNINGS +#include <boost/signals2.hpp> +#include <boost/filesystem.hpp> + + +class wxPreferencesEditor; +class PlayerInformation; + + +namespace dcpomatic { +namespace ui { + + +class PlayerFrame : public wxFrame +{ +public: + class DCPDropTarget : public wxFileDropTarget + { + public: + DCPDropTarget(PlayerFrame* owner); + bool OnDropFiles(wxCoord, wxCoord, wxArrayString const& filenames) override; + + private: + PlayerFrame* _frame; + }; + + PlayerFrame(); + ~PlayerFrame(); + + void close(wxCloseEvent& ev); + void setup_main_sizer(Config::PlayerMode mode); + bool playback_permitted(); + void too_many_frames_dropped(); + void set_decode_reduction(boost::optional<int> reduction); + void load_dcp(boost::filesystem::path dir); + void reset_film_weak(std::weak_ptr<Film> weak_film, boost::optional<float> crop_to_ratio); + void reset_film(std::shared_ptr<Film> film = std::make_shared<Film>(boost::none), boost::optional<float> crop_to_ratio = {}); + + /* _film is now something new: set up to play it */ + void prepare_to_play_film(boost::optional<float> crop_to_ratio); + void set_audio_delay_from_config(); + void load_stress_script(boost::filesystem::path path); + void idle(); + +private: + void examine_content(); + bool report_errors_from_last_job(wxWindow* parent) const; + void setup_menu(wxMenuBar* m); + void file_open(); + void file_add_ov(); + void file_add_kdm(); + void file_save_frame(); + void file_history(wxCommandEvent& event); + void file_close(); + void file_exit(); + void edit_preferences(); + void view_cpl(wxCommandEvent& ev); + void view_eye_changed(wxCommandEvent& ev); + void view_full_screen(); + void view_dual_screen(); + void setup_menu(); + void setup_screen(); + void dual_screen_key_press(wxKeyEvent& ev); + void view_closed_captions(); + void tools_verify(); + void tools_audio_graph(); + void tools_check_for_updates(); + void tools_timing(); + void tools_system_information(); + void help_about(); + void help_report_a_problem(); + + void update_checker_state_changed(); + void config_changed(Config::Property prop); + void stop_http_server(); + void setup_http_server(); + void setup_internal_player_server(); + void update_from_config(Config::Property prop); + void set_menu_sensitivity(); + void start_stop_pressed(); + void go_back_frame(); + void go_forward_frame(); + void go_seconds(int s); + void go_to_start(); + void go_to_end(); + + wxFrame* _dual_screen = nullptr; + bool _update_news_requested = false; + PlayerInformation* _info = nullptr; + Config::PlayerMode _mode; + wxPreferencesEditor* _config_dialog = nullptr; + wxPanel* _overall_panel = nullptr; + wxMenu* _file_menu = nullptr; + wxMenuItem* _view_cpl = nullptr; + wxMenu* _cpl_menu = nullptr; + wxMenuItem* _view_eye = nullptr; + wxMenuItem* _view_eye_left = nullptr; + wxMenu* _view_eye_menu = nullptr; + int _history_items = 0; + int _history_position = 0; + wxMenuItem* _history_separator = nullptr; + FilmViewer _viewer; + Controls* _controls; + wx_ptr<SystemInformationDialog> _system_information_dialog; + std::shared_ptr<Film> _film; + boost::signals2::scoped_connection _config_changed_connection; + boost::signals2::scoped_connection _examine_job_connection; + wxMenuItem* _file_add_ov = nullptr; + wxMenuItem* _file_add_kdm = nullptr; + wxMenuItem* _file_save_frame = nullptr; + wxMenuItem* _tools_verify = nullptr; + wxMenuItem* _tools_audio_graph = nullptr; + wxMenuItem* _view_full_screen = nullptr; + wxMenuItem* _view_dual_screen = nullptr; + wxSizer* _main_sizer = nullptr; + PlayerStressTester _stress; + /** KDMs that have been loaded, so that we can pass them to the verifier */ + std::vector<boost::filesystem::path> _kdms; + boost::thread _http_server_thread; + std::unique_ptr<HTTPServer> _http_server; + struct timeval _last_http_server_update = { 0, 0 }; + wx_ptr<AudioDialog> _audio_dialog; +}; + + +} +} + + diff --git a/src/wx/wscript b/src/wx/wscript index 99a6f4814..03d6a236a 100644 --- a/src/wx/wscript +++ b/src/wx/wscript @@ -136,6 +136,7 @@ sources = """ password_entry.cc player_config_dialog.cc player_information.cc + player_frame.cc player_stress_tester.cc playhead_to_timecode_dialog.cc playhead_to_frame_dialog.cc |
