Add go-to-position dialogue when clicking on preview timecode.
[dcpomatic.git] / src / wx / film_viewer.cc
1 /*
2     Copyright (C) 2012-2016 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 /** @file  src/film_viewer.cc
22  *  @brief A wx widget to view a preview of a Film.
23  */
24
25 #include "lib/film.h"
26 #include "lib/ratio.h"
27 #include "lib/util.h"
28 #include "lib/job_manager.h"
29 #include "lib/image.h"
30 #include "lib/exceptions.h"
31 #include "lib/examine_content_job.h"
32 #include "lib/filter.h"
33 #include "lib/player.h"
34 #include "lib/player_video.h"
35 #include "lib/video_content.h"
36 #include "lib/video_decoder.h"
37 #include "lib/timer.h"
38 #include "lib/log.h"
39 #include "film_viewer.h"
40 #include "playhead_to_timecode_dialog.h"
41 #include "wx_util.h"
42 extern "C" {
43 #include <libavutil/pixfmt.h>
44 }
45 #include <dcp/exceptions.h>
46 #include <wx/tglbtn.h>
47 #include <iostream>
48 #include <iomanip>
49
50 using std::string;
51 using std::pair;
52 using std::min;
53 using std::max;
54 using std::cout;
55 using std::list;
56 using std::bad_alloc;
57 using std::make_pair;
58 using std::exception;
59 using boost::shared_ptr;
60 using boost::dynamic_pointer_cast;
61 using boost::weak_ptr;
62 using boost::optional;
63 using dcp::Size;
64
65 FilmViewer::FilmViewer (wxWindow* p)
66         : wxPanel (p)
67         , _panel (new wxPanel (this))
68         , _outline_content (new wxCheckBox (this, wxID_ANY, _("Outline content")))
69         , _left_eye (new wxRadioButton (this, wxID_ANY, _("Left eye"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP))
70         , _right_eye (new wxRadioButton (this, wxID_ANY, _("Right eye")))
71         , _slider (new wxSlider (this, wxID_ANY, 0, 0, 4096))
72         , _back_button (new wxButton (this, wxID_ANY, wxT("<")))
73         , _forward_button (new wxButton (this, wxID_ANY, wxT(">")))
74         , _frame_number (new wxStaticText (this, wxID_ANY, wxT("")))
75         , _timecode (new wxStaticText (this, wxID_ANY, wxT("")))
76         , _play_button (new wxToggleButton (this, wxID_ANY, _("Play")))
77         , _coalesce_player_changes (false)
78         , _pending_player_change (false)
79         , _last_get_accurate (true)
80 {
81 #ifndef __WXOSX__
82         _panel->SetDoubleBuffered (true);
83 #endif
84
85         _panel->SetBackgroundStyle (wxBG_STYLE_PAINT);
86
87         _v_sizer = new wxBoxSizer (wxVERTICAL);
88         SetSizer (_v_sizer);
89
90         _v_sizer->Add (_panel, 1, wxEXPAND);
91
92         wxBoxSizer* view_options = new wxBoxSizer (wxHORIZONTAL);
93         view_options->Add (_outline_content, 0, wxRIGHT, DCPOMATIC_SIZER_GAP);
94         view_options->Add (_left_eye, 0, wxLEFT | wxRIGHT, DCPOMATIC_SIZER_GAP);
95         view_options->Add (_right_eye, 0, wxLEFT | wxRIGHT, DCPOMATIC_SIZER_GAP);
96         _v_sizer->Add (view_options, 0, wxALL, DCPOMATIC_SIZER_GAP);
97
98         wxBoxSizer* h_sizer = new wxBoxSizer (wxHORIZONTAL);
99
100         wxBoxSizer* time_sizer = new wxBoxSizer (wxVERTICAL);
101         time_sizer->Add (_frame_number, 0, wxEXPAND);
102         time_sizer->Add (_timecode, 0, wxEXPAND);
103
104         h_sizer->Add (_back_button, 0, wxALL, 2);
105         h_sizer->Add (time_sizer, 0, wxEXPAND);
106         h_sizer->Add (_forward_button, 0, wxALL, 2);
107         h_sizer->Add (_play_button, 0, wxEXPAND);
108         h_sizer->Add (_slider, 1, wxEXPAND);
109
110         _v_sizer->Add (h_sizer, 0, wxEXPAND | wxALL, 6);
111
112         _frame_number->SetMinSize (wxSize (84, -1));
113         _back_button->SetMinSize (wxSize (32, -1));
114         _forward_button->SetMinSize (wxSize (32, -1));
115
116         _panel->Bind          (wxEVT_PAINT,                        boost::bind (&FilmViewer::paint_panel,     this));
117         _panel->Bind          (wxEVT_SIZE,                         boost::bind (&FilmViewer::panel_sized,     this, _1));
118         _outline_content->Bind(wxEVT_COMMAND_CHECKBOX_CLICKED,     boost::bind (&FilmViewer::refresh_panel,   this));
119         _left_eye->Bind       (wxEVT_COMMAND_RADIOBUTTON_SELECTED, boost::bind (&FilmViewer::refresh,         this));
120         _right_eye->Bind      (wxEVT_COMMAND_RADIOBUTTON_SELECTED, boost::bind (&FilmViewer::refresh,         this));
121         _slider->Bind         (wxEVT_SCROLL_THUMBTRACK,            boost::bind (&FilmViewer::slider_moved,    this));
122         _slider->Bind         (wxEVT_SCROLL_PAGEUP,                boost::bind (&FilmViewer::slider_moved,    this));
123         _slider->Bind         (wxEVT_SCROLL_PAGEDOWN,              boost::bind (&FilmViewer::slider_moved,    this));
124         _play_button->Bind    (wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, boost::bind (&FilmViewer::play_clicked,    this));
125         _timer.Bind           (wxEVT_TIMER,                        boost::bind (&FilmViewer::timer,           this));
126         _back_button->Bind    (wxEVT_LEFT_DOWN,                    boost::bind (&FilmViewer::back_clicked,    this, _1));
127         _forward_button->Bind (wxEVT_LEFT_DOWN,                    boost::bind (&FilmViewer::forward_clicked, this, _1));
128         _timecode->Bind       (wxEVT_LEFT_DOWN,                    boost::bind (&FilmViewer::timecode_clicked, this));
129
130         set_film (shared_ptr<Film> ());
131
132         JobManager::instance()->ActiveJobsChanged.connect (
133                 bind (&FilmViewer::active_jobs_changed, this, _2)
134                 );
135
136         setup_sensitivity ();
137 }
138
139 void
140 FilmViewer::set_film (shared_ptr<Film> film)
141 {
142         if (_film == film) {
143                 return;
144         }
145
146         _film = film;
147
148         _frame.reset ();
149
150         update_position_slider ();
151         update_position_label ();
152
153         if (!_film) {
154                 return;
155         }
156
157         try {
158                 _player.reset (new Player (_film, _film->playlist ()));
159                 _player->set_fast ();
160         } catch (bad_alloc) {
161                 error_dialog (this, _("There is not enough free memory to do that."));
162                 _film.reset ();
163                 return;
164         }
165
166         /* Always burn in subtitles, even if content is set not to, otherwise we won't see them
167            in the preview.
168         */
169         _player->set_always_burn_subtitles (true);
170         _player->set_ignore_audio ();
171         _player->set_play_referenced ();
172
173         _film_connection = _film->Changed.connect (boost::bind (&FilmViewer::film_changed, this, _1));
174
175         _player_connection = _player->Changed.connect (boost::bind (&FilmViewer::player_changed, this, _1));
176
177         calculate_sizes ();
178         refresh ();
179
180         setup_sensitivity ();
181 }
182
183 void
184 FilmViewer::refresh_panel ()
185 {
186         _panel->Refresh ();
187         _panel->Update ();
188 }
189
190 void
191 FilmViewer::get (DCPTime p, bool accurate)
192 {
193         if (!_player) {
194                 return;
195         }
196
197         list<shared_ptr<PlayerVideo> > all_pv;
198         try {
199                 all_pv = _player->get_video (p, accurate);
200         } catch (exception& e) {
201                 error_dialog (this, wxString::Format (_("Could not get video for view (%s)"), std_to_wx(e.what()).data()));
202         }
203
204         if (!all_pv.empty ()) {
205                 try {
206                         shared_ptr<PlayerVideo> pv;
207                         if (all_pv.size() == 2) {
208                                 /* We have 3D; choose the correct eye */
209                                 if (_left_eye->GetValue()) {
210                                         if (all_pv.front()->eyes() == EYES_LEFT) {
211                                                 pv = all_pv.front();
212                                         } else {
213                                                 pv = all_pv.back();
214                                         }
215                                 } else {
216                                         if (all_pv.front()->eyes() == EYES_RIGHT) {
217                                                 pv = all_pv.front();
218                                         } else {
219                                                 pv = all_pv.back();
220                                         }
221                                 }
222                         } else {
223                                 /* 2D; no choice to make */
224                                 pv = all_pv.front ();
225                         }
226
227                         /* In an ideal world, what we would do here is:
228                          *
229                          * 1. convert to XYZ exactly as we do in the DCP creation path.
230                          * 2. convert back to RGB for the preview display, compensating
231                          *    for the monitor etc. etc.
232                          *
233                          * but this is inefficient if the source is RGB.  Since we don't
234                          * (currently) care too much about the precise accuracy of the preview's
235                          * colour mapping (and we care more about its speed) we try to short-
236                          * circuit this "ideal" situation in some cases.
237                          *
238                          * The content's specified colour conversion indicates the colourspace
239                          * which the content is in (according to the user).
240                          *
241                          * PlayerVideo::image (bound to PlayerVideo::always_rgb) will take the source
242                          * image and convert it (from whatever the user has said it is) to RGB.
243                          */
244
245                         _frame = pv->image (
246                                 bind (&Log::dcp_log, _film->log().get(), _1, _2),
247                                 bind (&PlayerVideo::always_rgb, _1),
248                                 false, true
249                                 );
250
251                         ImageChanged (pv);
252
253                         _position = pv->time ();
254                         _inter_position = pv->inter_position ();
255                         _inter_size = pv->inter_size ();
256                 } catch (dcp::DCPReadError& e) {
257                         /* This can happen on the following sequence of events:
258                          * - load encrypted DCP
259                          * - add KDM
260                          * - DCP is examined again, which sets its "playable" flag to 1
261                          * - as a side effect of the exam, the viewer is updated using the old pieces
262                          * - the DCPDecoder in the old piece gives us an encrypted frame
263                          * - then, the pieces are re-made (but too late).
264                          *
265                          * I hope there's a better way to handle this ...
266                          */
267                         _frame.reset ();
268                         _position = p;
269                 }
270         } else {
271                 _frame.reset ();
272                 _position = p;
273         }
274
275         refresh_panel ();
276
277         _last_get_accurate = accurate;
278 }
279
280 void
281 FilmViewer::timer ()
282 {
283         DCPTime const frame = DCPTime::from_frames (1, _film->video_frame_rate ());
284
285         if ((_position + frame) >= _film->length ()) {
286                 _play_button->SetValue (false);
287                 check_play_state ();
288         } else {
289                 get (_position + frame, true);
290         }
291
292         update_position_label ();
293         update_position_slider ();
294 }
295
296 void
297 FilmViewer::paint_panel ()
298 {
299         wxPaintDC dc (_panel);
300
301         if (!_frame || !_film || !_out_size.width || !_out_size.height) {
302                 dc.Clear ();
303                 return;
304         }
305
306         wxImage frame (_out_size.width, _out_size.height, _frame->data()[0], true);
307         wxBitmap frame_bitmap (frame);
308         dc.DrawBitmap (frame_bitmap, 0, 0);
309
310         if (_out_size.width < _panel_size.width) {
311                 wxPen p (GetBackgroundColour ());
312                 wxBrush b (GetBackgroundColour ());
313                 dc.SetPen (p);
314                 dc.SetBrush (b);
315                 dc.DrawRectangle (_out_size.width, 0, _panel_size.width - _out_size.width, _panel_size.height);
316         }
317
318         if (_out_size.height < _panel_size.height) {
319                 wxPen p (GetBackgroundColour ());
320                 wxBrush b (GetBackgroundColour ());
321                 dc.SetPen (p);
322                 dc.SetBrush (b);
323                 dc.DrawRectangle (0, _out_size.height, _panel_size.width, _panel_size.height - _out_size.height);
324         }
325
326         if (_outline_content->GetValue ()) {
327                 wxPen p (wxColour (255, 0, 0), 2);
328                 dc.SetPen (p);
329                 dc.SetBrush (*wxTRANSPARENT_BRUSH);
330                 dc.DrawRectangle (_inter_position.x, _inter_position.y, _inter_size.width, _inter_size.height);
331         }
332 }
333
334 void
335 FilmViewer::slider_moved ()
336 {
337         if (!_film) {
338                 return;
339         }
340
341         DCPTime t (_slider->GetValue() * _film->length().get() / 4096);
342         /* Ensure that we hit the end of the film at the end of the slider */
343         if (t >= _film->length ()) {
344                 t = _film->length() - DCPTime::from_frames (1, _film->video_frame_rate ());
345         }
346         get (t, false);
347         update_position_label ();
348 }
349
350 void
351 FilmViewer::panel_sized (wxSizeEvent& ev)
352 {
353         _panel_size.width = ev.GetSize().GetWidth();
354         _panel_size.height = ev.GetSize().GetHeight();
355
356         calculate_sizes ();
357         refresh ();
358         update_position_label ();
359         update_position_slider ();
360 }
361
362 void
363 FilmViewer::calculate_sizes ()
364 {
365         if (!_film || !_player) {
366                 return;
367         }
368
369         Ratio const * container = _film->container ();
370
371         float const panel_ratio = _panel_size.ratio ();
372         float const film_ratio = container ? container->ratio () : 1.78;
373
374         if (panel_ratio < film_ratio) {
375                 /* panel is less widscreen than the film; clamp width */
376                 _out_size.width = _panel_size.width;
377                 _out_size.height = lrintf (_out_size.width / film_ratio);
378         } else {
379                 /* panel is more widescreen than the film; clamp height */
380                 _out_size.height = _panel_size.height;
381                 _out_size.width = lrintf (_out_size.height * film_ratio);
382         }
383
384         /* Catch silly values */
385         _out_size.width = max (64, _out_size.width);
386         _out_size.height = max (64, _out_size.height);
387
388         _player->set_video_container_size (_out_size);
389 }
390
391 void
392 FilmViewer::play_clicked ()
393 {
394         check_play_state ();
395 }
396
397 void
398 FilmViewer::check_play_state ()
399 {
400         if (!_film || _film->video_frame_rate() == 0) {
401                 return;
402         }
403
404         if (_play_button->GetValue()) {
405                 _timer.Start (1000 / _film->video_frame_rate());
406         } else {
407                 _timer.Stop ();
408         }
409 }
410
411 void
412 FilmViewer::update_position_slider ()
413 {
414         if (!_film) {
415                 _slider->SetValue (0);
416                 return;
417         }
418
419         DCPTime const len = _film->length ();
420
421         if (len.get ()) {
422                 int const new_slider_position = 4096 * _position.get() / len.get();
423                 if (new_slider_position != _slider->GetValue()) {
424                         _slider->SetValue (new_slider_position);
425                 }
426         }
427 }
428
429 void
430 FilmViewer::update_position_label ()
431 {
432         if (!_film) {
433                 _frame_number->SetLabel ("0");
434                 _timecode->SetLabel ("0:0:0.0");
435                 return;
436         }
437
438         double const fps = _film->video_frame_rate ();
439         /* Count frame number from 1 ... not sure if this is the best idea */
440         _frame_number->SetLabel (wxString::Format (wxT("%ld"), lrint (_position.seconds() * fps) + 1));
441         _timecode->SetLabel (time_to_timecode (_position, fps));
442 }
443
444 void
445 FilmViewer::active_jobs_changed (optional<string> j)
446 {
447         /* examine content is the only job which stops the viewer working */
448         bool const a = !j || *j != "examine_content";
449         _slider->Enable (a);
450         _play_button->Enable (a);
451 }
452
453 DCPTime
454 FilmViewer::nudge_amount (wxMouseEvent& ev)
455 {
456         DCPTime amount = DCPTime::from_frames (1, _film->video_frame_rate ());
457
458         if (ev.ShiftDown() && !ev.ControlDown()) {
459                 amount = DCPTime::from_seconds (1);
460         } else if (!ev.ShiftDown() && ev.ControlDown()) {
461                 amount = DCPTime::from_seconds (10);
462         } else if (ev.ShiftDown() && ev.ControlDown()) {
463                 amount = DCPTime::from_seconds (60);
464         }
465
466         return amount;
467 }
468
469 void
470 FilmViewer::go_to (DCPTime t)
471 {
472         if (t < DCPTime ()) {
473                 t = DCPTime ();
474         }
475
476         if (t >= _film->length ()) {
477                 t = _film->length ();
478         }
479
480         get (t, true);
481         update_position_label ();
482         update_position_slider ();
483 }
484
485 void
486 FilmViewer::back_clicked (wxMouseEvent& ev)
487 {
488         go_to (_position - nudge_amount (ev));
489         ev.Skip ();
490 }
491
492 void
493 FilmViewer::forward_clicked (wxMouseEvent& ev)
494 {
495         go_to (_position + nudge_amount (ev));
496         ev.Skip ();
497 }
498
499 void
500 FilmViewer::player_changed (bool frequent)
501 {
502         if (frequent) {
503                 return;
504         }
505
506         if (_coalesce_player_changes) {
507                 _pending_player_change = true;
508                 return;
509         }
510
511         calculate_sizes ();
512         refresh ();
513         update_position_label ();
514         update_position_slider ();
515 }
516
517 void
518 FilmViewer::setup_sensitivity ()
519 {
520         bool const c = _film && !_film->content().empty ();
521
522         _slider->Enable (c);
523         _back_button->Enable (c);
524         _forward_button->Enable (c);
525         _play_button->Enable (c);
526         _outline_content->Enable (c);
527         _frame_number->Enable (c);
528         _timecode->Enable (c);
529
530         _left_eye->Enable (c && _film->three_d ());
531         _right_eye->Enable (c && _film->three_d ());
532 }
533
534 void
535 FilmViewer::film_changed (Film::Property p)
536 {
537         if (p == Film::CONTENT || p == Film::THREE_D) {
538                 setup_sensitivity ();
539         }
540 }
541
542 /** Re-get the current frame */
543 void
544 FilmViewer::refresh ()
545 {
546         get (_position, _last_get_accurate);
547 }
548
549 void
550 FilmViewer::set_position (DCPTime p)
551 {
552         _position = p;
553         get (_position, true);
554         update_position_label ();
555         update_position_slider ();
556 }
557
558 void
559 FilmViewer::set_coalesce_player_changes (bool c)
560 {
561         _coalesce_player_changes = c;
562
563         if (c) {
564                 _pending_player_change = false;
565         } else {
566                 if (_pending_player_change) {
567                         player_changed (false);
568                 }
569         }
570 }
571
572 void
573 FilmViewer::timecode_clicked ()
574 {
575         PlayheadToTimecodeDialog* dialog = new PlayheadToTimecodeDialog (this, _film->video_frame_rate ());
576         if (dialog->ShowModal() == wxID_OK) {
577                 go_to (dialog->get ());
578         }
579         dialog->Destroy ();
580 }