Supporters update.
[dcpomatic.git] / src / wx / film_viewer.cc
1 /*
2     Copyright (C) 2012-2021 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 /** @file  src/film_viewer.cc
23  *  @brief A wx widget to view a preview of a Film.
24  */
25
26
27 #include "closed_captions_dialog.h"
28 #include "film_viewer.h"
29 #include "gl_video_view.h"
30 #include "nag_dialog.h"
31 #include "playhead_to_frame_dialog.h"
32 #include "playhead_to_timecode_dialog.h"
33 #include "simple_video_view.h"
34 #include "wx_util.h"
35 #include "lib/butler.h"
36 #include "lib/compose.hpp"
37 #include "lib/config.h"
38 #include "lib/dcpomatic_log.h"
39 #include "lib/examine_content_job.h"
40 #include "lib/exceptions.h"
41 #include "lib/film.h"
42 #include "lib/filter.h"
43 #include "lib/image.h"
44 #include "lib/job_manager.h"
45 #include "lib/log.h"
46 #include "lib/player.h"
47 #include "lib/player_video.h"
48 #include "lib/ratio.h"
49 #include "lib/text_content.h"
50 #include "lib/timer.h"
51 #include "lib/util.h"
52 #include "lib/video_content.h"
53 #include "lib/video_decoder.h"
54 #include <dcp/exceptions.h>
55 #include <dcp/warnings.h>
56 extern "C" {
57 #include <libavutil/pixfmt.h>
58 }
59 LIBDCP_DISABLE_WARNINGS
60 #include <wx/tglbtn.h>
61 LIBDCP_ENABLE_WARNINGS
62 #include <iomanip>
63
64
65 using std::bad_alloc;
66 using std::dynamic_pointer_cast;
67 using std::make_shared;
68 using std::max;
69 using std::shared_ptr;
70 using std::string;
71 using std::vector;
72 using boost::optional;
73 #if BOOST_VERSION >= 106100
74 using namespace boost::placeholders;
75 #endif
76 using dcp::Size;
77 using namespace dcpomatic;
78
79
80 static
81 int
82 rtaudio_callback (void* out, void *, unsigned int frames, double, RtAudioStreamStatus, void* data)
83 {
84         return reinterpret_cast<FilmViewer*>(data)->audio_callback (out, frames);
85 }
86
87
88 FilmViewer::FilmViewer (wxWindow* p)
89 #if (RTAUDIO_VERSION_MAJOR >= 6)
90         : _audio(DCPOMATIC_RTAUDIO_API, boost::bind(&FilmViewer::rtaudio_error_callback, this, _2))
91 #else
92         : _audio (DCPOMATIC_RTAUDIO_API)
93 #endif
94         , _closed_captions_dialog (new ClosedCaptionsDialog(p, this))
95 {
96 #if wxCHECK_VERSION(3, 1, 0)
97         switch (Config::instance()->video_view_type()) {
98         case Config::VIDEO_VIEW_OPENGL:
99                 _video_view = std::make_shared<GLVideoView>(this, p);
100                 break;
101         case Config::VIDEO_VIEW_SIMPLE:
102                 _video_view = std::make_shared<SimpleVideoView>(this, p);
103                 break;
104         }
105 #else
106         _video_view = std::make_shared<SimpleVideoView>(this, p);
107 #endif
108
109         _video_view->Sized.connect (boost::bind(&FilmViewer::video_view_sized, this));
110         _video_view->TooManyDropped.connect (boost::bind(boost::ref(TooManyDropped)));
111
112         set_film (shared_ptr<Film>());
113
114         _config_changed_connection = Config::instance()->Changed.connect(bind(&FilmViewer::config_changed, this, _1));
115         config_changed (Config::SOUND_OUTPUT);
116 }
117
118
119 FilmViewer::~FilmViewer ()
120 {
121         stop ();
122 }
123
124
125 /** Ask for ::idle_handler() to be called next time we are idle */
126 void
127 FilmViewer::request_idle_display_next_frame ()
128 {
129         if (_idle_get) {
130                 return;
131         }
132
133         _idle_get = true;
134         DCPOMATIC_ASSERT (signal_manager);
135         signal_manager->when_idle (boost::bind(&FilmViewer::idle_handler, this));
136 }
137
138
139 void
140 FilmViewer::idle_handler ()
141 {
142         if (!_idle_get) {
143                 return;
144         }
145
146         if (_video_view->display_next_frame(true) == VideoView::AGAIN) {
147                 /* get() could not complete quickly so we'll try again later */
148                 signal_manager->when_idle (boost::bind(&FilmViewer::idle_handler, this));
149         } else {
150                 _idle_get = false;
151         }
152 }
153
154
155 void
156 FilmViewer::set_film (shared_ptr<Film> film)
157 {
158         if (_film == film) {
159                 return;
160         }
161
162         _film = film;
163
164         _video_view->clear ();
165         _closed_captions_dialog->clear ();
166
167         destroy_butler();
168
169         if (!_film) {
170                 _player = boost::none;
171                 resume();
172                 _video_view->update ();
173                 return;
174         }
175
176         try {
177                 _player.emplace(_film, _optimise_for_j2k ? Image::Alignment::COMPACT : Image::Alignment::PADDED);
178                 _player->set_fast ();
179                 if (_dcp_decode_reduction) {
180                         _player->set_dcp_decode_reduction (_dcp_decode_reduction);
181                 }
182         } catch (bad_alloc &) {
183                 error_dialog (_video_view->get(), _("There is not enough free memory to do that."));
184                 _film.reset ();
185                 resume();
186                 return;
187         }
188
189         _player->set_always_burn_open_subtitles ();
190         _player->set_play_referenced ();
191
192         _film->Change.connect (boost::bind (&FilmViewer::film_change, this, _1, _2));
193         _film->LengthChange.connect (boost::bind(&FilmViewer::film_length_change, this));
194         _player->Change.connect (boost::bind (&FilmViewer::player_change, this, _1, _2, _3));
195
196         film_change(ChangeType::DONE, FilmProperty::VIDEO_FRAME_RATE);
197         film_change(ChangeType::DONE, FilmProperty::THREE_D);
198         film_length_change ();
199
200         /* Keep about 1 second's worth of history samples */
201         _latency_history_count = _film->audio_frame_rate() / _audio_block_size;
202
203         _closed_captions_dialog->update_tracks (_film);
204
205         create_butler();
206
207         calculate_sizes ();
208         slow_refresh ();
209 }
210
211
212 void
213 FilmViewer::destroy_butler()
214 {
215         suspend ();
216         _butler.reset ();
217 }
218
219
220 void
221 FilmViewer::destroy_and_maybe_create_butler()
222 {
223         destroy_butler();
224
225         if (!_film) {
226                 resume ();
227                 return;
228         }
229
230         create_butler();
231 }
232
233
234 void
235 FilmViewer::create_butler()
236 {
237 #if wxCHECK_VERSION(3, 1, 0)
238         auto const j2k_gl_optimised = dynamic_pointer_cast<GLVideoView>(_video_view) && _optimise_for_j2k;
239 #else
240         auto const j2k_gl_optimised = false;
241 #endif
242
243         DCPOMATIC_ASSERT(_player);
244
245         _butler = std::make_shared<Butler>(
246                 _film,
247                 *_player,
248                 Config::instance()->audio_mapping(_audio_channels),
249                 _audio_channels,
250                 boost::bind(&PlayerVideo::force, AV_PIX_FMT_RGB24),
251                 VideoRange::FULL,
252                 j2k_gl_optimised ? Image::Alignment::COMPACT : Image::Alignment::PADDED,
253                 true,
254                 j2k_gl_optimised,
255                 (Config::instance()->sound() && _audio.isStreamOpen()) ? Butler::Audio::ENABLED : Butler::Audio::DISABLED
256                 );
257
258         _closed_captions_dialog->set_butler (_butler);
259
260         resume ();
261 }
262
263
264 void
265 FilmViewer::set_outline_content (bool o)
266 {
267         _outline_content = o;
268         _video_view->update ();
269 }
270
271
272 void
273 FilmViewer::set_outline_subtitles (optional<dcpomatic::Rect<double>> rect)
274 {
275         _outline_subtitles = rect;
276         _video_view->update ();
277 }
278
279
280 void
281 FilmViewer::set_eyes (Eyes e)
282 {
283         _video_view->set_eyes (e);
284         slow_refresh ();
285 }
286
287
288 void
289 FilmViewer::video_view_sized ()
290 {
291         calculate_sizes ();
292         if (!quick_refresh()) {
293                 slow_refresh ();
294         }
295 }
296
297
298 void
299 FilmViewer::calculate_sizes ()
300 {
301         if (!_film || !_player) {
302                 return;
303         }
304
305         auto const container = _film->container ();
306
307         auto const scale = dpi_scale_factor (_video_view->get());
308         int const video_view_width = std::round(_video_view->get()->GetSize().x * scale);
309         int const video_view_height = std::round(_video_view->get()->GetSize().y * scale);
310
311         auto const view_ratio = float(video_view_width) / video_view_height;
312         auto const film_ratio = container ? container->ratio () : 1.78;
313
314         dcp::Size out_size;
315         if (view_ratio < film_ratio) {
316                 /* panel is less widscreen than the film; clamp width */
317                 out_size.width = video_view_width;
318                 out_size.height = lrintf (out_size.width / film_ratio);
319         } else {
320                 /* panel is more widescreen than the film; clamp height */
321                 out_size.height = video_view_height;
322                 out_size.width = lrintf (out_size.height * film_ratio);
323         }
324
325         /* Catch silly values */
326         out_size.width = max (64, out_size.width);
327         out_size.height = max (64, out_size.height);
328
329         /* Make sure the video container sizes are always a multiple of 2 so that
330          * we don't get gaps with subsampled sources (e.g. YUV420)
331          */
332         if (out_size.width % 2) {
333                 out_size.width++;
334         }
335         if (out_size.height % 2) {
336                 out_size.height++;
337         }
338
339         _player->set_video_container_size (out_size);
340 }
341
342
343 void
344 FilmViewer::suspend ()
345 {
346         ++_suspended;
347         if (_audio.isStreamRunning()) {
348                 _audio.abortStream();
349         }
350 }
351
352
353 void
354 FilmViewer::start_audio_stream_if_open ()
355 {
356         if (_audio.isStreamOpen()) {
357                 _audio.setStreamTime (_video_view->position().seconds());
358 #if (RTAUDIO_VERSION_MAJOR >= 6)
359                 if (_audio.startStream() != RTAUDIO_NO_ERROR) {
360                         _audio_channels = 0;
361                         error_dialog(
362                                 _video_view->get(),
363                                 _("There was a problem starting audio playback.  Please try another audio output device in Preferences."), std_to_wx(last_rtaudio_error())
364                                 );
365                 }
366 #else
367                 try {
368                         _audio.startStream ();
369                 } catch (RtAudioError& e) {
370                         _audio_channels = 0;
371                         error_dialog (
372                                 _video_view->get(),
373                                 _("There was a problem starting audio playback.  Please try another audio output device in Preferences."), std_to_wx(e.what())
374                                 );
375                 }
376 #endif
377         }
378 }
379
380
381 void
382 FilmViewer::resume ()
383 {
384         DCPOMATIC_ASSERT (_suspended > 0);
385         --_suspended;
386         if (_playing && !_suspended) {
387                 start_audio_stream_if_open ();
388                 _video_view->start ();
389         }
390 }
391
392
393 void
394 FilmViewer::start ()
395 {
396         if (!_film) {
397                 return;
398         }
399
400         auto v = PlaybackPermitted ();
401         if (v && !*v) {
402                 /* Computer says no */
403                 return;
404         }
405
406         /* We are about to set up the audio stream from the position of the video view.
407            If there is `lazy' seek in progress we need to wait for it to go through so that
408            _video_view->position() gives us a sensible answer.
409          */
410         while (_idle_get) {
411                 idle_handler ();
412         }
413
414         /* Take the video view's idea of position as our `playhead' and start the
415            audio stream (which is the timing reference) there.
416          */
417         start_audio_stream_if_open ();
418
419         _playing = true;
420         /* Calling start() below may directly result in Stopped being emitted, and if that
421          * happens we want it to come after the Started signal, so do that first.
422          */
423         Started ();
424         _video_view->start ();
425 }
426
427
428 bool
429 FilmViewer::stop ()
430 {
431         if (_audio.isStreamRunning()) {
432                 /* stop stream and discard any remaining queued samples */
433                 _audio.abortStream ();
434         }
435
436         if (!_playing) {
437                 return false;
438         }
439
440         _playing = false;
441         _video_view->stop ();
442         Stopped ();
443
444         _video_view->rethrow ();
445         return true;
446 }
447
448
449 void
450 FilmViewer::player_change (ChangeType type, int property, bool frequent)
451 {
452         if (type != ChangeType::DONE || frequent) {
453                 return;
454         }
455
456         if (_coalesce_player_changes) {
457                 _pending_player_changes.push_back (property);
458                 return;
459         }
460
461         player_change ({property});
462 }
463
464
465 void
466 FilmViewer::player_change (vector<int> properties)
467 {
468         calculate_sizes ();
469
470         bool try_quick_refresh = false;
471         bool update_ccap_tracks = false;
472
473         for (auto i: properties) {
474                 if (
475                         i == VideoContentProperty::CROP ||
476                         i == VideoContentProperty::CUSTOM_RATIO ||
477                         i == VideoContentProperty::CUSTOM_SIZE ||
478                         i == VideoContentProperty::FADE_IN ||
479                         i == VideoContentProperty::FADE_OUT ||
480                         i == VideoContentProperty::COLOUR_CONVERSION ||
481                         i == PlayerProperty::VIDEO_CONTAINER_SIZE ||
482                         i == PlayerProperty::FILM_CONTAINER
483                    ) {
484                         try_quick_refresh = true;
485                 }
486
487                 if (i == TextContentProperty::USE || i == TextContentProperty::TYPE || i == TextContentProperty::DCP_TRACK) {
488                         update_ccap_tracks = true;
489                 }
490         }
491
492         if (!try_quick_refresh || !quick_refresh()) {
493                 slow_refresh ();
494         }
495
496         if (update_ccap_tracks) {
497                 _closed_captions_dialog->update_tracks (_film);
498         }
499 }
500
501
502 void
503 FilmViewer::film_change(ChangeType type, FilmProperty p)
504 {
505         if (type != ChangeType::DONE) {
506                 return;
507         }
508
509         if (p == FilmProperty::AUDIO_CHANNELS) {
510                 destroy_and_maybe_create_butler();
511         } else if (p == FilmProperty::VIDEO_FRAME_RATE) {
512                 _video_view->set_video_frame_rate (_film->video_frame_rate());
513         } else if (p == FilmProperty::THREE_D) {
514                 _video_view->set_three_d (_film->three_d());
515         } else if (p == FilmProperty::CONTENT) {
516                 _closed_captions_dialog->update_tracks (_film);
517         }
518 }
519
520
521 void
522 FilmViewer::film_length_change ()
523 {
524         _video_view->set_length (_film->length());
525 }
526
527
528 /** Re-get the current frame slowly by seeking */
529 void
530 FilmViewer::slow_refresh ()
531 {
532         seek (_video_view->position(), true);
533 }
534
535
536 /** Try to re-get the current frame quickly by resetting the metadata
537  *  in the PlayerVideo that we used last time.
538  *  @return true if this was possible, false if not.
539  */
540 bool
541 FilmViewer::quick_refresh ()
542 {
543         if (!_video_view || !_film || !_player) {
544                 return true;
545         }
546         return _video_view->reset_metadata (_film, _player->video_container_size());
547 }
548
549
550 void
551 FilmViewer::seek (shared_ptr<Content> content, ContentTime t, bool accurate)
552 {
553         DCPOMATIC_ASSERT(_player);
554         auto dt = _player->content_time_to_dcp (content, t);
555         if (dt) {
556                 seek (*dt, accurate);
557         }
558 }
559
560
561 void
562 FilmViewer::set_coalesce_player_changes (bool c)
563 {
564         _coalesce_player_changes = c;
565
566         if (!c) {
567                 player_change (_pending_player_changes);
568                 _pending_player_changes.clear ();
569         }
570 }
571
572
573 void
574 FilmViewer::seek (DCPTime t, bool accurate)
575 {
576         if (!_butler) {
577                 return;
578         }
579
580         if (t < DCPTime()) {
581                 t = DCPTime ();
582         }
583
584         if (t >= _film->length()) {
585                 t = _film->length() - one_video_frame();
586         }
587
588         suspend ();
589
590         _closed_captions_dialog->clear ();
591         _butler->seek (t, accurate);
592
593         if (!_playing) {
594                 /* We're not playing, so let the GUI thread get on and
595                    come back later to get the next frame after the seek.
596                 */
597                 request_idle_display_next_frame ();
598         } else {
599                 /* We're going to start playing again straight away
600                    so wait for the seek to finish.
601                 */
602                 while (_video_view->display_next_frame(false) == VideoView::AGAIN) {}
603         }
604
605         resume ();
606 }
607
608
609 void
610 FilmViewer::config_changed (Config::Property p)
611 {
612         if (p == Config::AUDIO_MAPPING) {
613                 destroy_and_maybe_create_butler();
614                 return;
615         }
616
617         if (p != Config::SOUND && p != Config::SOUND_OUTPUT) {
618                 return;
619         }
620
621         if (_audio.isStreamOpen ()) {
622                 _audio.closeStream ();
623         }
624
625         if (Config::instance()->sound() && _audio.getDeviceCount() > 0) {
626                 optional<unsigned int> chosen_device_id;
627 #if (RTAUDIO_VERSION_MAJOR >= 6)
628                 if (Config::instance()->sound_output()) {
629                         for (auto device_id: _audio.getDeviceIds()) {
630                                 if (_audio.getDeviceInfo(device_id).name == Config::instance()->sound_output().get()) {
631                                         chosen_device_id = device_id;
632                                         break;
633                                 }
634                         }
635                 }
636
637                 if (!chosen_device_id) {
638                         chosen_device_id = _audio.getDefaultOutputDevice();
639                 }
640                 _audio_channels = _audio.getDeviceInfo(*chosen_device_id).outputChannels;
641                 RtAudio::StreamParameters sp;
642                 sp.deviceId = *chosen_device_id;
643                 sp.nChannels = _audio_channels;
644                 sp.firstChannel = 0;
645                 if (_audio.openStream(&sp, 0, RTAUDIO_FLOAT32, 48000, &_audio_block_size, &rtaudio_callback, this) != RTAUDIO_NO_ERROR) {
646                         _audio_channels = 0;
647                         error_dialog(
648                                 _video_view->get(),
649                                 _("Could not set up audio output.  There will be no audio during the preview."), std_to_wx(last_rtaudio_error())
650                                 );
651                 }
652 #else
653                 unsigned int st = 0;
654                 if (Config::instance()->sound_output()) {
655                         while (st < _audio.getDeviceCount()) {
656                                 try {
657                                         if (_audio.getDeviceInfo(st).name == Config::instance()->sound_output().get()) {
658                                                 break;
659                                         }
660                                 } catch (RtAudioError&) {
661                                         /* Something went wrong with that device so we don't want to use it anyway */
662                                 }
663                                 ++st;
664                         }
665                         if (st == _audio.getDeviceCount()) {
666                                 try {
667                                         st = _audio.getDefaultOutputDevice();
668                                 } catch (RtAudioError&) {
669                                         /* Something went wrong with that device so we don't want to use it anyway */
670                                 }
671                         }
672                 } else {
673                         try {
674                                 st = _audio.getDefaultOutputDevice();
675                         } catch (RtAudioError&) {
676                                 /* Something went wrong with that device so we don't want to use it anyway */
677                         }
678                 }
679
680                 try {
681                         _audio_channels = _audio.getDeviceInfo(st).outputChannels;
682                         RtAudio::StreamParameters sp;
683                         sp.deviceId = st;
684                         sp.nChannels = _audio_channels;
685                         sp.firstChannel = 0;
686                         _audio.openStream (&sp, 0, RTAUDIO_FLOAT32, 48000, &_audio_block_size, &rtaudio_callback, this);
687                 } catch (RtAudioError& e) {
688                         _audio_channels = 0;
689                         error_dialog (
690                                 _video_view->get(),
691                                 _("Could not set up audio output.  There will be no audio during the preview."), std_to_wx(e.what())
692                                 );
693                 }
694 #endif
695                 destroy_and_maybe_create_butler();
696
697         } else {
698                 _audio_channels = 0;
699                 destroy_and_maybe_create_butler();
700         }
701 }
702
703
704 DCPTime
705 FilmViewer::uncorrected_time () const
706 {
707         if (_audio.isStreamRunning()) {
708                 return DCPTime::from_seconds (const_cast<RtAudio*>(&_audio)->getStreamTime());
709         }
710
711         return _video_view->position();
712 }
713
714
715 optional<DCPTime>
716 FilmViewer::audio_time () const
717 {
718         if (!_audio.isStreamRunning()) {
719                 return {};
720         }
721
722         return DCPTime::from_seconds (const_cast<RtAudio*>(&_audio)->getStreamTime ()) -
723                 DCPTime::from_frames (average_latency(), _film->audio_frame_rate());
724 }
725
726
727 DCPTime
728 FilmViewer::time () const
729 {
730         return audio_time().get_value_or(_video_view->position());
731 }
732
733
734 int
735 FilmViewer::audio_callback (void* out_p, unsigned int frames)
736 {
737         while (true) {
738                 auto t = _butler->get_audio (Butler::Behaviour::NON_BLOCKING, reinterpret_cast<float*> (out_p), frames);
739                 if (!t || DCPTime(uncorrected_time() - *t) < one_video_frame()) {
740                         /* There was an underrun or this audio is on time; carry on */
741                         break;
742                 }
743                 /* The audio we just got was (very) late; drop it and get some more. */
744         }
745
746         boost::mutex::scoped_lock lm (_latency_history_mutex, boost::try_to_lock);
747         if (lm) {
748                 _latency_history.push_back (_audio.getStreamLatency ());
749                 if (_latency_history.size() > static_cast<size_t> (_latency_history_count)) {
750                         _latency_history.pop_front ();
751                 }
752         }
753
754         return 0;
755 }
756
757
758 Frame
759 FilmViewer::average_latency () const
760 {
761         boost::mutex::scoped_lock lm (_latency_history_mutex);
762         if (_latency_history.empty()) {
763                 return 0;
764         }
765
766         Frame total = 0;
767         for (auto i: _latency_history) {
768                 total += i;
769         }
770
771         return total / _latency_history.size();
772 }
773
774
775 void
776 FilmViewer::set_dcp_decode_reduction (optional<int> reduction)
777 {
778         _dcp_decode_reduction = reduction;
779         if (_player) {
780                 _player->set_dcp_decode_reduction (reduction);
781         }
782 }
783
784
785 optional<int>
786 FilmViewer::dcp_decode_reduction () const
787 {
788         return _dcp_decode_reduction;
789 }
790
791
792 optional<ContentTime>
793 FilmViewer::position_in_content (shared_ptr<const Content> content) const
794 {
795         DCPOMATIC_ASSERT(_player);
796         return _player->dcp_to_content_time (content, position());
797 }
798
799
800 DCPTime
801 FilmViewer::one_video_frame () const
802 {
803         return DCPTime::from_frames (1, _film ? _film->video_frame_rate() : 24);
804 }
805
806
807 /** Open a dialog box showing our film's closed captions */
808 void
809 FilmViewer::show_closed_captions ()
810 {
811         _closed_captions_dialog->Show();
812 }
813
814
815 void
816 FilmViewer::seek_by (DCPTime by, bool accurate)
817 {
818         seek (_video_view->position() + by, accurate);
819 }
820
821
822 void
823 FilmViewer::set_pad_black (bool p)
824 {
825         _pad_black = p;
826 }
827
828
829 /** Called when a player has finished the current film.
830  *  May be called from a non-UI thread.
831  */
832 void
833 FilmViewer::finished ()
834 {
835         emit (boost::bind(&FilmViewer::ui_finished, this));
836 }
837
838
839 /** Called by finished() in the UI thread */
840 void
841 FilmViewer::ui_finished ()
842 {
843         stop ();
844         Finished ();
845 }
846
847
848 int
849 FilmViewer::dropped () const
850 {
851         return _video_view->dropped ();
852 }
853
854
855 int
856 FilmViewer::errored () const
857 {
858         return _video_view->errored ();
859 }
860
861
862 int
863 FilmViewer::gets () const
864 {
865         return _video_view->gets ();
866 }
867
868
869 void
870 FilmViewer::image_changed (shared_ptr<PlayerVideo> pv)
871 {
872         emit (boost::bind(boost::ref(ImageChanged), pv));
873 }
874
875
876 void
877 FilmViewer::set_optimise_for_j2k (bool o)
878 {
879         _optimise_for_j2k = o;
880         _video_view->set_optimise_for_j2k (o);
881 }
882
883
884 void
885 FilmViewer::set_crop_guess (dcpomatic::Rect<float> crop)
886 {
887         if (crop != _crop_guess) {
888                 _crop_guess = crop;
889                 _video_view->update ();
890         }
891 }
892
893
894 void
895 FilmViewer::unset_crop_guess ()
896 {
897         _crop_guess = boost::none;
898         _video_view->update ();
899 }
900
901
902 #if (RTAUDIO_VERSION_MAJOR >= 6)
903 void
904 FilmViewer::rtaudio_error_callback(string const& error)
905 {
906         boost::mutex::scoped_lock lm(_last_rtaudio_error_mutex);
907         _last_rtaudio_error = error;
908 }
909
910
911 string
912 FilmViewer::last_rtaudio_error() const
913 {
914         boost::mutex::scoped_lock lm(_last_rtaudio_error_mutex);
915         return _last_rtaudio_error;
916 }
917 #endif
918