thread, hopefully making things more elegant and robust.
--- /dev/null
+#!/usr/bin/python3
+
+import random
+
+def make(dcp, output, seeks):
+ with open(output, 'w') as f:
+ # Open the DCP and start it playing
+ print("O %s" % dcp, file=f)
+ print("P", file=f)
+ for i in range(seeks):
+ # Wait a bit
+ print("W %d" % random.randint(500, 60000), file=f)
+ # Seek
+ print("K %d" % random.randint(0, 4095), file=f)
+ # Make sure we're still playing
+ print("P", file=f)
+ print("S", file=f)
+
+
+make("/home/carl/DCP/Examples/BohemianRhapsody_TLR-7_S_DE-XX_DE_51_2K_TCFG_20180514_TM_IOP_OV/", "boho", 64)
+
+
#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 ()
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 ();
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)
}
}
+void
+Film::playlist_length_change ()
+{
+ LengthChange ();
+}
+
void
Film::playlist_change (ChangeType type)
{
/** 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;
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 ();
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;
}
if (changed) {
- OrderChanged ();
+ OrderChange ();
}
+
+ /* The length might have changed, and that's good enough for this signal */
+ LengthChange ();
}
}
}
Change (CHANGE_TYPE_DONE);
+
+ LengthChange ();
}
void
}
/* This won't change order, so it does not need a sort */
+
+ LengthChange ();
}
void
}
}
+ Change (CHANGE_TYPE_DONE);
+
/* This won't change order, so it does not need a sort */
- Change (CHANGE_TYPE_DONE);
+ LengthChange ();
}
class FrameRateCandidate
/** 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;
#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
: wxApp ()
, _frame (0)
, _splash (0)
- {}
+ {
+#ifdef DCPOMATIC_LINUX
+ XInitThreads ();
+#endif
+ }
private:
#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
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,
, _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)
#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 ()
{
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 }
};
App ()
: wxApp ()
, _frame (0)
- {}
+ {
+#ifdef DCPOMATIC_LINUX
+ XInitThreads ();
+#endif
+ }
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 ()) {
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;
}
DOMFrame* _frame;
string _dcp_to_load;
+#ifdef DCPOMATIC_PLAYER_STRESS_TEST
+ boost::optional<string> _stress;
+#endif
};
IMPLEMENT_APP (App)
--- /dev/null
+#!/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)
+
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
, _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);
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));
SetSizerAndFit (sizer);
}
+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
};
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 */
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 ();
std::vector<wxString> _lines;
std::vector<DCPTextTrack> _tracks;
boost::weak_ptr<Butler> _butler;
- dcpomatic::DCPTime _last_update;
+ wxTimer _timer;
};
/*
- 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.
, _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);
_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));
_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 ();
}
void
-Controls::position_changed ()
+Controls::update_position ()
{
if (!_slider_being_moved) {
update_position_label ();
}
}
}
+
+#ifdef DCPOMATIC_PLAYER_STRESS_TEST
+void
+Controls::seek (int slider)
+{
+ _slider->SetValue (slider);
+ slider_moved (false);
+ slider_released ();
+}
+#endif
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 ();
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;
ClosedCaptionsDialog* _closed_captions_dialog;
+ wxTimer _timer;
+
boost::signals2::scoped_connection _film_change_connection;
boost::signals2::scoped_connection _config_changed_connection;
};
, _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()) {
}
_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> ());
/** 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;
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 */
}
_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;
}
_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;
resume ();
}
-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 ();
}
if (!quick_refresh()) {
slow_refresh ();
}
- PositionChanged ();
}
void
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 ();
}
}
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());
}
}
_playing = false;
+ _video_view->stop ();
Stopped (position());
+
+ _video_view->rethrow ();
return true;
}
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
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
_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 ();
{
#ifdef DCPOMATIC_VARIANT_SWAROOP
if (p == Config::PLAYER_BACKGROUND_IMAGE) {
- refresh_view ();
+ _video_view->update ();
return;
}
#endif
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
void
FilmViewer::seek_by (DCPTime by, bool accurate)
{
- seek (_video_position + by, accurate);
+ seek (_video_view->position() + by, accurate);
}
void
{
_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 ();
+}
+
+
#include "lib/config.h"
#include "lib/player_text.h"
#include "lib/timer.h"
+#include "lib/signaller.h"
#include <RtAudio.h>
#include <wx/wx.h>
/** @class FilmViewer
* @brief A wx widget to view a Film.
*/
-class FilmViewer
+class FilmViewer : public Signaller
{
public:
FilmViewer (wxWindow *);
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;
bool stop ();
void suspend ();
void resume ();
+
bool playing () const {
return _playing;
}
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 {
#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) */
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;
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.
*/
bool _background_image;
#endif
- StateTimer _state_timer;
- int _gets;
-
/** true if an get() is required next time we are idle */
bool _idle_get;
#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>
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")) {
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) {
}
}
-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");
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);
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 */
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);
}
glFlush();
+
+ boost::mutex::scoped_lock lm (_canvas_mutex);
_canvas->SwapBuffers();
}
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);
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));
+ }
+}
*/
#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
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;
};
#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>
using std::max;
using std::string;
using boost::optional;
+using boost::shared_ptr;
using namespace dcpomatic;
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 ();
#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();
}
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 ();
}
*/
#include "video_view.h"
+#include "lib/position.h"
+#include <dcp/types.h>
#include <wx/wx.h>
class FilmViewer;
}
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;
};
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
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 ();
, _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);
_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 ();
}
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;
}
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 ();
std::vector<SPL> _playlists;
boost::optional<int> _selected_playlist;
int _selected_playlist_position;
+
+ wxTimer _timer;
};
, _y_scroll_rate (16)
, _pixels_per_track (48)
, _first_resize (true)
+ , _timer (this)
{
#ifndef __WXOSX__
_labels_canvas->SetDoubleBuffered (true);
_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 ();
}
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;
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;
};
--- /dev/null
+/*
+ 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;
+}
+
#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
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
update_dialog.cc
verify_dcp_dialog.cc
video_panel.cc
+ video_view.cc
video_waveform_dialog.cc
video_waveform_plot.cc
wx_util.cc
opt.add_option('--workaround-gssapi', action='store_true', default=False, help='link to gssapi_krb5')
opt.add_option('--force-cpp11', action='store_true', default=False, help='force use of C++11')
opt.add_option('--variant', help='build variant (swaroop-studio, swaroop-theater)', choices=['swaroop-studio', 'swaroop-theater'])
+ opt.add_option('--enable-player-stress-test', action='store_true', default=False, help='build the player with stress testing enabled')
opt.add_option('--use-lld', action='store_true', default=False, help='use lld linker')
def configure(conf):
if conf.options.variant.startswith('swaroop-'):
conf.env.append_value('CXXFLAGS', '-DDCPOMATIC_VARIANT_SWAROOP')
+ if conf.options.enable_player_stress_test:
+ conf.env.append_value('CXXFLAGS', '-DDCPOMATIC_PLAYER_STRESS_TEST')
+
if conf.options.use_lld:
try:
conf.find_program('ld.lld')
conf.env['CXXFLAGS_AVCODEC'] = []
conf.env['CXXFLAGS_AVUTIL'] = []
+ if conf.env.TARGET_LINUX:
+ conf.env.LIB_X11 = ['X11']
+
# Boost
if conf.options.static_boost:
conf.env.STLIB_BOOST_THREAD = ['boost_thread']