diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/cross.cc | 11 | ||||
| -rw-r--r-- | src/lib/cross.h | 1 | ||||
| -rw-r--r-- | src/lib/film.cc | 9 | ||||
| -rw-r--r-- | src/lib/film.h | 7 | ||||
| -rw-r--r-- | src/lib/playlist.cc | 13 | ||||
| -rw-r--r-- | src/lib/playlist.h | 4 | ||||
| -rw-r--r-- | src/tools/dcpomatic.cc | 9 | ||||
| -rw-r--r-- | src/tools/dcpomatic_player.cc | 160 | ||||
| -rwxr-xr-x | src/tools/stress | 57 | ||||
| -rw-r--r-- | src/tools/wscript | 2 | ||||
| -rw-r--r-- | src/wx/closed_captions_dialog.cc | 19 | ||||
| -rw-r--r-- | src/wx/closed_captions_dialog.h | 5 | ||||
| -rw-r--r-- | src/wx/controls.cc | 26 | ||||
| -rw-r--r-- | src/wx/controls.h | 9 | ||||
| -rw-r--r-- | src/wx/film_viewer.cc | 277 | ||||
| -rw-r--r-- | src/wx/film_viewer.h | 64 | ||||
| -rw-r--r-- | src/wx/gl_video_view.cc | 204 | ||||
| -rw-r--r-- | src/wx/gl_video_view.h | 35 | ||||
| -rw-r--r-- | src/wx/simple_video_view.cc | 134 | ||||
| -rw-r--r-- | src/wx/simple_video_view.h | 9 | ||||
| -rw-r--r-- | src/wx/standard_controls.cc | 16 | ||||
| -rw-r--r-- | src/wx/standard_controls.h | 5 | ||||
| -rw-r--r-- | src/wx/swaroop_controls.cc | 11 | ||||
| -rw-r--r-- | src/wx/swaroop_controls.h | 4 | ||||
| -rw-r--r-- | src/wx/timeline.cc | 8 | ||||
| -rw-r--r-- | src/wx/timeline.h | 4 | ||||
| -rw-r--r-- | src/wx/video_view.cc | 133 | ||||
| -rw-r--r-- | src/wx/video_view.h | 122 | ||||
| -rw-r--r-- | src/wx/wscript | 1 |
29 files changed, 1056 insertions, 303 deletions
diff --git a/src/lib/cross.cc b/src/lib/cross.cc index 8d82f7a51..5d35d5a4b 100644 --- a/src/lib/cross.cc +++ b/src/lib/cross.cc @@ -75,6 +75,17 @@ dcpomatic_sleep_seconds (int s) #endif } +void +dcpomatic_sleep_milliseconds (int ms) +{ +#ifdef DCPOMATIC_POSIX + usleep (ms * 1000); +#endif +#ifdef DCPOMATIC_WINDOWS + Sleep (ms); +#endif +} + /** @return A string of CPU information (model name etc.) */ string cpu_info () diff --git a/src/lib/cross.h b/src/lib/cross.h index 67709463a..584fa2b42 100644 --- a/src/lib/cross.h +++ b/src/lib/cross.h @@ -39,6 +39,7 @@ class Log; struct AVIOContext; void dcpomatic_sleep_seconds (int); +void dcpomatic_sleep_milliseconds (int); extern std::string cpu_info (); extern void run_ffprobe (boost::filesystem::path, boost::filesystem::path); extern std::list<std::pair<std::string, std::string> > mount_info (); diff --git a/src/lib/film.cc b/src/lib/film.cc index 2a50e8c81..90b18ea70 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -167,8 +167,9 @@ Film::Film (optional<boost::filesystem::path> dir) set_isdcf_date_today (); _playlist_change_connection = _playlist->Change.connect (bind (&Film::playlist_change, this, _1)); - _playlist_order_changed_connection = _playlist->OrderChanged.connect (bind (&Film::playlist_order_changed, this)); + _playlist_order_changed_connection = _playlist->OrderChange.connect (bind (&Film::playlist_order_changed, this)); _playlist_content_change_connection = _playlist->ContentChange.connect (bind (&Film::playlist_content_change, this, _1, _2, _3, _4)); + _playlist_length_change_connection = _playlist->LengthChange.connect (bind(&Film::playlist_length_change, this)); if (dir) { /* Make state.directory a complete path without ..s (where possible) @@ -1293,6 +1294,12 @@ Film::playlist_content_change (ChangeType type, weak_ptr<Content> c, int p, bool } void +Film::playlist_length_change () +{ + LengthChange (); +} + +void Film::playlist_change (ChangeType type) { signal_change (type, CONTENT); diff --git a/src/lib/film.h b/src/lib/film.h index 68f8b5334..c72251880 100644 --- a/src/lib/film.h +++ b/src/lib/film.h @@ -390,6 +390,11 @@ public: /** Emitted when some property of our content has changed */ mutable boost::signals2::signal<void (ChangeType, boost::weak_ptr<Content>, int, bool)> ContentChange; + /** Emitted when the film's length might have changed; this is not like a normal + property as its value is derived from the playlist, so it has its own signal. + */ + mutable boost::signals2::signal<void ()> LengthChange; + /** Emitted when we have something important to tell the user */ boost::signals2::signal<void (std::string)> Message; @@ -409,6 +414,7 @@ private: void playlist_change (ChangeType); void playlist_order_changed (); void playlist_content_change (ChangeType type, boost::weak_ptr<Content>, int, bool frequent); + void playlist_length_change (); void maybe_add_content (boost::weak_ptr<Job>, boost::weak_ptr<Content>, bool disable_audio_analysis); void audio_analysis_finished (); void check_settings_consistency (); @@ -486,6 +492,7 @@ private: boost::signals2::scoped_connection _playlist_change_connection; boost::signals2::scoped_connection _playlist_order_changed_connection; boost::signals2::scoped_connection _playlist_content_change_connection; + boost::signals2::scoped_connection _playlist_length_change_connection; std::list<boost::signals2::connection> _job_connections; std::list<boost::signals2::connection> _audio_analysis_connections; diff --git a/src/lib/playlist.cc b/src/lib/playlist.cc index 9e96c693a..48053bbf4 100644 --- a/src/lib/playlist.cc +++ b/src/lib/playlist.cc @@ -111,8 +111,11 @@ Playlist::content_change (weak_ptr<const Film> weak_film, ChangeType type, weak_ } if (changed) { - OrderChanged (); + OrderChange (); } + + /* The length might have changed, and that's good enough for this signal */ + LengthChange (); } } @@ -281,6 +284,8 @@ Playlist::add (shared_ptr<const Film> film, shared_ptr<Content> c) } Change (CHANGE_TYPE_DONE); + + LengthChange (); } void @@ -312,6 +317,8 @@ Playlist::remove (shared_ptr<Content> c) } /* This won't change order, so it does not need a sort */ + + LengthChange (); } void @@ -334,9 +341,11 @@ Playlist::remove (ContentList c) } } + Change (CHANGE_TYPE_DONE); + /* This won't change order, so it does not need a sort */ - Change (CHANGE_TYPE_DONE); + LengthChange (); } class FrameRateCandidate diff --git a/src/lib/playlist.h b/src/lib/playlist.h index d7db75d0f..dc984aacf 100644 --- a/src/lib/playlist.h +++ b/src/lib/playlist.h @@ -77,7 +77,9 @@ public: /** Emitted when content has been added to or removed from the playlist; implies OrderChanged */ mutable boost::signals2::signal<void (ChangeType)> Change; - mutable boost::signals2::signal<void ()> OrderChanged; + mutable boost::signals2::signal<void ()> OrderChange; + /** Emitted when the length might have changed; may sometimes be emitted when it has not */ + mutable boost::signals2::signal<void ()> LengthChange; mutable boost::signals2::signal<void (ChangeType, boost::weak_ptr<Content>, int, bool)> ContentChange; diff --git a/src/tools/dcpomatic.cc b/src/tools/dcpomatic.cc index 68bf20732..47851a218 100644 --- a/src/tools/dcpomatic.cc +++ b/src/tools/dcpomatic.cc @@ -92,6 +92,9 @@ #include <wx/preferences.h> #include <wx/splash.h> #include <wx/wxhtml.h> +#ifdef __WXGTK__ +#include <X11/Xlib.h> +#endif #ifdef __WXMSW__ #include <shellapi.h> #endif @@ -1503,7 +1506,11 @@ public: : wxApp () , _frame (0) , _splash (0) - {} + { +#ifdef DCPOMATIC_LINUX + XInitThreads (); +#endif + } private: diff --git a/src/tools/dcpomatic_player.cc b/src/tools/dcpomatic_player.cc index f68d0ead2..328093d8a 100644 --- a/src/tools/dcpomatic_player.cc +++ b/src/tools/dcpomatic_player.cc @@ -66,10 +66,14 @@ #include <wx/preferences.h> #include <wx/progdlg.h> #include <wx/display.h> +#ifdef __WXGTK__ +#include <X11/Xlib.h> +#endif #ifdef __WXOSX__ #include <ApplicationServices/ApplicationServices.h> #endif #include <boost/bind.hpp> +#include <boost/algorithm/string.hpp> #include <iostream> #ifdef check @@ -90,8 +94,61 @@ using boost::optional; using boost::dynamic_pointer_cast; using boost::thread; using boost::bind; +using dcp::raw_convert; using namespace dcpomatic; +#ifdef DCPOMATIC_PLAYER_STRESS_TEST +#define STRESS_TEST_CHECK_INTERVAL 20 + +class Command +{ +public: + enum Type { + NONE, + OPEN, + PLAY, + WAIT, + STOP, + SEEK, + }; + + Command(string line) + : type (NONE) + , int_param (0) + { + vector<string> bits; + boost::split (bits, line, boost::is_any_of(" ")); + if (bits[0] == "O") { + if (bits.size() != 2) { + return; + } + type = OPEN; + string_param = bits[1]; + } else if (bits[0] == "P") { + type = PLAY; + } else if (bits[0] == "W") { + if (bits.size() != 2) { + return; + } + type = WAIT; + int_param = raw_convert<int>(bits[1]); + } else if (bits[0] == "S") { + type = STOP; + } else if (bits[0] == "K") { + if (bits.size() != 2) { + return; + } + type = SEEK; + int_param = raw_convert<int>(bits[1]); + } + } + + Type type; + string string_param; + int int_param; +}; +#endif + enum { ID_file_open = 1, ID_file_add_ov, @@ -136,7 +193,10 @@ public: , _system_information_dialog (0) , _view_full_screen (0) , _view_dual_screen (0) - { +#ifdef DCPOMATIC_PLAYER_STRESS_TEST + , _timer (this) +#endif +{ dcpomatic_log.reset (new NullLog()); #if defined(DCPOMATIC_WINDOWS) @@ -233,6 +293,70 @@ public: #endif } +#ifdef DCPOMATIC_PLAYER_STRESS_TEST + void stress (boost::filesystem::path script_file) + { + Bind (wxEVT_TIMER, boost::bind(&DOMFrame::check_commands, this)); + _timer.Start(STRESS_TEST_CHECK_INTERVAL); + vector<string> lines; + string const script = dcp::file_to_string(script_file); + boost::split (lines, script, boost::is_any_of("\n")); + BOOST_FOREACH (string i, lines) { + _commands.push_back (Command(i)); + } + _current_command = _commands.begin(); + } + + void check_commands () + { + if (_current_command == _commands.end()) { + _timer.Stop (); + cout << "ST: finished.\n"; + return; + } + + switch (_current_command->type) { + case Command::OPEN: + cout << "ST: load " << _current_command->string_param << "\n"; + load_dcp (_current_command->string_param); + ++_current_command; + break; + case Command::PLAY: + cout << "ST: play\n"; + _controls->play (); + ++_current_command; + break; + case Command::WAIT: + if (_wait_remaining) { + _wait_remaining = *_wait_remaining - STRESS_TEST_CHECK_INTERVAL; + if (_wait_remaining < 0) { + cout << "ST: wait done.\n"; + _wait_remaining = optional<int>(); + ++_current_command; + } + } else { + _wait_remaining = _current_command->int_param; + cout << "ST: waiting for " << *_wait_remaining << ".\n"; + } + break; + case Command::STOP: + cout << "ST: stop\n"; + _controls->stop (); + ++_current_command; + break; + case Command::NONE: + ++_current_command; + break; + case Command::SEEK: + /* int_param here is a number between 0 and 4095, corresponding to the possible slider positions */ + cout << "ST: seek to " << _current_command->int_param << "\n"; + _controls->seek (_current_command->int_param); + ++_current_command; + break; + } + } +#endif + #ifdef DCPOMATIC_VARIANT_SWAROOP void monitor_checker_state_changed () { @@ -1001,11 +1125,20 @@ private: wxMenuItem* _tools_verify; wxMenuItem* _view_full_screen; wxMenuItem* _view_dual_screen; +#ifdef DCPOMATIC_PLAYER_STRESS_TEST + wxTimer _timer; + list<Command> _commands; + list<Command>::const_iterator _current_command; + optional<int> _wait_remaining; +#endif }; static const wxCmdLineEntryDesc command_line_description[] = { { wxCMD_LINE_PARAM, 0, 0, "DCP to load or create", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, { wxCMD_LINE_OPTION, "c", "config", "Directory containing config.xml", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, +#ifdef DCPOMATIC_PLAYER_STRESS_TEST + { wxCMD_LINE_OPTION, "s", "stress", "File containing description of stress test", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, +#endif { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 } }; @@ -1044,7 +1177,11 @@ public: App () : wxApp () , _frame (0) - {} + { +#ifdef DCPOMATIC_LINUX + XInitThreads (); +#endif + } private: @@ -1117,6 +1254,16 @@ private: } } +#ifdef DCPOMATIC_PLAYER_STRESS_TEST + if (_stress) { + try { + _frame->stress (_stress.get()); + } catch (exception& e) { + error_dialog (0, wxString::Format("Could not load stress test file %s", std_to_wx(*_stress))); + } + } +#endif + Bind (wxEVT_IDLE, boost::bind (&App::idle, this)); if (Config::instance()->check_for_updates ()) { @@ -1150,6 +1297,12 @@ private: if (parser.Found("c", &config)) { Config::override_path = wx_to_std (config); } +#ifdef DCPOMATIC_PLAYER_STRESS_TEST + wxString stress; + if (parser.Found("s", &stress)) { + _stress = wx_to_std (stress); + } +#endif return true; } @@ -1210,6 +1363,9 @@ private: DOMFrame* _frame; string _dcp_to_load; +#ifdef DCPOMATIC_PLAYER_STRESS_TEST + boost::optional<string> _stress; +#endif }; IMPLEMENT_APP (App) diff --git a/src/tools/stress b/src/tools/stress new file mode 100755 index 000000000..f861753e6 --- /dev/null +++ b/src/tools/stress @@ -0,0 +1,57 @@ +#!/usr/bin/python3.7 + +import argparse +import subprocess +import sys +import random + +def hms_to_seconds(h): + s = h.split(':') + assert(1 <= len(s) and len(s) <= 3) + if len(s) == 1: + return int(h) + elif len(s) == 2: + return int(s[0]) * 60 + int(s[1]) + elif len(s) == 3: + return ((int(s[0]) * 60 + int(s[1])) * 60) + int(s[2]) + +def seek(dcp_seconds): + print("O %s" % args.dcp) + print("P") + test_seconds = hms_to_seconds(args.length) + while test_seconds > 0: + wait = random.randint(500, dcp_seconds * 1000) + # Wait some milliseconds + print("W %d" % wait) + # Seek + print("S %d" % random.randint(0, 4095)) + # Make sure we're stil playing + print("P") + test_seconds -= wait / 1000 + +def repeat(dcp_seconds): + print("O %s" % args.dcp) + test_seconds = hms_to_seconds(args.length) + while test_seconds > 0: + print("P") + print("W %d" % (dcp_seconds * 1000)) + test_seconds -= dcp_seconds + +parser = argparse.ArgumentParser() +parser.add_argument('-d', '--dcp', help='DCP to make a script for', required=True) +parser.add_argument('-t', '--type', help='script type: seek - seek a lot, repeat - play back DCP over and over', required=True) +parser.add_argument('-l', '--length', help='approximate test length in H:M:S', required=True) +args = parser.parse_args() + +for l in subprocess.run(['dcpinfo', args.dcp], capture_output=True).stdout.splitlines(): + if l.startswith(b'Total:'): + b = l.split(b':') + dcp_seconds = (int(b[1]) * 60 + int(b[2])) * 60 + int(b[3]) +if args.type == 'seek': + seek(dcp_seconds) +elif args.type == 'repeat': + repeat(dcp_seconds) +else: + print('Unknown type %s' % args.type, file=sys.stderr) + sys.exit(1) + diff --git a/src/tools/wscript b/src/tools/wscript index 8fd23cfb3..3b2c0a04c 100644 --- a/src/tools/wscript +++ b/src/tools/wscript @@ -68,6 +68,8 @@ def build(bld): obj.uselib += ' WXWIDGETS' if not bld.env.TARGET_OSX: obj.uselib += ' GL GLU' + if bld.env.TARGET_LINUX: + obj.uselib += ' X11' obj.includes = ['..'] obj.use = ['libdcpomatic2', 'libdcpomatic2-wx'] obj.source = '%s.cc' % t diff --git a/src/wx/closed_captions_dialog.cc b/src/wx/closed_captions_dialog.cc index 061262cdd..afcf5fa87 100644 --- a/src/wx/closed_captions_dialog.cc +++ b/src/wx/closed_captions_dialog.cc @@ -53,6 +53,7 @@ ClosedCaptionsDialog::ClosedCaptionsDialog (wxWindow* parent, FilmViewer* viewer , _display (new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(640, (640 / 10) + 64))) , _track (new wxChoice(this, wxID_ANY)) , _current_in_lines (false) + , _timer (this) { _lines.resize (CLOSED_CAPTION_LINES); @@ -65,6 +66,8 @@ ClosedCaptionsDialog::ClosedCaptionsDialog (wxWindow* parent, FilmViewer* viewer sizer->Add (track_sizer, 0, wxALL, DCPOMATIC_SIZER_GAP); sizer->Add (_display, 1, wxEXPAND); + Bind (wxEVT_SHOW, boost::bind(&ClosedCaptionsDialog::shown, this, _1)); + Bind (wxEVT_TIMER, boost::bind(&ClosedCaptionsDialog::update, this)); _display->Bind (wxEVT_PAINT, boost::bind(&ClosedCaptionsDialog::paint, this)); _track->Bind (wxEVT_CHOICE, boost::bind(&ClosedCaptionsDialog::track_selected, this)); @@ -72,11 +75,21 @@ ClosedCaptionsDialog::ClosedCaptionsDialog (wxWindow* parent, FilmViewer* viewer } void +ClosedCaptionsDialog::shown (wxShowEvent ev) +{ + if (ev.IsShown ()) { + _timer.Start (40); + } else { + _timer.Stop (); + } +} + +void ClosedCaptionsDialog::track_selected () { _current = optional<TextRingBuffers::Data> (); _viewer->slow_refresh (); - update (_last_update); + update (); } void @@ -131,9 +144,9 @@ private: }; void -ClosedCaptionsDialog::update (DCPTime time) +ClosedCaptionsDialog::update () { - _last_update = time; + DCPTime const time = _viewer->time (); if (_current_in_lines && _current && _current->period.to > time) { /* Current one is fine */ diff --git a/src/wx/closed_captions_dialog.h b/src/wx/closed_captions_dialog.h index fb4e9d59b..5c366ca7b 100644 --- a/src/wx/closed_captions_dialog.h +++ b/src/wx/closed_captions_dialog.h @@ -31,11 +31,12 @@ class ClosedCaptionsDialog : public wxDialog public: explicit ClosedCaptionsDialog (wxWindow* parent, FilmViewer* viewer); - void update (dcpomatic::DCPTime); void clear (); void set_film_and_butler (boost::shared_ptr<Film>, boost::weak_ptr<Butler>); private: + void shown (wxShowEvent); + void update (); void paint (); void track_selected (); @@ -47,5 +48,5 @@ private: std::vector<wxString> _lines; std::vector<DCPTextTrack> _tracks; boost::weak_ptr<Butler> _butler; - dcpomatic::DCPTime _last_update; + wxTimer _timer; }; diff --git a/src/wx/controls.cc b/src/wx/controls.cc index b173b43ff..27139f1f5 100644 --- a/src/wx/controls.cc +++ b/src/wx/controls.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2018 Carl Hetherington <cth@carlh.net> + Copyright (C) 2018-2019 Carl Hetherington <cth@carlh.net> This file is part of DCP-o-matic. @@ -67,6 +67,7 @@ Controls::Controls (wxWindow* parent, shared_ptr<FilmViewer> viewer, bool editor , _forward_button (new Button (this, wxT(">"))) , _frame_number (new StaticText (this, wxT(""))) , _timecode (new StaticText (this, wxT(""))) + , _timer (this) { _v_sizer = new wxBoxSizer (wxVERTICAL); SetSizer (_v_sizer); @@ -119,14 +120,7 @@ Controls::Controls (wxWindow* parent, shared_ptr<FilmViewer> viewer, bool editor _slider->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind(&Controls::slider_moved, this, false)); _slider->Bind (wxEVT_SCROLL_PAGEUP, boost::bind(&Controls::slider_moved, this, true)); _slider->Bind (wxEVT_SCROLL_PAGEDOWN, boost::bind(&Controls::slider_moved, this, true)); - _slider->Bind (wxEVT_SCROLL_CHANGED, boost::bind(&Controls::slider_released, this)); -#ifdef DCPOMATIC_OSX - /* _CHANGED is not received on OS X (at least, not when the - slider is dragged), so use this instead. Perhaps all - platforms could just use _THUMBRELEASE. - */ _slider->Bind (wxEVT_SCROLL_THUMBRELEASE, boost::bind(&Controls::slider_released, this)); -#endif _rewind_button->Bind (wxEVT_LEFT_DOWN, boost::bind(&Controls::rewind_clicked, this, _1)); _back_button->Bind (wxEVT_LEFT_DOWN, boost::bind(&Controls::back_clicked, this, _1)); _forward_button->Bind (wxEVT_LEFT_DOWN, boost::bind(&Controls::forward_clicked, this, _1)); @@ -137,10 +131,12 @@ Controls::Controls (wxWindow* parent, shared_ptr<FilmViewer> viewer, bool editor _jump_to_selected->SetValue (Config::instance()->jump_to_selected ()); } - _viewer->PositionChanged.connect (boost::bind(&Controls::position_changed, this)); _viewer->Started.connect (boost::bind(&Controls::started, this)); _viewer->Stopped.connect (boost::bind(&Controls::stopped, this)); + Bind (wxEVT_TIMER, boost::bind(&Controls::update_position, this)); + _timer.Start (80, wxTIMER_CONTINUOUS); + set_film (_viewer->film()); setup_sensitivity (); @@ -172,7 +168,7 @@ Controls::stopped () } void -Controls::position_changed () +Controls::update_position () { if (!_slider_being_moved) { update_position_label (); @@ -410,3 +406,13 @@ Controls::film_change (ChangeType type, Film::Property p) } } } + +#ifdef DCPOMATIC_PLAYER_STRESS_TEST +void +Controls::seek (int slider) +{ + _slider->SetValue (slider); + slider_moved (false); + slider_released (); +} +#endif diff --git a/src/wx/controls.h b/src/wx/controls.h index dfa11e6d7..1b6a379cc 100644 --- a/src/wx/controls.h +++ b/src/wx/controls.h @@ -53,6 +53,11 @@ public: virtual void log (wxString) {} virtual void set_film (boost::shared_ptr<Film> film); +#ifdef DCPOMATIC_PLAYER_STRESS_TEST + virtual void play () {}; + virtual void stop () {}; + void seek (int slider); +#endif boost::shared_ptr<Film> film () const; void back_frame (); void forward_frame (); @@ -87,7 +92,7 @@ private: void image_changed (boost::weak_ptr<PlayerVideo>); void outline_content_changed (); void eye_changed (); - void position_changed (); + void update_position (); void film_change (ChangeType, Film::Property); typedef std::pair<boost::shared_ptr<dcp::CPL>, boost::filesystem::path> CPL; @@ -105,6 +110,8 @@ private: ClosedCaptionsDialog* _closed_captions_dialog; + wxTimer _timer; + boost::signals2::scoped_connection _film_change_connection; boost::signals2::scoped_connection _config_changed_connection; }; diff --git a/src/wx/film_viewer.cc b/src/wx/film_viewer.cc index 509cc9426..9c3a9c81e 100644 --- a/src/wx/film_viewer.cc +++ b/src/wx/film_viewer.cc @@ -86,16 +86,12 @@ FilmViewer::FilmViewer (wxWindow* p) , _playing (false) , _suspended (0) , _latency_history_count (0) - , _dropped (0) , _closed_captions_dialog (new ClosedCaptionsDialog(p, this)) , _outline_content (false) - , _eyes (EYES_LEFT) , _pad_black (false) #ifdef DCPOMATIC_VARIANT_SWAROOP , _background_image (false) #endif - , _state_timer ("viewer") - , _gets (0) , _idle_get (false) { switch (Config::instance()->video_view_type()) { @@ -108,7 +104,6 @@ FilmViewer::FilmViewer (wxWindow* p) } _video_view->Sized.connect (boost::bind(&FilmViewer::video_view_sized, this)); - _timer.Bind (wxEVT_TIMER, boost::bind(&FilmViewer::timer, this)); set_film (shared_ptr<Film> ()); @@ -123,7 +118,7 @@ FilmViewer::~FilmViewer () /** Ask for ::get() to be called next time we are idle */ void -FilmViewer::request_idle_get () +FilmViewer::request_idle_display_next_frame () { if (_idle_get) { return; @@ -141,7 +136,7 @@ FilmViewer::idle_handler () return; } - if (get(true)) { + if (_video_view->display_next_frame(true)) { _idle_get = false; } else { /* get() could not complete quickly so we'll try again later */ @@ -157,17 +152,14 @@ FilmViewer::set_film (shared_ptr<Film> film) } _film = film; - _video_position = DCPTime (); - _player_video.first.reset (); - _player_video.second = DCPTime (); - _video_view->set_image (shared_ptr<Image>()); + _video_view->clear (); _closed_captions_dialog->clear (); if (!_film) { _player.reset (); recreate_butler (); - refresh_view (); + _video_view->update (); return; } @@ -187,8 +179,13 @@ FilmViewer::set_film (shared_ptr<Film> film) _player->set_play_referenced (); _film->Change.connect (boost::bind (&FilmViewer::film_change, this, _1, _2)); + _film->LengthChange.connect (boost::bind(&FilmViewer::film_length_change, this)); _player->Change.connect (boost::bind (&FilmViewer::player_change, this, _1, _2, _3)); + film_change (CHANGE_TYPE_DONE, Film::VIDEO_FRAME_RATE); + film_change (CHANGE_TYPE_DONE, Film::THREE_D); + film_length_change (); + /* Keep about 1 second's worth of history samples */ _latency_history_count = _film->audio_frame_rate() / _audio_block_size; @@ -230,146 +227,16 @@ FilmViewer::recreate_butler () } void -FilmViewer::refresh_view () -{ - _state_timer.set ("update-view"); - _video_view->update (); - _state_timer.unset (); -} - -/** Try to get a frame from the butler and display it. - * @param lazy true to return false quickly if no video is available quickly (i.e. we are waiting for the butler). - * false to ask the butler to block until it has video (unless it is suspended). - * @return true on success, false if we did nothing because it would have taken too long. - */ -bool -FilmViewer::get (bool lazy) -{ - DCPOMATIC_ASSERT (_butler); - ++_gets; - - do { - Butler::Error e; - _player_video = _butler->get_video (!lazy, &e); - if (!_player_video.first && e == Butler::AGAIN) { - if (lazy) { - /* No video available; return saying we failed */ - return false; - } else { - /* Player was suspended; come back later */ - signal_manager->when_idle (boost::bind(&FilmViewer::get, this, false)); - return false; - } - } - } while ( - _player_video.first && - _film->three_d() && - _eyes != _player_video.first->eyes() && - _player_video.first->eyes() != EYES_BOTH - ); - - try { - _butler->rethrow (); - } catch (DecodeError& e) { - error_dialog (_video_view->get(), e.what()); - } - - display_player_video (); - PositionChanged (); - - return true; -} - -void -FilmViewer::display_player_video () -{ - if (!_player_video.first) { - _video_view->set_image (shared_ptr<Image>()); - refresh_view (); - return; - } - - if (_playing && !_suspended && (time() - _player_video.second) > one_video_frame()) { - /* Too late; just drop this frame before we try to get its image (which will be the time-consuming - part if this frame is J2K). - */ - _video_position = _player_video.second; - ++_dropped; - return; - } - - /* In an ideal world, what we would do here is: - * - * 1. convert to XYZ exactly as we do in the DCP creation path. - * 2. convert back to RGB for the preview display, compensating - * for the monitor etc. etc. - * - * but this is inefficient if the source is RGB. Since we don't - * (currently) care too much about the precise accuracy of the preview's - * colour mapping (and we care more about its speed) we try to short- - * circuit this "ideal" situation in some cases. - * - * The content's specified colour conversion indicates the colourspace - * which the content is in (according to the user). - * - * PlayerVideo::image (bound to PlayerVideo::force) will take the source - * image and convert it (from whatever the user has said it is) to RGB. - */ - - _state_timer.set ("get image"); - - _video_view->set_image ( - _player_video.first->image(bind(&PlayerVideo::force, _1, AV_PIX_FMT_RGB24), false, true) - ); - - _state_timer.set ("ImageChanged"); - ImageChanged (_player_video.first); - _state_timer.unset (); - - _video_position = _player_video.second; - _inter_position = _player_video.first->inter_position (); - _inter_size = _player_video.first->inter_size (); - - refresh_view (); - - _closed_captions_dialog->update (time()); -} - -void -FilmViewer::timer () -{ - if (!_film || !_playing || _suspended) { - return; - } - - get (false); - DCPTime const next = _video_position + one_video_frame(); - - if (next >= _film->length()) { - stop (); - Finished (); - return; - } - - LOG_DEBUG_PLAYER("%1 -> %2; delay %3", next.seconds(), time().seconds(), max((next.seconds() - time().seconds()) * 1000, 1.0)); - _timer.Start (max ((next.seconds() - time().seconds()) * 1000, 1.0), wxTIMER_ONE_SHOT); - - if (_butler) { - _butler->rethrow (); - } -} - -void FilmViewer::set_outline_content (bool o) { _outline_content = o; - refresh_view (); + _video_view->update (); } void FilmViewer::set_eyes (Eyes e) { - _eyes = e; + _video_view->set_eyes (e); slow_refresh (); } @@ -380,7 +247,6 @@ FilmViewer::video_view_sized () if (!quick_refresh()) { slow_refresh (); } - PositionChanged (); } void @@ -424,13 +290,14 @@ FilmViewer::suspend () void FilmViewer::resume () { + DCPOMATIC_ASSERT (_suspended > 0); --_suspended; if (_playing && !_suspended) { if (_audio.isStreamOpen()) { - _audio.setStreamTime (_video_position.seconds()); + _audio.setStreamTime (_video_view->position().seconds()); _audio.startStream (); } - timer (); + _video_view->start (); } } @@ -447,14 +314,24 @@ FilmViewer::start () return; } + /* We are about to set up the audio stream from the position of the video view. + If there is `lazy' seek in progress we need to wait for it to go through so that + _video_view->position() gives us a sensible answer. + */ + while (_idle_get) { + idle_handler (); + } + + /* Take the video view's idea of position as our `playhead' and start the + audio stream (which is the timing reference) there. + */ if (_audio.isStreamOpen()) { - _audio.setStreamTime (_video_position.seconds()); + _audio.setStreamTime (_video_view->position().seconds()); _audio.startStream (); } _playing = true; - _dropped = 0; - timer (); + _video_view->start (); Started (position()); } @@ -471,7 +348,10 @@ FilmViewer::stop () } _playing = false; + _video_view->stop (); Stopped (position()); + + _video_view->rethrow (); return true; } @@ -504,22 +384,35 @@ FilmViewer::player_change (ChangeType type, int property, bool frequent) if (!refreshed) { slow_refresh (); } - PositionChanged (); } void FilmViewer::film_change (ChangeType type, Film::Property p) { - if (type == CHANGE_TYPE_DONE && p == Film::AUDIO_CHANNELS) { + if (type != CHANGE_TYPE_DONE) { + return; + } + + if (p == Film::AUDIO_CHANNELS) { recreate_butler (); + } else if (p == Film::VIDEO_FRAME_RATE) { + _video_view->set_video_frame_rate (_film->video_frame_rate()); + } else if (p == Film::THREE_D) { + _video_view->set_three_d (_film->three_d()); } } +void +FilmViewer::film_length_change () +{ + _video_view->set_length (_film->length()); +} + /** Re-get the current frame slowly by seeking */ void FilmViewer::slow_refresh () { - seek (_video_position, true); + seek (_video_view->position(), true); } /** Try to re-get the current frame quickly by resetting the metadata @@ -529,16 +422,10 @@ FilmViewer::slow_refresh () bool FilmViewer::quick_refresh () { - if (!_player_video.first) { - return false; - } - - if (!_player_video.first->reset_metadata (_film, _player->video_container_size(), _film->frame_size())) { - return false; + if (!_video_view || !_film) { + return true; } - - display_player_video (); - return true; + return _video_view->refresh_metadata (_film, _player->video_container_size(), _film->frame_size()); } void @@ -584,10 +471,15 @@ FilmViewer::seek (DCPTime t, bool accurate) _butler->seek (t, accurate); if (!_playing) { - request_idle_get (); + /* We're not playing, so let the GUI thread get on and + come back later to get the next frame after the seek. + */ + request_idle_display_next_frame (); } else { - /* Make sure we get a frame so that _video_position is set up before we resume */ - while (!get(true)) {} + /* We're going to start playing again straight away + so wait for the seek to finish. + */ + while (!_video_view->display_next_frame(false)) {} } resume (); @@ -598,7 +490,7 @@ FilmViewer::config_changed (Config::Property p) { #ifdef DCPOMATIC_VARIANT_SWAROOP if (p == Config::PLAYER_BACKGROUND_IMAGE) { - refresh_view (); + _video_view->update (); return; } #endif @@ -665,18 +557,24 @@ FilmViewer::uncorrected_time () const return DCPTime::from_seconds (const_cast<RtAudio*>(&_audio)->getStreamTime()); } - return _video_position; + return _video_view->position(); } -DCPTime -FilmViewer::time () const +optional<DCPTime> +FilmViewer::audio_time () const { - if (_audio.isStreamRunning ()) { - return DCPTime::from_seconds (const_cast<RtAudio*>(&_audio)->getStreamTime ()) - - DCPTime::from_frames (average_latency(), _film->audio_frame_rate()); + if (!_audio.isStreamRunning()) { + return optional<DCPTime>(); } - return _video_position; + return DCPTime::from_seconds (const_cast<RtAudio*>(&_audio)->getStreamTime ()) - + DCPTime::from_frames (average_latency(), _film->audio_frame_rate()); +} + +DCPTime +FilmViewer::time () const +{ + return audio_time().get_value_or(_video_view->position()); } int @@ -749,7 +647,7 @@ FilmViewer::show_closed_captions () void FilmViewer::seek_by (DCPTime by, bool accurate) { - seek (_video_position + by, accurate); + seek (_video_view->position() + by, accurate); } void @@ -757,3 +655,34 @@ FilmViewer::set_pad_black (bool p) { _pad_black = p; } + +/** Called when a player has finished the current film. + * May be called from a non-UI thread. + */ +void +FilmViewer::finished () +{ + emit (boost::bind(&FilmViewer::ui_finished, this)); +} + +/** Called by finished() in the UI thread */ +void +FilmViewer::ui_finished () +{ + stop (); + Finished (); +} + +int +FilmViewer::dropped () const +{ + return _video_view->dropped (); +} + +int +FilmViewer::gets () const +{ + return _video_view->gets (); +} + + diff --git a/src/wx/film_viewer.h b/src/wx/film_viewer.h index 1f20c3321..60cde60d0 100644 --- a/src/wx/film_viewer.h +++ b/src/wx/film_viewer.h @@ -27,6 +27,7 @@ #include "lib/config.h" #include "lib/player_text.h" #include "lib/timer.h" +#include "lib/signaller.h" #include <RtAudio.h> #include <wx/wx.h> @@ -42,7 +43,7 @@ class ClosedCaptionsDialog; /** @class FilmViewer * @brief A wx widget to view a Film. */ -class FilmViewer +class FilmViewer : public Signaller { public: FilmViewer (wxWindow *); @@ -69,7 +70,7 @@ public: void seek_by (dcpomatic::DCPTime by, bool accurate); /** @return our `playhead' position; this may not lie exactly on a frame boundary */ dcpomatic::DCPTime position () const { - return _video_position; + return _video_view->position(); } dcpomatic::DCPTime one_video_frame () const; @@ -77,6 +78,7 @@ public: bool stop (); void suspend (); void resume (); + bool playing () const { return _playing; } @@ -90,16 +92,18 @@ public: void slow_refresh (); - int dropped () const { - return _dropped; - } + dcpomatic::DCPTime time () const; + boost::optional<dcpomatic::DCPTime> audio_time () const; + + int dropped () const; + int gets () const; int audio_callback (void* out, unsigned int frames); #ifdef DCPOMATIC_VARIANT_SWAROOP void set_background_image (bool b) { _background_image = b; - refresh_view (); + _video_view->update (); } bool background_image () const { @@ -108,39 +112,28 @@ public: #endif StateTimer const & state_timer () const { - return _state_timer; - } - - StateTimer& state_timer () { - return _state_timer; - } - - int gets () const { - return _gets; + return _video_view->state_timer (); } - /* Some accessors that VideoView classes need */ + /* Some accessors and utility methods that VideoView classes need */ dcp::Size out_size () const { return _out_size; } - dcp::Size inter_size () const { - return _inter_size; - } - Position<int> inter_position () const { - return _inter_position; - } bool outline_content () const { return _outline_content; } bool pad_black () const { return _pad_black; } - dcpomatic::DCPTime video_position () const { - return _video_position; + boost::shared_ptr<Butler> butler () const { + return _butler; } + ClosedCaptionsDialog* closed_captions_dialog () const { + return _closed_captions_dialog; + } + void finished (); boost::signals2::signal<void (boost::weak_ptr<PlayerVideo>)> ImageChanged; - boost::signals2::signal<void ()> PositionChanged; boost::signals2::signal<void (dcpomatic::DCPTime)> Started; boost::signals2::signal<void (dcpomatic::DCPTime)> Stopped; /** While playing back we reached the end of the film (emitted from GUI thread) */ @@ -149,38 +142,30 @@ public: boost::signals2::signal<bool ()> PlaybackPermitted; private: + void video_view_sized (); - void timer (); void calculate_sizes (); void player_change (ChangeType type, int, bool); - bool get (bool lazy); void idle_handler (); - void request_idle_get (); - void display_player_video (); + void request_idle_display_next_frame (); void film_change (ChangeType, Film::Property); void recreate_butler (); void config_changed (Config::Property); + void film_length_change (); + void ui_finished (); - dcpomatic::DCPTime time () const; dcpomatic::DCPTime uncorrected_time () const; Frame average_latency () const; - void refresh_view (); bool quick_refresh (); boost::shared_ptr<Film> _film; boost::shared_ptr<Player> _player; VideoView* _video_view; - wxTimer _timer; bool _coalesce_player_changes; std::list<int> _pending_player_changes; - std::pair<boost::shared_ptr<PlayerVideo>, dcpomatic::DCPTime> _player_video; - dcpomatic::DCPTime _video_position; - Position<int> _inter_position; - dcp::Size _inter_size; - /** Size of our output (including padding if we have any) */ dcp::Size _out_size; @@ -196,13 +181,11 @@ private: mutable boost::mutex _latency_history_mutex; int _latency_history_count; - int _dropped; boost::optional<int> _dcp_decode_reduction; ClosedCaptionsDialog* _closed_captions_dialog; bool _outline_content; - Eyes _eyes; /** true to pad the viewer panel with black, false to use the normal window background colour. */ @@ -212,9 +195,6 @@ private: bool _background_image; #endif - StateTimer _state_timer; - int _gets; - /** true if an get() is required next time we are idle */ bool _idle_get; diff --git a/src/wx/gl_video_view.cc b/src/wx/gl_video_view.cc index a0f83db6d..d47ad87f4 100644 --- a/src/wx/gl_video_view.cc +++ b/src/wx/gl_video_view.cc @@ -20,9 +20,13 @@ #include "gl_video_view.h" #include "film_viewer.h" +#include "wx_util.h" #include "lib/image.h" #include "lib/dcpomatic_assert.h" #include "lib/exceptions.h" +#include "lib/cross.h" +#include "lib/player_video.h" +#include "lib/butler.h" #include <boost/bind.hpp> #include <iostream> @@ -51,12 +55,20 @@ using boost::optional; GLVideoView::GLVideoView (FilmViewer* viewer, wxWindow *parent) : VideoView (viewer) + , _have_storage (false) , _vsync_enabled (false) + , _thread (0) + , _playing (false) + , _one_shot (false) { _canvas = new wxGLCanvas (parent, wxID_ANY, 0, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE); - _context = new wxGLContext (_canvas); - _canvas->Bind (wxEVT_PAINT, boost::bind(&GLVideoView::paint, this)); + _canvas->Bind (wxEVT_PAINT, boost::bind(&GLVideoView::update, this)); _canvas->Bind (wxEVT_SIZE, boost::bind(boost::ref(Sized))); + _canvas->Bind (wxEVT_CREATE, boost::bind(&GLVideoView::create, this)); + + _canvas->Bind (wxEVT_TIMER, boost::bind(&GLVideoView::check_for_butler_errors, this)); + _timer.reset (new wxTimer(_canvas)); + _timer->Start (2000); #if defined(DCPOMATIC_LINUX) && defined(DCPOMATIC_HAVE_GLX_SWAP_INTERVAL_EXT) if (_canvas->IsExtensionSupported("GLX_EXT_swap_control")) { @@ -93,12 +105,25 @@ GLVideoView::GLVideoView (FilmViewer* viewer, wxWindow *parent) GLVideoView::~GLVideoView () { + _thread->interrupt (); + _thread->join (); + delete _thread; + glDeleteTextures (1, &_id); - delete _context; +} + +void +GLVideoView::check_for_butler_errors () +{ + try { + _viewer->butler()->rethrow (); + } catch (DecodeError& e) { + error_dialog (get(), e.what()); + } } static void - check_gl_error (char const * last) +check_gl_error (char const * last) { GLenum const e = glGetError (); if (e != GL_NO_ERROR) { @@ -107,27 +132,19 @@ static void } void -GLVideoView::paint () -{ - _viewer->state_timer().set("paint-panel"); - _canvas->SetCurrent (*_context); - wxPaintDC dc (_canvas); - draw (); - _viewer->state_timer().unset(); -} - -void GLVideoView::update () { - if (!_canvas->IsShownOnScreen()) { - return; + { + boost::mutex::scoped_lock lm (_canvas_mutex); + if (!_canvas->IsShownOnScreen()) { + return; + } } - wxClientDC dc (_canvas); - draw (); + request_one_shot (); } void -GLVideoView::draw () +GLVideoView::draw (Position<int> inter_position, dcp::Size inter_size) { glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); check_gl_error ("glClear"); @@ -142,25 +159,33 @@ GLVideoView::draw () check_gl_error ("glDisable GL_DEPTH_TEST"); glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - if (_canvas->GetSize().x < 64 || _canvas->GetSize().y < 0) { + wxSize canvas_size; + { + boost::mutex::scoped_lock lm (_canvas_mutex); + canvas_size = _canvas->GetSize (); + } + + if (canvas_size.GetWidth() < 64 || canvas_size.GetHeight() < 0) { return; } - glViewport (0, 0, _canvas->GetSize().x, _canvas->GetSize().y); + glViewport (0, 0, canvas_size.GetWidth(), canvas_size.GetHeight()); check_gl_error ("glViewport"); glMatrixMode (GL_PROJECTION); glLoadIdentity (); - gluOrtho2D (0, _canvas->GetSize().x, _canvas->GetSize().y, 0); + gluOrtho2D (0, canvas_size.GetWidth(), canvas_size.GetHeight(), 0); check_gl_error ("gluOrtho2d"); glMatrixMode (GL_MODELVIEW); glLoadIdentity (); glTranslatef (0, 0, 0); + dcp::Size const out_size = _viewer->out_size (); + if (_size) { + /* Render our image (texture) */ glBegin (GL_QUADS); - glTexCoord2f (0, 1); glVertex2f (0, _size->height); glTexCoord2f (1, 1); @@ -169,13 +194,19 @@ GLVideoView::draw () glVertex2f (_size->width, 0); glTexCoord2f (0, 0); glVertex2f (0, 0); - + glEnd (); + } else { + /* No image, so just fill with black */ + glBegin (GL_QUADS); + glColor3ub (0, 0, 0); + glVertex2f (0, 0); + glVertex2f (out_size.width, 0); + glVertex2f (out_size.width, out_size.height); + glVertex2f (0, out_size.height); + glVertex2f (0, 0); glEnd (); } - dcp::Size const out_size = _viewer->out_size (); - wxSize const canvas_size = _canvas->GetSize (); - if (!_viewer->pad_black() && out_size.width < canvas_size.GetWidth()) { glBegin (GL_QUADS); /* XXX: these colours are right for GNOME; may need adjusting for other OS */ @@ -209,8 +240,6 @@ GLVideoView::draw () if (_viewer->outline_content()) { glColor3ub (255, 0, 0); glBegin (GL_LINE_LOOP); - Position<int> inter_position = _viewer->inter_position (); - dcp::Size inter_size = _viewer->inter_size (); glVertex2f (inter_position.x, inter_position.y + (canvas_size.GetHeight() - out_size.height) / 2); glVertex2f (inter_position.x + inter_size.width, inter_position.y + (canvas_size.GetHeight() - out_size.height) / 2); glVertex2f (inter_position.x + inter_size.width, inter_position.y + (canvas_size.GetHeight() - out_size.height) / 2 + inter_size.height); @@ -220,6 +249,8 @@ GLVideoView::draw () } glFlush(); + + boost::mutex::scoped_lock lm (_canvas_mutex); _canvas->SwapBuffers(); } @@ -234,11 +265,21 @@ GLVideoView::set_image (shared_ptr<const Image> image) DCPOMATIC_ASSERT (image->pixel_format() == AV_PIX_FMT_RGB24); DCPOMATIC_ASSERT (!image->aligned()); + if (image->size() != _size) { + _have_storage = false; + } + _size = image->size (); glPixelStorei (GL_UNPACK_ALIGNMENT, 1); check_gl_error ("glPixelStorei"); - glTexImage2D (GL_TEXTURE_2D, 0, GL_RGB8, _size->width, _size->height, 0, GL_RGB, GL_UNSIGNED_BYTE, image->data()[0]); - check_gl_error ("glTexImage2D"); + if (_have_storage) { + glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, _size->width, _size->height, GL_RGB, GL_UNSIGNED_BYTE, image->data()[0]); + check_gl_error ("glTexSubImage2D"); + } else { + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGB8, _size->width, _size->height, 0, GL_RGB, GL_UNSIGNED_BYTE, image->data()[0]); + _have_storage = true; + check_gl_error ("glTexImage2D"); + } glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); @@ -248,3 +289,104 @@ GLVideoView::set_image (shared_ptr<const Image> image) glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); check_gl_error ("glTexParameterf"); } + +void +GLVideoView::start () +{ + VideoView::start (); + + boost::mutex::scoped_lock lm (_playing_mutex); + _playing = true; + _playing_condition.notify_all (); +} + +void +GLVideoView::stop () +{ + boost::mutex::scoped_lock lm (_playing_mutex); + _playing = false; +} + +void +GLVideoView::thread () +try +{ + { + boost::mutex::scoped_lock lm (_canvas_mutex); + _context = new wxGLContext (_canvas); //local + _canvas->SetCurrent (*_context); + } + + while (true) { + boost::mutex::scoped_lock lm (_playing_mutex); + while (!_playing && !_one_shot) { + _playing_condition.wait (lm); + } + _one_shot = false; + lm.unlock (); + + Position<int> inter_position; + dcp::Size inter_size; + if (length() != dcpomatic::DCPTime()) { + dcpomatic::DCPTime const next = position() + one_video_frame(); + + if (next >= length()) { + _viewer->finished (); + continue; + } + + get_next_frame (false); + shared_ptr<PlayerVideo> pv = player_video().first; + if (pv) { + set_image (pv->image(bind(&PlayerVideo::force, _1, AV_PIX_FMT_RGB24), false, true)); + inter_position = pv->inter_position(); + inter_size = pv->inter_size(); + } + } + draw (inter_position, inter_size); + + while (true) { + optional<int> n = time_until_next_frame(); + if (!n || *n > 5) { + break; + } + get_next_frame (true); + add_dropped (); + } + + boost::this_thread::interruption_point (); + dcpomatic_sleep_milliseconds (time_until_next_frame().get_value_or(0)); + } + + /* XXX: leaks _context, but that seems preferable to deleting it here + * without also deleting the wxGLCanvas. + */ +} +catch (boost::thread_interrupted& e) +{ + store_current (); +} + +bool +GLVideoView::display_next_frame (bool non_blocking) +{ + bool const r = get_next_frame (non_blocking); + request_one_shot (); + return r; +} + +void +GLVideoView::request_one_shot () +{ + boost::mutex::scoped_lock lm (_playing_mutex); + _one_shot = true; + _playing_condition.notify_all (); +} + +void +GLVideoView::create () +{ + if (!_thread) { + _thread = new boost::thread (boost::bind(&GLVideoView::thread, this)); + } +} diff --git a/src/wx/gl_video_view.h b/src/wx/gl_video_view.h index ba4c7cfdc..2f3c8c2a1 100644 --- a/src/wx/gl_video_view.h +++ b/src/wx/gl_video_view.h @@ -19,10 +19,14 @@ */ #include "video_view.h" +#include "lib/signaller.h" +#include "lib/position.h" #include <wx/wx.h> #include <wx/glcanvas.h> #include <dcp/util.h> #include <boost/shared_ptr.hpp> +#include <boost/thread.hpp> +#include <boost/thread/condition.hpp> #undef None #undef Success @@ -37,18 +41,39 @@ public: return _canvas; } void update (); + void start (); + void stop (); + + bool display_next_frame (bool); bool vsync_enabled () const { return _vsync_enabled; } private: - void paint (); - void draw (); + void draw (Position<int> inter_position, dcp::Size inter_size); + void thread (); + void request_one_shot (); + void create (); + void check_for_butler_errors (); + /* Mutex for use of _canvas; it's only contended when our ::thread + is started up so this may be overkill. + */ + boost::mutex _canvas_mutex; wxGLCanvas* _canvas; - wxGLContext* _context; - GLuint _id; - boost::optional<dcp::Size> _size; + wxGLContext* _context; + + GLuint _id; + boost::optional<dcp::Size> _size; + bool _have_storage; bool _vsync_enabled; + boost::thread* _thread; + + boost::mutex _playing_mutex; + boost::condition _playing_condition; + bool _playing; + bool _one_shot; + + boost::shared_ptr<wxTimer> _timer; }; diff --git a/src/wx/simple_video_view.cc b/src/wx/simple_video_view.cc index 66af8f19f..ae13fe1bc 100644 --- a/src/wx/simple_video_view.cc +++ b/src/wx/simple_video_view.cc @@ -21,7 +21,10 @@ #include "simple_video_view.h" #include "film_viewer.h" #include "wx_util.h" +#include "closed_captions_dialog.h" #include "lib/image.h" +#include "lib/dcpomatic_log.h" +#include "lib/butler.h" #include <dcp/util.h> #include <wx/wx.h> #include <boost/bind.hpp> @@ -29,6 +32,7 @@ using std::max; using std::string; using boost::optional; +using boost::shared_ptr; using namespace dcpomatic; SimpleVideoView::SimpleVideoView (FilmViewer* viewer, wxWindow* parent) @@ -45,12 +49,14 @@ SimpleVideoView::SimpleVideoView (FilmViewer* viewer, wxWindow* parent) _panel->Bind (wxEVT_PAINT, boost::bind (&SimpleVideoView::paint, this)); _panel->Bind (wxEVT_SIZE, boost::bind(boost::ref(Sized))); + + _timer.Bind (wxEVT_TIMER, boost::bind(&SimpleVideoView::timer, this)); } void SimpleVideoView::paint () { - _viewer->state_timer().set("paint-panel"); + _state_timer.set("paint-panel"); wxPaintDC dc (_panel); dcp::Size const out_size = _viewer->out_size (); @@ -79,10 +85,10 @@ SimpleVideoView::paint () #ifdef DCPOMATIC_VARIANT_SWAROOP DCPTime const period = DCPTime::from_seconds(Config::instance()->player_watermark_period() * 60); - int64_t n = _viewer->video_position().get() / period.get(); + int64_t n = position().get() / period.get(); DCPTime from(n * period.get()); DCPTime to = from + DCPTime::from_seconds(Config::instance()->player_watermark_duration() / 1000.0); - if (from <= _viewer->video_position() && _viewer->video_position() <= to) { + if (from <= position() && position() <= to) { if (!_in_watermark) { _in_watermark = true; _watermark_x = rand() % panel_size.GetWidth(); @@ -119,19 +125,131 @@ SimpleVideoView::paint () } if (_viewer->outline_content()) { - Position<int> inter_position = _viewer->inter_position (); - dcp::Size inter_size = _viewer->inter_size (); wxPen p (wxColour (255, 0, 0), 2); dc.SetPen (p); dc.SetBrush (*wxTRANSPARENT_BRUSH); - dc.DrawRectangle (inter_position.x, inter_position.y + (panel_size.GetHeight() - out_size.height) / 2, inter_size.width, inter_size.height); + dc.DrawRectangle (_inter_position.x, _inter_position.y + (panel_size.GetHeight() - out_size.height) / 2, _inter_size.width, _inter_size.height); } - _viewer->state_timer().unset(); + _state_timer.unset(); } void -SimpleVideoView::update () +SimpleVideoView::refresh_panel () { + _state_timer.set ("refresh-panel"); _panel->Refresh (); _panel->Update (); + _state_timer.unset (); +} + +void +SimpleVideoView::timer () +{ + if (!_viewer->playing()) { + return; + } + + display_next_frame (false); + DCPTime const next = position() + _viewer->one_video_frame(); + + if (next >= length()) { + _viewer->finished (); + return; + } + + LOG_DEBUG_PLAYER("%1 -> %2; delay %3", next.seconds(), _viewer->time().seconds(), max((next.seconds() - _viewer->time().seconds()) * 1000, 1.0)); + _timer.Start (time_until_next_frame().get_value_or(0), wxTIMER_ONE_SHOT); + + if (_viewer->butler()) { + _viewer->butler()->rethrow (); + } +} + +void +SimpleVideoView::start () +{ + VideoView::start (); + timer (); +} + +/** Try to get a frame from the butler and display it. + * @param non_blocking true to return false quickly if no video is available quickly (i.e. we are waiting for the butler). + * false to ask the butler to block until it has video (unless it is suspended). + * @return true on success, false if we did nothing because it would have taken too long. + */ +bool +SimpleVideoView::display_next_frame (bool non_blocking) +{ + bool r = get_next_frame (non_blocking); + if (!r) { + if (non_blocking) { + /* No video available; return saying we failed */ + return false; + } else { + /* Player was suspended; come back later */ + signal_manager->when_idle (boost::bind(&SimpleVideoView::display_next_frame, this, false)); + return false; + } + } + + update (); + + try { + _viewer->butler()->rethrow (); + } catch (DecodeError& e) { + error_dialog (get(), e.what()); + } + + return true; +} + +void +SimpleVideoView::update () +{ + if (!player_video().first) { + set_image (shared_ptr<Image>()); + refresh_panel (); + return; + } + + if (_viewer->playing() && (_viewer->time() - player_video().second) > one_video_frame()) { + /* Too late; just drop this frame before we try to get its image (which will be the time-consuming + part if this frame is J2K). + */ + add_dropped (); + return; + } + + /* In an ideal world, what we would do here is: + * + * 1. convert to XYZ exactly as we do in the DCP creation path. + * 2. convert back to RGB for the preview display, compensating + * for the monitor etc. etc. + * + * but this is inefficient if the source is RGB. Since we don't + * (currently) care too much about the precise accuracy of the preview's + * colour mapping (and we care more about its speed) we try to short- + * circuit this "ideal" situation in some cases. + * + * The content's specified colour conversion indicates the colourspace + * which the content is in (according to the user). + * + * PlayerVideo::image (bound to PlayerVideo::force) will take the source + * image and convert it (from whatever the user has said it is) to RGB. + */ + + _state_timer.set ("get image"); + + set_image ( + player_video().first->image(bind(&PlayerVideo::force, _1, AV_PIX_FMT_RGB24), false, true) + ); + + _state_timer.set ("ImageChanged"); + _viewer->ImageChanged (player_video().first); + _state_timer.unset (); + + _inter_position = player_video().first->inter_position (); + _inter_size = player_video().first->inter_size (); + + refresh_panel (); } diff --git a/src/wx/simple_video_view.h b/src/wx/simple_video_view.h index 686a1a1f3..a6a5cf47f 100644 --- a/src/wx/simple_video_view.h +++ b/src/wx/simple_video_view.h @@ -19,6 +19,8 @@ */ #include "video_view.h" +#include "lib/position.h" +#include <dcp/types.h> #include <wx/wx.h> class FilmViewer; @@ -37,10 +39,17 @@ public: } void update (); + void start (); + bool display_next_frame (bool non_blocking); private: + void refresh_panel (); void paint (); + void timer (); wxPanel* _panel; boost::shared_ptr<const Image> _image; + wxTimer _timer; + Position<int> _inter_position; + dcp::Size _inter_size; }; diff --git a/src/wx/standard_controls.cc b/src/wx/standard_controls.cc index 956f82c96..e9a31c86b 100644 --- a/src/wx/standard_controls.cc +++ b/src/wx/standard_controls.cc @@ -74,3 +74,19 @@ StandardControls::setup_sensitivity () bool const active_job = _active_job && *_active_job != "examine_content"; _play_button->Enable (_film && !_film->content().empty() && !active_job); } + +#ifdef DCPOMATIC_PLAYER_STRESS_TEST +void +StandardControls::play () +{ + _play_button->SetValue (true); + play_clicked (); +} + +void +StandardControls::stop () +{ + _play_button->SetValue (false); + play_clicked (); +} +#endif diff --git a/src/wx/standard_controls.h b/src/wx/standard_controls.h index b485c48cc..f79e4a178 100644 --- a/src/wx/standard_controls.h +++ b/src/wx/standard_controls.h @@ -25,6 +25,11 @@ class StandardControls : public Controls public: StandardControls (wxWindow* parent, boost::shared_ptr<FilmViewer> viewer, bool editor_controls); +#ifdef DCPOMATIC_PLAYER_STRESS_TEST + void play (); + void stop (); +#endif + private: void check_play_state (); void play_clicked (); diff --git a/src/wx/swaroop_controls.cc b/src/wx/swaroop_controls.cc index 5ce6c45fc..add9bf3e0 100644 --- a/src/wx/swaroop_controls.cc +++ b/src/wx/swaroop_controls.cc @@ -54,6 +54,7 @@ SwaroopControls::SwaroopControls (wxWindow* parent, shared_ptr<FilmViewer> viewe , _previous_button (new Button(this, "Previous")) , _current_disable_timeline (false) , _current_disable_next (false) + , _timer (this) { _button_sizer->Add (_previous_button, 0, wxEXPAND); _button_sizer->Add (_play_button, 0, wxEXPAND); @@ -112,10 +113,13 @@ SwaroopControls::SwaroopControls (wxWindow* parent, shared_ptr<FilmViewer> viewe _spl_view->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind(&SwaroopControls::spl_selection_changed, this)); _spl_view->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind(&SwaroopControls::spl_selection_changed, this)); _viewer->Finished.connect (boost::bind(&SwaroopControls::viewer_finished, this)); - _viewer->PositionChanged.connect (boost::bind(&SwaroopControls::viewer_position_changed, this)); _refresh_spl_view->Bind (wxEVT_BUTTON, boost::bind(&SwaroopControls::update_playlist_directory, this)); _refresh_content_view->Bind (wxEVT_BUTTON, boost::bind(&ContentView::update, _content_view)); + /* Write position every two minutes if we're playing */ + Bind (wxEVT_TIMER, boost::bind(&SwaroopControls::write_position, this)); + _timer.Start (2 * 60 * 1000, wxTIMER_CONTINUOUS); + _content_view->update (); update_playlist_directory (); @@ -153,10 +157,9 @@ SwaroopControls::check_restart () } void -SwaroopControls::viewer_position_changed () +SwaroopControls::write_position () { - /* Write position every two minutes if we're playing */ - if (!_selected_playlist || !_viewer->playing() || _viewer->position().get() % (2 * 60 * DCPTime::HZ)) { + if (!_selected_playlist || !_viewer->playing()) { return; } diff --git a/src/wx/swaroop_controls.h b/src/wx/swaroop_controls.h index 11dbcfc10..a8bb41dea 100644 --- a/src/wx/swaroop_controls.h +++ b/src/wx/swaroop_controls.h @@ -54,7 +54,7 @@ private: void setup_sensitivity (); void config_changed (int); void viewer_finished (); - void viewer_position_changed (); + void write_position (); void reset_film (); void update_current_content (); bool can_do_previous (); @@ -84,4 +84,6 @@ private: std::vector<SPL> _playlists; boost::optional<int> _selected_playlist; int _selected_playlist_position; + + wxTimer _timer; }; diff --git a/src/wx/timeline.cc b/src/wx/timeline.cc index daeeb0a51..9f71847f5 100644 --- a/src/wx/timeline.cc +++ b/src/wx/timeline.cc @@ -82,6 +82,7 @@ Timeline::Timeline (wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, w , _y_scroll_rate (16) , _pixels_per_track (48) , _first_resize (true) + , _timer (this) { #ifndef __WXOSX__ _labels_canvas->SetDoubleBuffered (true); @@ -116,16 +117,15 @@ Timeline::Timeline (wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, w _film_changed_connection = film->Change.connect (bind (&Timeline::film_change, this, _1, _2)); _film_content_change_connection = film->ContentChange.connect (bind (&Timeline::film_content_change, this, _1, _3, _4)); - shared_ptr<FilmViewer> vp = viewer.lock (); - DCPOMATIC_ASSERT (vp); - _viewer_position_change_connection = vp->PositionChanged.connect (bind(&Timeline::position_change, this)); + Bind (wxEVT_TIMER, boost::bind(&Timeline::update_playhead, this)); + _timer.Start (200, wxTIMER_CONTINUOUS); setup_scrollbars (); _labels_canvas->ShowScrollbars (wxSHOW_SB_NEVER, wxSHOW_SB_NEVER); } void -Timeline::position_change () +Timeline::update_playhead () { Refresh (); } diff --git a/src/wx/timeline.h b/src/wx/timeline.h index ef887dab8..44a897371 100644 --- a/src/wx/timeline.h +++ b/src/wx/timeline.h @@ -104,7 +104,7 @@ private: void set_pixels_per_second (double pps); void set_pixels_per_track (int h); void zoom_all (); - void position_change (); + void update_playhead (); boost::shared_ptr<TimelineView> event_to_view (wxMouseEvent &); TimelineContentViewList selected_views () const; @@ -137,11 +137,11 @@ private: int _y_scroll_rate; int _pixels_per_track; bool _first_resize; + wxTimer _timer; static double const _minimum_pixels_per_second; static int const _minimum_pixels_per_track; boost::signals2::scoped_connection _film_changed_connection; boost::signals2::scoped_connection _film_content_change_connection; - boost::signals2::scoped_connection _viewer_position_change_connection; }; diff --git a/src/wx/video_view.cc b/src/wx/video_view.cc new file mode 100644 index 000000000..4edc2cd23 --- /dev/null +++ b/src/wx/video_view.cc @@ -0,0 +1,133 @@ +/* + Copyright (C) 2019 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + +#include "video_view.h" +#include "wx_util.h" +#include "film_viewer.h" +#include "lib/butler.h" +#include <boost/optional.hpp> + +using boost::shared_ptr; +using boost::optional; + +VideoView::VideoView (FilmViewer* viewer) + : _viewer (viewer) +#ifdef DCPOMATIC_VARIANT_SWAROOP + , _in_watermark (false) +#endif + , _state_timer ("viewer") + , _video_frame_rate (0) + , _eyes (EYES_LEFT) + , _three_d (false) + , _dropped (0) + , _gets (0) +{ + +} + +void +VideoView::clear () +{ + boost::mutex::scoped_lock lm (_mutex); + _player_video.first.reset (); + _player_video.second = dcpomatic::DCPTime (); +} + +/** Could be called from any thread. + * @param non_blocking true to return false quickly if no video is available quickly. + * @return false if we gave up because it would take too long, otherwise true. + */ +bool +VideoView::get_next_frame (bool non_blocking) +{ + if (length() == dcpomatic::DCPTime()) { + return true; + } + + shared_ptr<Butler> butler = _viewer->butler (); + if (!butler) { + return false; + } + add_get (); + + boost::mutex::scoped_lock lm (_mutex); + + do { + Butler::Error e; + _player_video = butler->get_video (!non_blocking, &e); + if (!_player_video.first && e == Butler::AGAIN) { + return false; + } + } while ( + _player_video.first && + _three_d && + _eyes != _player_video.first->eyes() && + _player_video.first->eyes() != EYES_BOTH + ); + + return true; +} + +dcpomatic::DCPTime +VideoView::one_video_frame () const +{ + return dcpomatic::DCPTime::from_frames (1, video_frame_rate()); +} + +/** @return Time in ms until the next frame is due, or empty if nothing is due */ +optional<int> +VideoView::time_until_next_frame () const +{ + if (length() == dcpomatic::DCPTime()) { + /* There's no content, so this doesn't matter */ + return optional<int>(); + } + + dcpomatic::DCPTime const next = position() + one_video_frame(); + dcpomatic::DCPTime const time = _viewer->audio_time().get_value_or(position()); + if (next < time) { + return 0; + } + return (next.seconds() - time.seconds()) * 1000; +} + +void +VideoView::start () +{ + boost::mutex::scoped_lock lm (_mutex); + _dropped = 0; +} + +bool +VideoView::refresh_metadata (shared_ptr<const Film> film, dcp::Size video_container_size, dcp::Size film_frame_size) +{ + boost::mutex::scoped_lock lm (_mutex); + if (!_player_video.first) { + return false; + } + + if (!_player_video.first->reset_metadata (film, video_container_size, film_frame_size)) { + return false; + } + + update (); + return true; +} + diff --git a/src/wx/video_view.h b/src/wx/video_view.h index 892ffab12..f9e067043 100644 --- a/src/wx/video_view.h +++ b/src/wx/video_view.h @@ -21,32 +21,120 @@ #ifndef DCPOMATIC_VIDEO_VIEW_H #define DCPOMATIC_VIDEO_VIEW_H +#include "lib/dcpomatic_time.h" +#include "lib/timer.h" +#include "lib/types.h" +#include "lib/exception_store.h" #include <boost/shared_ptr.hpp> #include <boost/signals2.hpp> +#include <boost/thread.hpp> +#include <boost/noncopyable.hpp> class Image; class wxWindow; class FilmViewer; +class PlayerVideo; -class VideoView +class VideoView : public ExceptionStore, public boost::noncopyable { public: - VideoView (FilmViewer* viewer) - : _viewer (viewer) -#ifdef DCPOMATIC_VARIANT_SWAROOP - , _in_watermark (false) -#endif - {} - + VideoView (FilmViewer* viewer); virtual ~VideoView () {} - virtual void set_image (boost::shared_ptr<const Image> image) = 0; + /** @return the thing displaying the image */ virtual wxWindow* get () const = 0; + /** Re-make and display the image from the current _player_video */ virtual void update () = 0; + /** Called when playback starts */ + virtual void start (); + /** Called when playback stops */ + virtual void stop () {} + /** Get the next frame and display it; used after seek */ + virtual bool display_next_frame (bool) = 0; + + void clear (); + bool refresh_metadata (boost::shared_ptr<const Film> film, dcp::Size video_container_size, dcp::Size film_frame_size); + /** Emitted from the GUI thread when our display changes in size */ boost::signals2::signal<void()> Sized; + + /* Accessors for FilmViewer */ + + int dropped () const { + boost::mutex::scoped_lock lm (_mutex); + return _dropped; + } + + int gets () const { + boost::mutex::scoped_lock lm (_mutex); + return _gets; + } + + StateTimer const & state_timer () const { + return _state_timer; + } + + dcpomatic::DCPTime position () const { + boost::mutex::scoped_lock lm (_mutex); + return _player_video.second; + } + + + /* Setters for FilmViewer so it can tell us our state and + * we can then use (thread) safely. + */ + + void set_video_frame_rate (int r) { + boost::mutex::scoped_lock lm (_mutex); + _video_frame_rate = r; + } + + void set_length (dcpomatic::DCPTime len) { + boost::mutex::scoped_lock lm (_mutex); + _length = len; + } + + void set_eyes (Eyes eyes) { + boost::mutex::scoped_lock lm (_mutex); + _eyes = eyes; + } + + void set_three_d (bool t) { + boost::mutex::scoped_lock lm (_mutex); + _three_d = t; + } + protected: + bool get_next_frame (bool non_blocking); + boost::optional<int> time_until_next_frame () const; + dcpomatic::DCPTime one_video_frame () const; + + int video_frame_rate () const { + boost::mutex::scoped_lock lm (_mutex); + return _video_frame_rate; + } + + dcpomatic::DCPTime length () const { + boost::mutex::scoped_lock lm (_mutex); + return _length; + } + + std::pair<boost::shared_ptr<PlayerVideo>, dcpomatic::DCPTime> player_video () const { + boost::mutex::scoped_lock lm (_mutex); + return _player_video; + } + + void add_dropped () { + boost::mutex::scoped_lock lm (_mutex); + ++_dropped; + } + + void add_get () { + boost::mutex::scoped_lock lm (_mutex); + ++_gets; + } + FilmViewer* _viewer; #ifdef DCPOMATIC_VARIANT_SWAROOP @@ -54,6 +142,22 @@ protected: int _watermark_x; int _watermark_y; #endif + + StateTimer _state_timer; + +private: + /** Mutex protecting all the state in this class */ + mutable boost::mutex _mutex; + + std::pair<boost::shared_ptr<PlayerVideo>, dcpomatic::DCPTime> _player_video; + int _video_frame_rate; + /** length of the film we are playing, or 0 if there is none */ + dcpomatic::DCPTime _length; + Eyes _eyes; + bool _three_d; + + int _dropped; + int _gets; }; #endif diff --git a/src/wx/wscript b/src/wx/wscript index f4fc4927e..5dbf75ecb 100644 --- a/src/wx/wscript +++ b/src/wx/wscript @@ -145,6 +145,7 @@ sources = """ update_dialog.cc verify_dcp_dialog.cc video_panel.cc + video_view.cc video_waveform_dialog.cc video_waveform_plot.cc wx_util.cc |
