From 651722fb46269ff06e5ff41227fd874ed5fd9854 Mon Sep 17 00:00:00 2001 From: Carl Hetherington Date: Tue, 27 Jan 2026 00:38:03 +0100 Subject: Rework player content handling. The idea now is ... There is a "pending" or "next" playlist. You can load a playlist from the database, or add content to it. Play loads the pending playlist into the current one and starts playing it. Stop stops and clears the current playlist. Pause pauses. While something is playing you can do what you like to the next playlist. --- src/wx/content_view.cc | 13 ++ src/wx/content_view.h | 4 + src/wx/controls.cc | 15 ++ src/wx/controls.h | 6 +- src/wx/player_frame.cc | 391 ++++++++++++++++++++++++++++++++++++-------- src/wx/player_frame.h | 22 ++- src/wx/playlist_controls.cc | 327 +++++++++++++++--------------------- src/wx/playlist_controls.h | 38 +++-- src/wx/standard_controls.cc | 15 -- src/wx/standard_controls.h | 3 - web/index.html | 63 +++---- web/sidebar.html | 4 +- 12 files changed, 563 insertions(+), 338 deletions(-) diff --git a/src/wx/content_view.cc b/src/wx/content_view.cc index 46b8c8ce9..52e8e708e 100644 --- a/src/wx/content_view.cc +++ b/src/wx/content_view.cc @@ -51,6 +51,9 @@ using std::string; using std::vector; using std::weak_ptr; using boost::optional; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif using namespace dcpomatic; @@ -62,6 +65,16 @@ ContentView::ContentView (wxWindow* parent) AppendColumn({}, wxLIST_FORMAT_LEFT, 80); /* annotation text */ AppendColumn({}, wxLIST_FORMAT_LEFT, 580); + + Bind(wxEVT_LIST_ITEM_ACTIVATED, boost::bind(&ContentView::activated, this, _1)); +} + + +void +ContentView::activated(wxListEvent& ev) +{ + DCPOMATIC_ASSERT(ev.GetIndex() < static_cast(_content.size())); + Activated(_content[ev.GetIndex()]); } diff --git a/src/wx/content_view.h b/src/wx/content_view.h index 300e698f3..30284d42e 100644 --- a/src/wx/content_view.h +++ b/src/wx/content_view.h @@ -24,6 +24,7 @@ LIBDCP_DISABLE_WARNINGS #include LIBDCP_ENABLE_WARNINGS +#include #include @@ -39,8 +40,11 @@ public: std::shared_ptr selected () const; void update (); + boost::signals2::signal)> Activated; + private: void add (std::shared_ptr content); + void activated(wxListEvent& ev); std::weak_ptr _film; std::vector> _content; diff --git a/src/wx/controls.cc b/src/wx/controls.cc index fce3fd7eb..2c4e92108 100644 --- a/src/wx/controls.cc +++ b/src/wx/controls.cc @@ -457,3 +457,18 @@ Controls::seek(int slider) slider_moved(false); slider_released(); } + + +void +Controls::play() +{ + _viewer.start(); +} + + +void +Controls::stop() +{ + _viewer.stop(); +} + diff --git a/src/wx/controls.h b/src/wx/controls.h index 3dec6fb88..21b469bb7 100644 --- a/src/wx/controls.h +++ b/src/wx/controls.h @@ -62,14 +62,16 @@ public: void set_film(std::shared_ptr film); - virtual void play() {}; - virtual void stop() {}; + void play(); + void stop(); void seek(int slider); std::shared_ptr film() const; void back_frame(); void forward_frame(); + virtual void playlist_changed() {} + protected: virtual void started(); diff --git a/src/wx/player_frame.cc b/src/wx/player_frame.cc index a945a5b1d..afc43b807 100644 --- a/src/wx/player_frame.cc +++ b/src/wx/player_frame.cc @@ -48,6 +48,7 @@ #include "lib/font_config.h" #include "lib/job_manager.h" #include "lib/null_log.h" +#include "lib/show_playlist_content_store.h" #include "lib/text_content.h" #include "lib/update_checker.h" #include "lib/variant.h" @@ -59,6 +60,7 @@ LIBDCP_DISABLE_WARNINGS #include #include +#include #include LIBDCP_ENABLE_WARNINGS @@ -214,9 +216,7 @@ PlayerFrame::PlayerFrame() Bind(wxEVT_CLOSE_WINDOW, boost::bind(&PlayerFrame::close, this, _1)); - if (Config::instance()->enable_player_http_server()) { - update_content_store(); - } + update_content_store(); if (Config::instance()->player_mode() == Config::PlayerMode::DUAL) { _controls = new PlaylistControls(_overall_panel, this, _viewer); @@ -227,6 +227,7 @@ PlayerFrame::PlayerFrame() _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)); + _viewer.Finished.connect(boost::bind(&PlayerFrame::viewer_finished, this)); _info = new PlayerInformation(_overall_panel, _viewer); setup_main_sizer(Config::instance()->player_mode()); #ifdef __WXOSX__ @@ -267,7 +268,7 @@ PlayerFrame::PlayerFrame() 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(); + take_playlist_entry(); UpdateChecker::instance()->StateChanged.connect(boost::bind(&PlayerFrame::update_checker_state_changed, this)); setup_screen(); @@ -324,14 +325,14 @@ PlayerFrame::setup_main_sizer(Config::PlayerMode mode) bool PlayerFrame::playback_permitted() { - if (!_film || !Config::instance()->respect_kdm_validity_periods()) { + if (!Config::instance()->respect_kdm_validity_periods()) { return true; } bool ok = true; - for (auto i: _film->content()) { - auto d = dynamic_pointer_cast(i); - if (d && !d->kdm_timing_window_valid()) { + for (auto content: _playlist) { + auto dcp = dynamic_pointer_cast(content.first); + if (dcp && !dcp->kdm_timing_window_valid()) { ok = false; } } @@ -379,39 +380,30 @@ PlayerFrame::set_decode_reduction(optional reduction) void PlayerFrame::load_dcp(boost::filesystem::path dir) { - DCPOMATIC_ASSERT(_film); - - auto film = std::make_shared(optional()); - try { _stress.set_suspended(true); /* Handler to set things up once the DCP has been examined */ - auto setup = [this](weak_ptr weak_film, weak_ptr weak_job, weak_ptr weak_content) + auto setup = [this](weak_ptr weak_job, weak_ptr weak_content) { auto job = weak_job.lock(); if (!job || !job->finished_ok()) { return; } - auto content = weak_content.lock(); - if (!content) { - return; + if (auto content = weak_content.lock()) { + _playlist = { make_pair(content, boost::optional()) }; + _playlist_position = 0; + _controls->playlist_changed(); + take_playlist_entry(); } - auto film = weak_film.lock(); - if (!film) { - return; - } - - film->add_content({content}); _stress.set_suspended(false); - reset_film(film); }; auto dcp = make_shared(dir); auto job = make_shared(vector>{dcp}, true); - _examine_job_connection = job->Finished.connect(boost::bind(setup, weak_ptr(film), weak_ptr(job), weak_ptr(dcp))); + _examine_job_connection = job->Finished.connect(boost::bind(setup, weak_ptr(job), weak_ptr(dcp))); JobManager::instance()->add(job); bool const ok = display_progress(variant::wx::dcpomatic_player(), _("Loading content")); if (ok && report_errors_from_last_job(this)) { @@ -436,19 +428,6 @@ PlayerFrame::load_dcp(boost::filesystem::path dir) } -void -PlayerFrame::reset_film(shared_ptr film, optional 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 crop_to_ratio) @@ -553,13 +532,9 @@ PlayerFrame::prepare_to_play_film(optional crop_to_ratio) 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()); + for (auto content: _playlist) { + if (content.first->audio) { + content.first->audio->set_delay(Config::instance()->player_audio_delay()); } } } @@ -596,8 +571,11 @@ PlayerFrame::idle() void PlayerFrame::examine_content() { - DCPOMATIC_ASSERT(_film); - auto dcp = dynamic_pointer_cast(_film->content().front()); + if (_playlist.empty()) { + return; + } + + auto dcp = dynamic_pointer_cast(_playlist.front().first); DCPOMATIC_ASSERT(dcp); dcp->examine({}, true); @@ -761,9 +739,8 @@ PlayerFrame::file_add_ov() } if (r == wxID_OK) { - DCPOMATIC_ASSERT(_film); - DCPOMATIC_ASSERT(!_film->content().empty()); - auto dcp = std::dynamic_pointer_cast(_film->content().front()); + DCPOMATIC_ASSERT(!_playlist.empty()); + auto dcp = std::dynamic_pointer_cast(_playlist.front().first); DCPOMATIC_ASSERT(dcp); try { @@ -774,7 +751,7 @@ PlayerFrame::file_add_ov() } auto job = make_shared(vector>{dcp}, true); - _examine_job_connection = job->Finished.connect(boost::bind(&PlayerFrame::prepare_to_play_film, this, Config::instance()->player_crop_output_ratio())); + _examine_job_connection = job->Finished.connect(boost::bind(&PlayerFrame::take_playlist_entry, this)); JobManager::instance()->add(job); display_progress(variant::wx::dcpomatic_player(), _("Loading content")); @@ -789,21 +766,19 @@ 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(_film->content().front()); + DCPOMATIC_ASSERT(!_playlist.empty()); + auto dcp = std::dynamic_pointer_cast(_playlist.front().first); 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(); + 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; @@ -871,7 +846,11 @@ PlayerFrame::file_history(wxCommandEvent& event) void PlayerFrame::file_close() { - reset_film(); + _playlist.clear(); + _playlist_position = 0; + _controls->playlist_changed(); + + take_playlist_entry(); _info->triggered_update(); set_menu_sensitivity(); } @@ -901,7 +880,8 @@ PlayerFrame::edit_preferences() void PlayerFrame::view_cpl(wxCommandEvent& ev) { - auto dcp = std::dynamic_pointer_cast(_film->content().front()); + DCPOMATIC_ASSERT(!_playlist.empty()); + auto dcp = std::dynamic_pointer_cast(_playlist.front().first); DCPOMATIC_ASSERT(dcp); auto cpls = dcp->cpls(); int id = ev.GetId() - ID_view_cpl; @@ -1041,8 +1021,8 @@ PlayerFrame::view_closed_captions() void PlayerFrame::tools_verify() { - DCPOMATIC_ASSERT(!_film->content().empty()); - auto dcp = std::dynamic_pointer_cast(_film->content().front()); + DCPOMATIC_ASSERT(!_playlist.empty()); + auto dcp = std::dynamic_pointer_cast(_playlist.front().first); DCPOMATIC_ASSERT(dcp); VerifyDCPDialog dialog(this, _("Verify DCP"), dcp->directories(), _kdms); @@ -1053,8 +1033,8 @@ PlayerFrame::tools_verify() void PlayerFrame::tools_audio_graph() { - DCPOMATIC_ASSERT(!_film->content().empty()); - auto dcp = std::dynamic_pointer_cast(_film->content().front()); + DCPOMATIC_ASSERT(!_playlist.empty()); + auto dcp = std::dynamic_pointer_cast(_playlist.front().first); DCPOMATIC_ASSERT(dcp); _audio_dialog.reset(this, _film, dcp); @@ -1265,7 +1245,7 @@ PlayerFrame::update_from_config(Config::Property prop) void PlayerFrame::set_menu_sensitivity() { - auto const have_content = _film && !_film->content().empty(); + auto const have_content = !_playlist.empty(); auto const dcp = _viewer.dcp(); auto const playable = dcp && !dcp->needs_assets() && !dcp->needs_kdm(); _tools_verify->Enable(have_content); @@ -1322,3 +1302,276 @@ PlayerFrame::go_to_end() { _viewer.seek(_film->length() - _viewer.one_video_frame(), true); } + +static +optional +get_kdm_from_directory(shared_ptr dcp) +{ + using namespace boost::filesystem; + auto kdm_dir = Config::instance()->player_kdm_directory(); + if (!kdm_dir) { + return {}; + } + for (auto i: directory_iterator(*kdm_dir)) { + try { + if (file_size(i.path()) < MAX_KDM_SIZE) { + dcp::EncryptedKDM kdm(dcp::file_to_string(i.path())); + if (kdm.cpl_id() == dcp->cpl()) { + return kdm; + } + } + } catch (std::exception& e) { + /* Hey well */ + } + } + + return {}; +} + + +bool +PlayerFrame::set_playlist(vector playlist) +{ + bool was_playing = false; + if (_viewer.playing()) { + was_playing = true; + _viewer.stop(); + } + + wxProgressDialog dialog(variant::wx::dcpomatic(), _("Loading playlist and KDMs")); + + _playlist.clear(); + _playlist_position = 0; + + auto const store = ShowPlaylistContentStore::instance(); + for (auto const& entry: playlist) { + dialog.Pulse(); + auto content = store->get(entry); + if (!content) { + error_dialog(this, _("This playlist cannot be loaded as some content is missing.")); + _playlist.clear(); + _controls->playlist_changed(); + return false; + } + + auto dcp = dynamic_pointer_cast(content); + if (dcp && dcp->needs_kdm()) { + optional kdm; + kdm = get_kdm_from_directory(dcp); + if (kdm) { + try { + dcp->add_kdm(*kdm); + dcp->examine(shared_ptr(), true); + } catch (KDMError& e) { + error_dialog(this, _("Could not load KDM.")); + } + } + if (dcp->needs_kdm()) { + /* We didn't get a KDM for this */ + error_dialog(this, _("This playlist cannot be loaded as a KDM is missing or incorrect.")); + _playlist.clear(); + _controls->playlist_changed(); + return false; + } + } + _playlist.push_back({content, entry.crop_to_ratio()}); + } + + take_playlist_entry(); + + if (was_playing) { + _viewer.start(); + } + + _controls->playlist_changed(); + + return true; +} + + +/** Stop the viewer, take the thing at _playlist_position and prepare to play it. + * Set up to play nothing if the playlist is empty, or we're off the + * end of it. + * @return true if the viewer was playing when this method was called. + */ +bool +PlayerFrame::take_playlist_entry() +{ + boost::optional crop_to_ratio; + + if (_playlist_position < 0 || _playlist_position >= static_cast(_playlist.size())) { + _film = std::make_shared(boost::none); + } else { + auto const entry = _playlist[_playlist_position]; + + _film = std::make_shared(optional()); + _film->add_content({entry.first}); + + if (!entry.second) { + crop_to_ratio = Config::instance()->player_crop_output_ratio(); + } + } + + bool const playing = _viewer.playing(); + if (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(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(_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); + return playing; +} + + +void +PlayerFrame::viewer_finished() +{ + _playlist_position++; + + /* Either get the next piece of content, or go black */ + take_playlist_entry(); + + if (_playlist_position < static_cast(_playlist.size())) { + /* Start the next piece of content */ + _viewer.start(); + } else { + /* Be ready to start again from the top of the playlist */ + _playlist_position = 0; + } +} + + + +bool +PlayerFrame::can_do_next() const +{ + return _playlist_position < (static_cast(_playlist.size()) - 1); +} + + +void +PlayerFrame::next() +{ + _playlist_position++; + if (take_playlist_entry()) { + _viewer.start(); + } +} + + +bool +PlayerFrame::can_do_previous() const +{ + return _playlist_position > 0; +} + + +void +PlayerFrame::previous() +{ + _playlist_position--; + if (take_playlist_entry()) { + _viewer.start(); + } +} + + +vector> +PlayerFrame::playlist() const +{ + vector> content; + for (auto entry: _playlist) { + content.push_back(entry.first); + } + return content; +} + + diff --git a/src/wx/player_frame.h b/src/wx/player_frame.h index 84da6b3a6..e50bc7995 100644 --- a/src/wx/player_frame.h +++ b/src/wx/player_frame.h @@ -26,6 +26,7 @@ #include "wx_ptr.h" #include "lib/config.h" #include "lib/http_server.h" +#include "lib/show_playlist_entry.h" #include LIBDCP_DISABLE_WARNINGS #include @@ -65,14 +66,23 @@ public: void too_many_frames_dropped(); void set_decode_reduction(boost::optional reduction); void load_dcp(boost::filesystem::path dir); - void reset_film(std::shared_ptr film = std::make_shared(boost::none), boost::optional crop_to_ratio = {}); - /* _film is now something new: set up to play it */ - void prepare_to_play_film(boost::optional crop_to_ratio); void set_audio_delay_from_config(); void load_stress_script(boost::filesystem::path path); void idle(); + /** Set the playlist. If we're currently playing, this will stop whatever is + * happening now and start playing this playlist. + */ + bool set_playlist(std::vector playlist); + + std::vector> playlist() const; + + bool can_do_next() const; + void next(); + bool can_do_previous() const; + void previous(); + private: void examine_content(); bool report_errors_from_last_job(wxWindow* parent) const; @@ -115,6 +125,10 @@ private: void go_to_start(); void go_to_end(); + bool take_playlist_entry(); + void prepare_to_play_film(boost::optional crop_to_ratio); + void viewer_finished(); + wxFrame* _dual_screen = nullptr; bool _update_news_requested = false; PlayerInformation* _info = nullptr; @@ -134,6 +148,8 @@ private: Controls* _controls; wx_ptr _system_information_dialog; std::shared_ptr _film; + std::vector, boost::optional>> _playlist; + int _playlist_position = 0; boost::signals2::scoped_connection _config_changed_connection; boost::signals2::scoped_connection _examine_job_connection; wxMenuItem* _file_add_ov = nullptr; diff --git a/src/wx/playlist_controls.cc b/src/wx/playlist_controls.cc index d0d0c1d23..a0f91b00f 100644 --- a/src/wx/playlist_controls.cc +++ b/src/wx/playlist_controls.cc @@ -52,7 +52,11 @@ using std::shared_ptr; using std::sort; using std::string; using std::vector; +using std::weak_ptr; using boost::optional; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif using namespace dcpomatic; using namespace dcpomatic::ui; @@ -72,26 +76,26 @@ PlaylistControls::PlaylistControls(wxWindow* parent, PlayerFrame* player, FilmVi _button_sizer->Add(_stop_button, 0, wxEXPAND); _button_sizer->Add(_next_button, 0, wxEXPAND); - _spl_view = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER); - _spl_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 740); + _playlists_view = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER); + _playlists_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 740); auto left_sizer = new wxBoxSizer(wxVERTICAL); - auto e_sizer = new wxBoxSizer(wxHORIZONTAL); + auto h_sizer = new wxBoxSizer(wxHORIZONTAL); wxFont subheading_font(*wxNORMAL_FONT); subheading_font.SetWeight(wxFONTWEIGHT_BOLD); - auto spl_header = new wxBoxSizer(wxHORIZONTAL); + auto playlists_header = new wxBoxSizer(wxHORIZONTAL); { auto m = new StaticText(this, _("Playlists")); m->SetFont(subheading_font); - spl_header->Add(m, 1, wxALIGN_CENTER_VERTICAL); + playlists_header->Add(m, 1, wxALIGN_CENTER_VERTICAL); } - _refresh_spl_view = new Button(this, _("Refresh")); - spl_header->Add(_refresh_spl_view, 0, wxBOTTOM, DCPOMATIC_SIZER_GAP / 2); + _refresh_playlists_view = new Button(this, _("Refresh")); + playlists_header->Add(_refresh_playlists_view, 0, wxBOTTOM, DCPOMATIC_SIZER_GAP / 2); - left_sizer->Add(spl_header, 0, wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP); - left_sizer->Add(_spl_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP); + left_sizer->Add(playlists_header, 0, wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP); + left_sizer->Add(_playlists_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP); _content_view = new ContentView(this); @@ -107,24 +111,54 @@ PlaylistControls::PlaylistControls(wxWindow* parent, PlayerFrame* player, FilmVi left_sizer->Add(content_header, 0, wxTOP | wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP); left_sizer->Add(_content_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP); - _current_spl_view = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER); - _current_spl_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 500); - _current_spl_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 80); - e_sizer->Add(left_sizer, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP); - e_sizer->Add(_current_spl_view, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP); + auto right_sizer = new wxBoxSizer(wxVERTICAL); - _v_sizer->Add(e_sizer, 1, wxEXPAND); + _next_playlist_view = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER); + _next_playlist_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 600); + _next_playlist_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 80); + + auto next_playlist_header = new wxBoxSizer(wxHORIZONTAL); + { + auto m = new StaticText(this, _("Next playlist")); + m->SetFont(subheading_font); + next_playlist_header->Add(m, 1, wxALIGN_CENTER_VERTICAL); + } + _clear_next_playlist = new wxButton(this, wxID_ANY, _("Clear")); + next_playlist_header->Add(_clear_next_playlist, 0, wxBOTTOM, DCPOMATIC_SIZER_GAP / 2); + + right_sizer->Add(next_playlist_header, 0, wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP); + right_sizer->Add(_next_playlist_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP); + + _current_playlist_view = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER); + _current_playlist_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 600); + _current_playlist_view->AppendColumn({}, wxLIST_FORMAT_LEFT, 80); + + auto current_playlist_header = new wxBoxSizer(wxHORIZONTAL); + { + auto m = new StaticText(this, _("Current playlist")); + m->SetFont(subheading_font); + current_playlist_header->Add(m, 1, wxALIGN_CENTER_VERTICAL); + } + + right_sizer->Add(current_playlist_header, 0, wxTOP | wxBOTTOM | wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP); + right_sizer->Add(_current_playlist_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP); + + h_sizer->Add(left_sizer, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP); + h_sizer->Add(right_sizer, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP); + _v_sizer->Add(h_sizer, 1, wxEXPAND); _play_button->Bind (wxEVT_BUTTON, boost::bind(&PlaylistControls::play_clicked, this)); _pause_button->Bind (wxEVT_BUTTON, boost::bind(&PlaylistControls::pause_clicked, this)); _stop_button->Bind (wxEVT_BUTTON, boost::bind(&PlaylistControls::stop_clicked, this)); _next_button->Bind (wxEVT_BUTTON, boost::bind(&PlaylistControls::next_clicked, this)); _previous_button->Bind(wxEVT_BUTTON, boost::bind(&PlaylistControls::previous_clicked, this)); - _spl_view->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind(&PlaylistControls::spl_selection_changed, this)); - _spl_view->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind(&PlaylistControls::spl_selection_changed, this)); - _viewer.Finished.connect(boost::bind(&PlaylistControls::viewer_finished, this)); - _refresh_spl_view->Bind(wxEVT_BUTTON, boost::bind(&PlaylistControls::update_playlists, this)); + _playlists_view->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind(&PlaylistControls::playlist_selection_changed, this)); + _playlists_view->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind(&PlaylistControls::playlist_selection_changed, this)); + _refresh_playlists_view->Bind(wxEVT_BUTTON, boost::bind(&PlaylistControls::update_playlists, this)); _refresh_content_view->Bind(wxEVT_BUTTON, boost::bind(&ContentView::update, _content_view)); + _clear_next_playlist->Bind(wxEVT_BUTTON, boost::bind(&PlaylistControls::clear_next_playlist, this)); + + _content_view->Activated.connect(boost::bind(&PlaylistControls::content_activated, this, _1)); update_playlists(); _content_view->update(); @@ -132,40 +166,45 @@ PlaylistControls::PlaylistControls(wxWindow* parent, PlayerFrame* player, FilmVi void -PlaylistControls::started() +PlaylistControls::clear_next_playlist() { - Controls::started(); - _play_button->Enable(false); - _pause_button->Enable(true); + _next_playlist_view->DeleteAllItems(); + _next_playlist.clear(); } -/** Called when the viewer finishes a single piece of content, or it is explicitly stopped */ void -PlaylistControls::stopped() +PlaylistControls::content_activated(weak_ptr weak_content) { - Controls::stopped(); - _play_button->Enable(true); - _pause_button->Enable(false); + if (auto content = weak_content.lock()) { + add_next_playlist_entry(ShowPlaylistEntry(content, {})); + } } void -PlaylistControls::deselect_playlist() +PlaylistControls::started() { - long int const selected = _spl_view->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); - if (selected != -1) { - _selected_playlist = boost::none; - _spl_view->SetItemState(selected, 0, wxLIST_STATE_SELECTED); - } - _player->reset_film(); + Controls::started(); + setup_sensitivity(); +} + + +/** Called when the viewer finishes a single piece of content, or it is explicitly stopped */ +void +PlaylistControls::stopped() +{ + Controls::stopped(); + setup_sensitivity(); } void PlaylistControls::play_clicked() { - _viewer.start(); + if (_player->set_playlist(_next_playlist)) { + _viewer.start(); + } } @@ -173,92 +212,70 @@ void PlaylistControls::setup_sensitivity() { Controls::setup_sensitivity(); - bool const active_job = _active_job && *_active_job != "examine_content"; - bool const c = _film && !_film->content().empty() && !active_job; - _play_button->Enable(c && !_viewer.playing()); - _pause_button->Enable(_viewer.playing()); - _spl_view->Enable(!_viewer.playing()); - _next_button->Enable(can_do_next()); - _previous_button->Enable(can_do_previous()); + _play_button->Enable(!_viewer.playing() && !_paused && !_next_playlist.empty()); + _pause_button->Enable(_viewer.playing() || _paused); + _stop_button->Enable(_viewer.playing() || _paused); + _next_button->Enable(_player->can_do_next()); + _previous_button->Enable(_player->can_do_previous()); } void PlaylistControls::pause_clicked() { - _viewer.stop(); + if (_paused) { + _viewer.start(); + _paused = false; + } else { + _viewer.stop(); + _paused = true; + } + setup_sensitivity(); } void PlaylistControls::stop_clicked() { + _paused = false; _viewer.stop(); _viewer.seek(DCPTime(), true); - if (_selected_playlist) { - _selected_playlist_position = 0; - update_current_content(); - } - deselect_playlist(); -} - - -bool -PlaylistControls::can_do_previous() -{ - return _selected_playlist && (_selected_playlist_position - 1) >= 0; + _player->set_playlist({}); } void PlaylistControls::previous_clicked() { - if (!can_do_previous()) { - return; - } - - _selected_playlist_position--; - update_current_content(); -} - - -bool -PlaylistControls::can_do_next() -{ - return _selected_playlist && (_selected_playlist_position + 1) < static_cast(_playlists->entries(*_selected_playlist).size()); + _player->previous(); } void PlaylistControls::next_clicked() { - if (!can_do_next()) { - return; - } - - _selected_playlist_position++; - update_current_content(); + _player->next(); } void -PlaylistControls::add_playlist_to_list(ShowPlaylist spl) +PlaylistControls::add_playlist_to_list(ShowPlaylist playlist) { - int const N = _spl_view->GetItemCount(); + int const N = _playlists_view->GetItemCount(); wxListItem it; it.SetId(N); it.SetColumn(0); - auto id = _playlists->get_show_playlist_id(spl.uuid()); + auto id = _playlists->get_show_playlist_id(playlist.uuid()); DCPOMATIC_ASSERT(id); it.SetData(id->get()); - string t = spl.name(); + string t = playlist.name(); - if (_playlists->missing(spl.uuid())) { + if (_playlists->missing(playlist.uuid())) { t += " (content missing)"; } it.SetText(std_to_wx(t)); - _spl_view->InsertItem(it); + _playlists_view->InsertItem(it); } @@ -267,14 +284,12 @@ PlaylistControls::update_playlists() { using namespace boost::filesystem; - _spl_view->DeleteAllItems(); + _playlists_view->DeleteAllItems(); _playlists.reset(new ShowPlaylistList()); for (auto i: _playlists->show_playlists()) { add_playlist_to_list(i.second); } - - _selected_playlist = boost::none; } @@ -304,20 +319,31 @@ PlaylistControls::get_kdm_from_directory(shared_ptr dcp) void -PlaylistControls::spl_selection_changed() +PlaylistControls::add_next_playlist_entry(ShowPlaylistEntry entry) { - long int selected = _spl_view->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); + wxListItem it; + it.SetId(_next_playlist_view->GetItemCount()); + it.SetColumn(0); + it.SetText(std_to_wx(entry.name())); + _next_playlist_view->InsertItem(it); + _next_playlist.push_back(entry); + + setup_sensitivity(); +} + + +void +PlaylistControls::playlist_selection_changed() +{ + long int selected = _playlists_view->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); if (selected == -1) { - _current_spl_view->DeleteAllItems(); - _selected_playlist = boost::none; return; } - auto const id = ShowPlaylistID(_spl_view->GetItemData(selected)); + auto const id = ShowPlaylistID(_playlists_view->GetItemData(selected)); if (_playlists->missing(id)) { error_dialog(this, _("This playlist cannot be loaded as some content is missing.")); - deselect_playlist(); return; } @@ -326,72 +352,11 @@ PlaylistControls::spl_selection_changed() return; } - select_playlist(id, 0); -} - - -void -PlaylistControls::select_playlist(ShowPlaylistID selected, int position) -{ - wxProgressDialog dialog(variant::wx::dcpomatic(), _("Loading playlist and KDMs")); - - auto const store = ShowPlaylistContentStore::instance(); - for (auto const& i: _playlists->entries(selected)) { - dialog.Pulse(); - auto dcp = dynamic_pointer_cast(store->get(i)); - if (dcp && dcp->needs_kdm()) { - optional kdm; - kdm = get_kdm_from_directory(dcp); - if (kdm) { - try { - dcp->add_kdm(*kdm); - dcp->examine(shared_ptr(), true); - } catch (KDMError& e) { - error_dialog(this, _("Could not load KDM.")); - } - } - if (dcp->needs_kdm()) { - /* We didn't get a KDM for this */ - error_dialog(this, _("This playlist cannot be loaded as a KDM is missing or incorrect.")); - deselect_playlist(); - return; - } - } - } - - _current_spl_view->DeleteAllItems(); - - int N = 0; - for (auto const& i: _playlists->entries(selected)) { - wxListItem it; - it.SetId(N); - it.SetColumn(0); - it.SetText(std_to_wx(i.name())); - _current_spl_view->InsertItem(it); - ++N; + _next_playlist_view->DeleteAllItems(); + _next_playlist.clear(); + for (auto const& i: _playlists->entries(id)) { + add_next_playlist_entry(i); } - - _selected_playlist = selected; - _selected_playlist_position = position; - dialog.Pulse(); - reset_film(); - dialog.Pulse(); - update_current_content(); -} - - -void -PlaylistControls::reset_film() -{ - DCPOMATIC_ASSERT(_selected_playlist); - - auto film = std::make_shared(optional()); - - auto const entries = _playlists->entries(*_selected_playlist); - DCPOMATIC_ASSERT(_selected_playlist_position < static_cast(entries.size())); - auto const entry = entries[_selected_playlist_position]; - film->add_content(vector>{ShowPlaylistContentStore::instance()->get(entry)}); - _player->reset_film(film, entry.crop_to_ratio()); } @@ -409,50 +374,18 @@ PlaylistControls::config_changed(int property) void -PlaylistControls::update_current_content() -{ - DCPOMATIC_ASSERT(_selected_playlist); - - wxProgressDialog dialog(variant::wx::dcpomatic(), _("Loading content")); - - setup_sensitivity(); - dialog.Pulse(); - reset_film(); -} - - -/** One piece of content in our SPL has finished playing */ -void -PlaylistControls::viewer_finished() +PlaylistControls::playlist_changed() { - if (!_selected_playlist) { - return; - } + _current_playlist_view->DeleteAllItems(); - _selected_playlist_position++; - if (_selected_playlist_position < int(_playlists->entries(*_selected_playlist).size())) { - /* Next piece of content on the SPL */ - update_current_content(); - _viewer.start(); - } else { - /* Finished the whole SPL */ - _selected_playlist_position = 0; - _player->reset_film(); - _play_button->Enable(true); - _pause_button->Enable(false); + int N = 0; + for (auto content: _player->playlist()) { + wxListItem it; + it.SetId(N++); + it.SetColumn(0); + ShowPlaylistEntry entry(content, {}); + it.SetText(std_to_wx(entry.name())); + _current_playlist_view->InsertItem(it); } } - -void -PlaylistControls::play() -{ - play_clicked(); -} - - -void -PlaylistControls::stop() -{ - stop_clicked(); -} diff --git a/src/wx/playlist_controls.h b/src/wx/playlist_controls.h index 71f9d7f47..da17358cb 100644 --- a/src/wx/playlist_controls.h +++ b/src/wx/playlist_controls.h @@ -41,8 +41,7 @@ class PlaylistControls : public Controls public: PlaylistControls(wxWindow* parent, dcpomatic::ui::PlayerFrame* player, FilmViewer& viewer); - void play() override; - void stop() override; + void playlist_changed() override; private: void play_clicked(); @@ -50,21 +49,20 @@ private: void stop_clicked(); void next_clicked(); void previous_clicked(); - void add_playlist_to_list(ShowPlaylist spl); - void update_content_directory(); + void update_playlists(); - void spl_selection_changed(); - void select_playlist(ShowPlaylistID selected, int position); + void playlist_selection_changed(); + + void config_changed(int) override; void started() override; void stopped() override; + void setup_sensitivity() override; - void config_changed(int) override; - void viewer_finished(); - void reset_film(); - void update_current_content(); - bool can_do_previous(); - bool can_do_next(); - void deselect_playlist(); + + void add_playlist_to_list(ShowPlaylist spl); + void add_next_playlist_entry(ShowPlaylistEntry entry); + void clear_next_playlist(); + void content_activated(std::weak_ptr content); boost::optional get_kdm_from_directory(std::shared_ptr dcp); @@ -78,11 +76,15 @@ private: ContentView* _content_view; wxButton* _refresh_content_view; - wxListCtrl* _spl_view; - wxButton* _refresh_spl_view; - wxListCtrl* _current_spl_view; + wxListCtrl* _playlists_view; + wxButton* _refresh_playlists_view; + wxListCtrl* _next_playlist_view; + wxButton* _clear_next_playlist; + + wxListCtrl* _current_playlist_view; std::unique_ptr _playlists; - boost::optional _selected_playlist; - int _selected_playlist_position; + std::vector _next_playlist; + + bool _paused = false; }; diff --git a/src/wx/standard_controls.cc b/src/wx/standard_controls.cc index 9126f24b4..03934e482 100644 --- a/src/wx/standard_controls.cc +++ b/src/wx/standard_controls.cc @@ -87,18 +87,3 @@ StandardControls::setup_sensitivity () _play_button->Enable (_film && !_film->content().empty() && !active_job); } - -void -StandardControls::play () -{ - _play_button->SetValue (true); - play_clicked (); -} - - -void -StandardControls::stop () -{ - _play_button->SetValue (false); - play_clicked (); -} diff --git a/src/wx/standard_controls.h b/src/wx/standard_controls.h index 1b8618763..21f1b0039 100644 --- a/src/wx/standard_controls.h +++ b/src/wx/standard_controls.h @@ -27,9 +27,6 @@ class StandardControls : public Controls public: StandardControls(wxWindow* parent, FilmViewer& viewer, bool editor_controls); - void play () override; - void stop () override; - private: void check_play_state (); void play_clicked (); diff --git a/web/index.html b/web/index.html index db313c48e..b0b5b4505 100644 --- a/web/index.html +++ b/web/index.html @@ -24,33 +24,6 @@ function stop() { fetch("/api/v1/stop", { method: "POST" }); } -function selectedPlaylistId() -{ - var children = document.getElementById("content-and-playlists-inner").children; - for (var i = 0; i < children.length; i++) { - if (children[i].classList.contains("selected")) { - return children[i].uuid; - } - } - - return null; -} - - -function setSelectedPlaylist(uuid) -{ - var children = document.getElementById("content-and-playlists-inner").children; - for (var i = 0; i < children.length; i++) { - var child = children[i]; - if (child.uuid == uuid) { - child.classList.add("selected"); - } else { - child.classList.remove("selected"); - } - } -}; - - // Fetch all playlists and put them in the shared content/playlists div function showPlaylists() { @@ -67,7 +40,20 @@ function showPlaylists() li.appendChild(document.createTextNode(playlist.name)); li.uuid = playlist['uuid']; li.onclick = function() { - setSelectedPlaylist(playlist['uuid']); + fetch("/api/v1/playlist/" + playlist['uuid']).then(response => { + response.json().then(data => { + var nextPlaylist = document.getElementById('next-playlist'); + nextPlaylist.innerHTML = ""; + var ul = document.createElement("ul"); + nextPlaylist.appendChild(ul); + data.content.forEach(content => { + var li = document.createElement("li"); + li.appendChild(document.createTextNode(content.name)); + ul.appendChild(li); + }); + console.log(data); + }) + }); }; document.getElementById('content-and-playlists-inner').appendChild(li); }); @@ -159,6 +145,8 @@ li.playlist.selected { SIDEBAR +
+
@@ -169,14 +157,29 @@ SIDEBAR
-
+
+ +
+Next playlist: +
+
+
+ +
Current playlist: +
+
-
+
+ +
Playlists
Content
+ +
+ diff --git a/web/sidebar.html b/web/sidebar.html index 7ea629cc0..b7c3e7555 100644 --- a/web/sidebar.html +++ b/web/sidebar.html @@ -1,7 +1,9 @@