Don't use --target-macos-arm64 any more, since it's not supported.
[dcpomatic.git] / src / wx / playlist_controls.cc
1 /*
2     Copyright (C) 2018-2020 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 #include "content_view.h"
23 #include "dcpomatic_button.h"
24 #include "film_viewer.h"
25 #include "playlist_controls.h"
26 #include "static_text.h"
27 #include "wx_util.h"
28 #include "lib/compose.hpp"
29 #include "lib/constants.h"
30 #include "lib/cross.h"
31 #include "lib/dcp_content.h"
32 #include "lib/ffmpeg_content.h"
33 #include "lib/film.h"
34 #include "lib/internet.h"
35 #include "lib/player_video.h"
36 #include "lib/scoped_temporary.h"
37 #include <dcp/exceptions.h>
38 #include <dcp/raw_convert.h>
39 #include <dcp/warnings.h>
40 LIBDCP_DISABLE_WARNINGS
41 #include <wx/listctrl.h>
42 #include <wx/progdlg.h>
43 LIBDCP_ENABLE_WARNINGS
44
45
46 using std::cout;
47 using std::dynamic_pointer_cast;
48 using std::exception;
49 using std::shared_ptr;
50 using std::sort;
51 using std::string;
52 using boost::optional;
53 using namespace dcpomatic;
54
55
56 PlaylistControls::PlaylistControls(wxWindow* parent, FilmViewer& viewer)
57         : Controls (parent, viewer, false)
58         , _play_button (new Button(this, _("Play")))
59         , _pause_button (new Button(this, _("Pause")))
60         , _stop_button (new Button(this, _("Stop")))
61         , _next_button (new Button(this, "Next"))
62         , _previous_button (new Button(this, "Previous"))
63 {
64         _button_sizer->Add (_previous_button, 0, wxEXPAND);
65         _button_sizer->Add (_play_button, 0, wxEXPAND);
66         _button_sizer->Add (_pause_button, 0, wxEXPAND);
67         _button_sizer->Add (_stop_button, 0, wxEXPAND);
68         _button_sizer->Add (_next_button, 0, wxEXPAND);
69
70         _spl_view = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER);
71         _spl_view->AppendColumn (wxT(""), wxLIST_FORMAT_LEFT, 740);
72
73         auto left_sizer = new wxBoxSizer(wxVERTICAL);
74         auto e_sizer = new wxBoxSizer(wxHORIZONTAL);
75
76         wxFont subheading_font (*wxNORMAL_FONT);
77         subheading_font.SetWeight (wxFONTWEIGHT_BOLD);
78
79         auto spl_header = new wxBoxSizer(wxHORIZONTAL);
80         {
81                 auto m = new StaticText(this, "Playlists");
82                 m->SetFont (subheading_font);
83                 spl_header->Add (m, 1, wxALIGN_CENTER_VERTICAL);
84         }
85         _refresh_spl_view = new Button (this, "Refresh");
86         spl_header->Add (_refresh_spl_view, 0, wxBOTTOM, DCPOMATIC_SIZER_GAP / 2);
87
88         left_sizer->Add (spl_header, 0, wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP);
89         left_sizer->Add (_spl_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP);
90
91         _content_view = new ContentView (this);
92
93         auto content_header = new wxBoxSizer(wxHORIZONTAL);
94         {
95                 auto m = new StaticText(this, "Content");
96                 m->SetFont (subheading_font);
97                 content_header->Add (m, 1, wxALIGN_CENTER_VERTICAL);
98         }
99         _refresh_content_view = new Button (this, "Refresh");
100         content_header->Add (_refresh_content_view, 0, wxBOTTOM, DCPOMATIC_SIZER_GAP / 2);
101
102         left_sizer->Add (content_header, 0, wxTOP | wxLEFT | wxRIGHT | wxEXPAND, DCPOMATIC_SIZER_GAP);
103         left_sizer->Add (_content_view, 1, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, DCPOMATIC_SIZER_GAP);
104
105         _current_spl_view = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_NO_HEADER);
106         _current_spl_view->AppendColumn (wxT(""), wxLIST_FORMAT_LEFT, 500);
107         _current_spl_view->AppendColumn (wxT(""), wxLIST_FORMAT_LEFT, 80);
108         e_sizer->Add (left_sizer, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP);
109         e_sizer->Add (_current_spl_view, 1, wxALL | wxEXPAND, DCPOMATIC_SIZER_GAP);
110
111         _v_sizer->Add (e_sizer, 1, wxEXPAND);
112
113         _play_button->Bind     (wxEVT_BUTTON, boost::bind(&PlaylistControls::play_clicked,  this));
114         _pause_button->Bind    (wxEVT_BUTTON, boost::bind(&PlaylistControls::pause_clicked, this));
115         _stop_button->Bind     (wxEVT_BUTTON, boost::bind(&PlaylistControls::stop_clicked,  this));
116         _next_button->Bind     (wxEVT_BUTTON, boost::bind(&PlaylistControls::next_clicked,  this));
117         _previous_button->Bind (wxEVT_BUTTON, boost::bind(&PlaylistControls::previous_clicked,  this));
118         _spl_view->Bind        (wxEVT_LIST_ITEM_SELECTED,   boost::bind(&PlaylistControls::spl_selection_changed, this));
119         _spl_view->Bind        (wxEVT_LIST_ITEM_DESELECTED, boost::bind(&PlaylistControls::spl_selection_changed, this));
120         _viewer.Finished.connect(boost::bind(&PlaylistControls::viewer_finished, this));
121         _refresh_spl_view->Bind (wxEVT_BUTTON, boost::bind(&PlaylistControls::update_playlist_directory, this));
122         _refresh_content_view->Bind (wxEVT_BUTTON, boost::bind(&ContentView::update, _content_view));
123
124         _content_view->update ();
125         update_playlist_directory ();
126 }
127
128 void
129 PlaylistControls::started ()
130 {
131         Controls::started ();
132         _play_button->Enable (false);
133         _pause_button->Enable (true);
134 }
135
136 /** Called when the viewer finishes a single piece of content, or it is explicitly stopped */
137 void
138 PlaylistControls::stopped ()
139 {
140         Controls::stopped ();
141         _play_button->Enable (true);
142         _pause_button->Enable (false);
143 }
144
145 void
146 PlaylistControls::deselect_playlist ()
147 {
148         long int const selected = _spl_view->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
149         if (selected != -1) {
150                 _selected_playlist = boost::none;
151                 _spl_view->SetItemState (selected, 0, wxLIST_STATE_SELECTED);
152         }
153         ResetFilm(std::make_shared<Film>(optional<boost::filesystem::path>()));
154 }
155
156 void
157 PlaylistControls::play_clicked ()
158 {
159         _viewer.start();
160 }
161
162 void
163 PlaylistControls::setup_sensitivity ()
164 {
165         Controls::setup_sensitivity ();
166         bool const active_job = _active_job && *_active_job != "examine_content";
167         bool const c = _film && !_film->content().empty() && !active_job;
168         _play_button->Enable(c && !_viewer.playing());
169         _pause_button->Enable(_viewer.playing());
170         _spl_view->Enable(!_viewer.playing());
171         _next_button->Enable (can_do_next());
172         _previous_button->Enable (can_do_previous());
173 }
174
175 void
176 PlaylistControls::pause_clicked ()
177 {
178         _viewer.stop();
179 }
180
181 void
182 PlaylistControls::stop_clicked ()
183 {
184         _viewer.stop();
185         _viewer.seek(DCPTime(), true);
186         if (_selected_playlist) {
187                 _selected_playlist_position = 0;
188                 update_current_content ();
189         }
190         deselect_playlist ();
191 }
192
193 bool
194 PlaylistControls::can_do_previous ()
195 {
196         return _selected_playlist && (_selected_playlist_position - 1) >= 0;
197 }
198
199 void
200 PlaylistControls::previous_clicked ()
201 {
202         if (!can_do_previous ()) {
203                 return;
204         }
205
206         _selected_playlist_position--;
207         update_current_content ();
208 }
209
210 bool
211 PlaylistControls::can_do_next ()
212 {
213         return _selected_playlist && (_selected_playlist_position + 1) < int(_playlists[*_selected_playlist].get().size());
214 }
215
216 void
217 PlaylistControls::next_clicked ()
218 {
219         if (!can_do_next ()) {
220                 return;
221         }
222
223         _selected_playlist_position++;
224         update_current_content ();
225 }
226
227
228 void
229 PlaylistControls::add_playlist_to_list (SPL spl)
230 {
231         int const N = _spl_view->GetItemCount();
232
233         wxListItem it;
234         it.SetId(N);
235         it.SetColumn(0);
236         string t = spl.name();
237         if (spl.missing()) {
238                 t += " (content missing)";
239         }
240         it.SetText (std_to_wx(t));
241         _spl_view->InsertItem (it);
242 }
243
244 struct SPLComparator
245 {
246         bool operator() (SPL const & a, SPL const & b) {
247                 return a.name() < b.name();
248         }
249 };
250
251 void
252 PlaylistControls::update_playlist_directory ()
253 {
254         using namespace boost::filesystem;
255
256         _spl_view->DeleteAllItems ();
257         optional<path> dir = Config::instance()->player_playlist_directory();
258         if (!dir) {
259                 return;
260         }
261
262         _playlists.clear ();
263
264         for (directory_iterator i = directory_iterator(*dir); i != directory_iterator(); ++i) {
265                 try {
266                         if (is_regular_file(i->path()) && i->path().extension() == ".xml") {
267                                 SPL spl;
268                                 spl.read (i->path(), _content_view);
269                                 _playlists.push_back (spl);
270                         }
271                 } catch (exception& e) {
272                         /* Never mind */
273                 }
274         }
275
276         sort (_playlists.begin(), _playlists.end(), SPLComparator());
277         for (auto i: _playlists) {
278                 add_playlist_to_list (i);
279         }
280
281         _selected_playlist = boost::none;
282 }
283
284 optional<dcp::EncryptedKDM>
285 PlaylistControls::get_kdm_from_directory (shared_ptr<DCPContent> dcp)
286 {
287         using namespace boost::filesystem;
288         optional<path> kdm_dir = Config::instance()->player_kdm_directory();
289         if (!kdm_dir) {
290                 return optional<dcp::EncryptedKDM>();
291         }
292         for (directory_iterator i = directory_iterator(*kdm_dir); i != directory_iterator(); ++i) {
293                 try {
294                         if (file_size(i->path()) < MAX_KDM_SIZE) {
295                                 dcp::EncryptedKDM kdm (dcp::file_to_string(i->path()));
296                                 if (kdm.cpl_id() == dcp->cpl()) {
297                                         return kdm;
298                                 }
299                         }
300                 } catch (std::exception& e) {
301                         /* Hey well */
302                 }
303         }
304         return optional<dcp::EncryptedKDM>();
305 }
306
307 void
308 PlaylistControls::spl_selection_changed ()
309 {
310         long int selected = _spl_view->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
311         if (selected == -1) {
312                 _current_spl_view->DeleteAllItems ();
313                 _selected_playlist = boost::none;
314                 return;
315         }
316
317         if (_playlists[selected].missing()) {
318                 error_dialog (this, "This playlist cannot be loaded as some content is missing.");
319                 deselect_playlist ();
320                 return;
321         }
322
323         if (_playlists[selected].get().empty()) {
324                 error_dialog (this, "This playlist is empty.");
325                 return;
326         }
327
328         select_playlist (selected, 0);
329 }
330
331 void
332 PlaylistControls::select_playlist (int selected, int position)
333 {
334         wxProgressDialog dialog (_("DCP-o-matic"), "Loading playlist and KDMs");
335
336         for (auto const& i: _playlists[selected].get()) {
337                 dialog.Pulse ();
338                 shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (i.content);
339                 if (dcp && dcp->needs_kdm()) {
340                         optional<dcp::EncryptedKDM> kdm;
341                         kdm = get_kdm_from_directory (dcp);
342                         if (kdm) {
343                                 try {
344                                         dcp->add_kdm (*kdm);
345                                         dcp->examine (_film, shared_ptr<Job>());
346                                 } catch (KDMError& e) {
347                                         error_dialog (this, "Could not load KDM.");
348                                 }
349                         }
350                         if (dcp->needs_kdm()) {
351                                 /* We didn't get a KDM for this */
352                                 error_dialog (this, "This playlist cannot be loaded as a KDM is missing or incorrect.");
353                                 deselect_playlist ();
354                                 return;
355                         }
356                 }
357         }
358
359         _current_spl_view->DeleteAllItems ();
360
361         int N = 0;
362         for (auto i: _playlists[selected].get()) {
363                 wxListItem it;
364                 it.SetId (N);
365                 it.SetColumn (0);
366                 it.SetText (std_to_wx(i.name));
367                 _current_spl_view->InsertItem (it);
368                 ++N;
369         }
370
371         _selected_playlist = selected;
372         _selected_playlist_position = position;
373         dialog.Pulse ();
374         reset_film ();
375         dialog.Pulse ();
376         update_current_content ();
377 }
378
379 void
380 PlaylistControls::reset_film ()
381 {
382         DCPOMATIC_ASSERT (_selected_playlist);
383         shared_ptr<Film> film (new Film(optional<boost::filesystem::path>()));
384         film->add_content (_playlists[*_selected_playlist].get()[_selected_playlist_position].content);
385         ResetFilm (film);
386 }
387
388 void
389 PlaylistControls::config_changed (int property)
390 {
391         Controls::config_changed (property);
392
393         if (property == Config::PLAYER_CONTENT_DIRECTORY) {
394                 _content_view->update ();
395         } else if (property == Config::PLAYER_PLAYLIST_DIRECTORY) {
396                 update_playlist_directory ();
397         }
398 }
399
400
401 void
402 PlaylistControls::update_current_content ()
403 {
404         DCPOMATIC_ASSERT (_selected_playlist);
405
406         wxProgressDialog dialog (_("DCP-o-matic"), "Loading content");
407
408         setup_sensitivity ();
409         dialog.Pulse ();
410         reset_film ();
411 }
412
413 /** One piece of content in our SPL has finished playing */
414 void
415 PlaylistControls::viewer_finished ()
416 {
417         if (!_selected_playlist) {
418                 return;
419         }
420
421         _selected_playlist_position++;
422         if (_selected_playlist_position < int(_playlists[*_selected_playlist].get().size())) {
423                 /* Next piece of content on the SPL */
424                 update_current_content ();
425                 _viewer.start();
426         } else {
427                 /* Finished the whole SPL */
428                 _selected_playlist_position = 0;
429                 ResetFilm(std::make_shared<Film>(optional<boost::filesystem::path>()));
430                 _play_button->Enable (true);
431                 _pause_button->Enable (false);
432         }
433 }
434
435 void
436 PlaylistControls::play ()
437 {
438         play_clicked ();
439 }
440
441 void
442 PlaylistControls::stop ()
443 {
444         stop_clicked ();
445 }