summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2026-01-25 01:02:02 +0100
committerCarl Hetherington <cth@carlh.net>2026-02-16 01:20:38 +0100
commit5d522d0b3648d8b248efc16c0238af2cb6958cdf (patch)
treed05b15b8178b0c5c0478ebc6b098be3c0773dc51
parentf9e61eb0bf8b51a6ffd30857d6d4cced5479f19e (diff)
Move player frame (DOMFrame) class into wx/ as PlayerFrame.
-rw-r--r--src/lib/dcp_examiner.h1
-rw-r--r--src/tools/dcpomatic_player.cc1252
-rw-r--r--src/wx/player_frame.cc1335
-rw-r--r--src/wx/player_frame.h161
-rw-r--r--src/wx/wscript1
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