diff options
| -rw-r--r-- | src/wx/content_view.cc | 13 | ||||
| -rw-r--r-- | src/wx/content_view.h | 4 | ||||
| -rw-r--r-- | src/wx/controls.cc | 15 | ||||
| -rw-r--r-- | src/wx/controls.h | 6 | ||||
| -rw-r--r-- | src/wx/player_frame.cc | 391 | ||||
| -rw-r--r-- | src/wx/player_frame.h | 22 | ||||
| -rw-r--r-- | src/wx/playlist_controls.cc | 327 | ||||
| -rw-r--r-- | src/wx/playlist_controls.h | 38 | ||||
| -rw-r--r-- | src/wx/standard_controls.cc | 15 | ||||
| -rw-r--r-- | src/wx/standard_controls.h | 3 | ||||
| -rw-r--r-- | web/index.html | 63 | ||||
| -rw-r--r-- | 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<int>(_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 <wx/listctrl.h> LIBDCP_ENABLE_WARNINGS +#include <boost/signals2.hpp> #include <vector> @@ -39,8 +40,11 @@ public: std::shared_ptr<Content> selected () const; void update (); + boost::signals2::signal<void (std::weak_ptr<Content>)> Activated; + private: void add (std::shared_ptr<Content> content); + void activated(wxListEvent& ev); std::weak_ptr<Film> _film; std::vector<std::shared_ptr<Content>> _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> film); - virtual void play() {}; - virtual void stop() {}; + void play(); + void stop(); void seek(int slider); std::shared_ptr<Film> 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 <wx/display.h> #include <wx/preferences.h> +#include <wx/progdlg.h> #include <wx/stdpaths.h> 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<DCPContent>(i); - if (d && !d->kdm_timing_window_valid()) { + for (auto content: _playlist) { + auto dcp = dynamic_pointer_cast<DCPContent>(content.first); + if (dcp && !dcp->kdm_timing_window_valid()) { ok = false; } } @@ -379,39 +380,30 @@ PlayerFrame::set_decode_reduction(optional<int> 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 setup = [this](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; + if (auto content = weak_content.lock()) { + _playlist = { make_pair(content, boost::optional<float>()) }; + _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<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))); + _examine_job_connection = job->Finished.connect(boost::bind<void>(setup, 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)) { @@ -436,19 +428,6 @@ PlayerFrame::load_dcp(boost::filesystem::path dir) } -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) @@ -553,13 +532,9 @@ PlayerFrame::prepare_to_play_film(optional<float> 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<DCPContent>(_film->content().front()); + if (_playlist.empty()) { + return; + } + + auto dcp = dynamic_pointer_cast<DCPContent>(_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<DCPContent>(_film->content().front()); + DCPOMATIC_ASSERT(!_playlist.empty()); + auto dcp = std::dynamic_pointer_cast<DCPContent>(_playlist.front().first); DCPOMATIC_ASSERT(dcp); try { @@ -774,7 +751,7 @@ PlayerFrame::file_add_ov() } 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())); + _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<DCPContent>(_film->content().front()); + DCPOMATIC_ASSERT(!_playlist.empty()); + auto dcp = std::dynamic_pointer_cast<DCPContent>(_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<DCPContent>(_film->content().front()); + DCPOMATIC_ASSERT(!_playlist.empty()); + auto dcp = std::dynamic_pointer_cast<DCPContent>(_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<DCPContent>(_film->content().front()); + DCPOMATIC_ASSERT(!_playlist.empty()); + auto dcp = std::dynamic_pointer_cast<DCPContent>(_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<DCPContent>(_film->content().front()); + DCPOMATIC_ASSERT(!_playlist.empty()); + auto dcp = std::dynamic_pointer_cast<DCPContent>(_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<dcp::EncryptedKDM> +get_kdm_from_directory(shared_ptr<DCPContent> 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<ShowPlaylistEntry> 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<DCPContent>(content); + if (dcp && dcp->needs_kdm()) { + optional<dcp::EncryptedKDM> kdm; + kdm = get_kdm_from_directory(dcp); + if (kdm) { + try { + dcp->add_kdm(*kdm); + dcp->examine(shared_ptr<Job>(), 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<float> crop_to_ratio; + + if (_playlist_position < 0 || _playlist_position >= static_cast<int>(_playlist.size())) { + _film = std::make_shared<Film>(boost::none); + } else { + auto const entry = _playlist[_playlist_position]; + + _film = std::make_shared<Film>(optional<boost::filesystem::path>()); + _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<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); + 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<int>(_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<int>(_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<shared_ptr<Content>> +PlayerFrame::playlist() const +{ + vector<shared_ptr<Content>> 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 <dcp/warnings.h> LIBDCP_DISABLE_WARNINGS #include <wx/dnd.h> @@ -65,14 +66,23 @@ public: void too_many_frames_dropped(); void set_decode_reduction(boost::optional<int> reduction); void load_dcp(boost::filesystem::path dir); - 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(); + /** 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<ShowPlaylistEntry> playlist); + + std::vector<std::shared_ptr<Content>> 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<float> 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<SystemInformationDialog> _system_information_dialog; std::shared_ptr<Film> _film; + std::vector<std::pair<std::shared_ptr<Content>, boost::optional<float>>> _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<Content> 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<int>(_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<DCPContent> 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<DCPContent>(store->get(i)); - if (dcp && dcp->needs_kdm()) { - optional<dcp::EncryptedKDM> kdm; - kdm = get_kdm_from_directory(dcp); - if (kdm) { - try { - dcp->add_kdm(*kdm); - dcp->examine(shared_ptr<Job>(), 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<Film>(optional<boost::filesystem::path>()); - - auto const entries = _playlists->entries(*_selected_playlist); - DCPOMATIC_ASSERT(_selected_playlist_position < static_cast<int>(entries.size())); - auto const entry = entries[_selected_playlist_position]; - film->add_content(vector<shared_ptr<Content>>{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> content); boost::optional<dcp::EncryptedKDM> get_kdm_from_directory(std::shared_ptr<DCPContent> 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<ShowPlaylistList> _playlists; - boost::optional<ShowPlaylistID> _selected_playlist; - int _selected_playlist_position; + std::vector<ShowPlaylistEntry> _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 +<div style="margin-left: 15%;"> + <div id="controls" style="float: left; width: 20%;"> <button name="play" value="play" onclick="play()">Play</button> <button name="stop" value="stop" onclick="stop()">Stop</button> @@ -169,14 +157,29 @@ SIDEBAR </table> </div> -<div id="current-playlist" style="float: left; width: 20%;"> +<div style="float: right; width: 70%;"> + +<div style="float: right; width: 100%;"> +Next playlist: +<div id="next-playlist" style="width: 100%; height: 40vh;"> +</div> +</div> + +<div style="float: right; width: 100%; margin-top: 4em;"> Current playlist: +<div id="current-playlist" style="width: 100%; height: 10vh;"> +</div> </div> -<div id="content-and-playlists" style="float: left; width: 20%;"> +</div> + +<div id="content-and-playlists" style="float: left; width: 20%; left-margin: 10%;"> <div id="playlists-tab" onclick="showPlaylists();" class="tab selected">Playlists</div> <div id="content-tab" onclick="showContent();" class="tab">Content</div> <div id="content-and-playlists-inner" style="margin-top: 5pt; margin-bottom: 5pt;"></div> </div> + +</div> + </body> </html> 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 @@ <style> div.nav { - float: left; margin-right: 4em; + height: 100vh; + width: 10%; + position: fixed; } li.nav { border: 1px solid rgba(57, 61, 65, 0.50); |
