summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2012-09-28 23:09:15 +0100
committerCarl Hetherington <cth@carlh.net>2012-09-28 23:09:15 +0100
commitc0ed407fb02891f0dd364e78b6192f0e6dbe1d8d (patch)
treecfa1abb09f891f220f15886b4cad2d1562f4a79c /src
parentd50fe6707c973d4a1397aa40b67ae753744ce748 (diff)
parentc252cb33a3ca8088fbe091af903a77ad8a098969 (diff)
Merge branch 'master' of /home/carl/git/dvdomatic
Diffstat (limited to 'src')
-rw-r--r--src/lib/check_hashes_job.cc104
-rw-r--r--src/lib/check_hashes_job.h33
-rw-r--r--src/lib/config.cc4
-rw-r--r--src/lib/config.h26
-rw-r--r--src/lib/dcp_video_frame.cc68
-rw-r--r--src/lib/dcp_video_frame.h10
-rw-r--r--src/lib/decoder.cc78
-rw-r--r--src/lib/decoder.h8
-rw-r--r--src/lib/encoder.cc36
-rw-r--r--src/lib/encoder.h14
-rw-r--r--src/lib/exceptions.h5
-rw-r--r--src/lib/ffmpeg_compatibility.cc109
-rw-r--r--src/lib/ffmpeg_decoder.cc12
-rw-r--r--src/lib/film.cc29
-rw-r--r--src/lib/film.h3
-rw-r--r--src/lib/film_state.cc12
-rw-r--r--src/lib/format.cc1
-rw-r--r--src/lib/image.cc31
-rw-r--r--src/lib/image.h4
-rw-r--r--src/lib/j2k_still_encoder.cc7
-rw-r--r--src/lib/j2k_wav_encoder.cc20
-rw-r--r--src/lib/j2k_wav_encoder.h4
-rw-r--r--src/lib/job.cc17
-rw-r--r--src/lib/job.h17
-rw-r--r--src/lib/job_manager.cc12
-rw-r--r--src/lib/job_manager.h1
-rw-r--r--src/lib/log.cc29
-rw-r--r--src/lib/log.h32
-rw-r--r--src/lib/make_dcp_job.cc7
-rw-r--r--src/lib/server.cc158
-rw-r--r--src/lib/server.h34
-rw-r--r--src/lib/tiff_encoder.cc2
-rw-r--r--src/lib/transcode_job.cc22
-rw-r--r--src/lib/transcode_job.h3
-rw-r--r--src/lib/util.cc290
-rw-r--r--src/lib/util.h78
-rw-r--r--src/lib/wscript9
-rw-r--r--src/tools/dvdomatic.cc12
-rw-r--r--src/tools/servomatic.cc194
-rw-r--r--src/tools/servomatic_cli.cc94
-rw-r--r--src/tools/servomatic_gui.cc151
-rw-r--r--src/tools/servomatictest.cc9
-rw-r--r--src/tools/wscript4
-rw-r--r--src/wscript1
-rw-r--r--src/wx/config_dialog.cc35
-rw-r--r--src/wx/config_dialog.h4
-rw-r--r--src/wx/dcp_range_dialog.cc2
-rw-r--r--src/wx/film_editor.cc15
-rw-r--r--src/wx/film_viewer.cc4
-rw-r--r--src/wx/filter_dialog.cc2
-rw-r--r--src/wx/gain_calculator_dialog.cc10
-rw-r--r--src/wx/job_manager_view.cc14
-rw-r--r--src/wx/job_wrapper.cc6
-rw-r--r--src/wx/properties_dialog.cc89
-rw-r--r--src/wx/properties_dialog.h39
-rw-r--r--src/wx/server_dialog.cc8
-rw-r--r--src/wx/server_dialog.h8
-rw-r--r--src/wx/wscript1
-rw-r--r--src/wx/wx_util.cc56
-rw-r--r--src/wx/wx_util.h25
60 files changed, 1528 insertions, 584 deletions
diff --git a/src/lib/check_hashes_job.cc b/src/lib/check_hashes_job.cc
new file mode 100644
index 000000000..5a927f752
--- /dev/null
+++ b/src/lib/check_hashes_job.cc
@@ -0,0 +1,104 @@
+/*
+ Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+
+ This program 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.
+
+ This program 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 this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <fstream>
+#include <boost/lexical_cast.hpp>
+#include <boost/filesystem.hpp>
+#include "check_hashes_job.h"
+#include "film_state.h"
+#include "options.h"
+#include "log.h"
+#include "job_manager.h"
+#include "ab_transcode_job.h"
+#include "transcode_job.h"
+
+using namespace std;
+using namespace boost;
+
+CheckHashesJob::CheckHashesJob (shared_ptr<const FilmState> s, shared_ptr<const Options> o, Log* l)
+ : Job (s, o, l)
+ , _bad (0)
+{
+
+}
+
+string
+CheckHashesJob::name () const
+{
+ stringstream s;
+ s << "Check hashes of " << _fs->name;
+ return s.str ();
+}
+
+void
+CheckHashesJob::run ()
+{
+ _bad = 0;
+
+ for (int i = 0; i < _fs->length; ++i) {
+ string const j2k_file = _opt->frame_out_path (i, false);
+ string const hash_file = j2k_file + ".md5";
+
+ ifstream ref (hash_file.c_str ());
+ string hash;
+ ref >> hash;
+
+ if (hash != md5_digest (j2k_file)) {
+ _log->log ("Frame " + lexical_cast<string> (i) + " has wrong hash; deleting.");
+ filesystem::remove (j2k_file);
+ filesystem::remove (hash_file);
+ ++_bad;
+ }
+
+ set_progress (float (i) / _fs->length);
+ }
+
+ if (_bad) {
+ shared_ptr<Job> tc;
+
+ if (_fs->dcp_ab) {
+ tc.reset (new ABTranscodeJob (_fs, _opt, _log));
+ } else {
+ tc.reset (new TranscodeJob (_fs, _opt, _log));
+ }
+
+ JobManager::instance()->add_after (shared_from_this(), tc);
+ JobManager::instance()->add_after (tc, shared_ptr<Job> (new CheckHashesJob (_fs, _opt, _log)));
+ }
+
+ set_progress (1);
+ set_state (FINISHED_OK);
+}
+
+string
+CheckHashesJob::status () const
+{
+ stringstream s;
+ s << Job::status ();
+ if (overall_progress() > 0) {
+ if (_bad == 0) {
+ s << "; no bad frames found";
+ } else if (_bad == 1) {
+ s << "; 1 bad frame found";
+ } else {
+ s << "; " << _bad << " bad frames found";
+ }
+ }
+ return s.str ();
+}
diff --git a/src/lib/check_hashes_job.h b/src/lib/check_hashes_job.h
new file mode 100644
index 000000000..b59cf031b
--- /dev/null
+++ b/src/lib/check_hashes_job.h
@@ -0,0 +1,33 @@
+/*
+ Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+
+ This program 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.
+
+ This program 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 this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "job.h"
+
+class CheckHashesJob : public Job
+{
+public:
+ CheckHashesJob (boost::shared_ptr<const FilmState> s, boost::shared_ptr<const Options> o, Log* l);
+
+ std::string name () const;
+ void run ();
+ std::string status () const;
+
+private:
+ int _bad;
+};
diff --git a/src/lib/config.cc b/src/lib/config.cc
index 53674645d..44d110689 100644
--- a/src/lib/config.cc
+++ b/src/lib/config.cc
@@ -76,7 +76,7 @@ Config::Config ()
} else if (k == "reference_filter") {
_reference_filters.push_back (Filter::from_id (v));
} else if (k == "server") {
- _servers.push_back (Server::create_from_metadata (v));
+ _servers.push_back (ServerDescription::create_from_metadata (v));
} else if (k == "screen") {
_screens.push_back (Screen::create_from_metadata (v));
} else if (k == "tms_ip") {
@@ -131,7 +131,7 @@ Config::write () const
f << "reference_filter " << (*i)->id () << "\n";
}
- for (vector<Server*>::const_iterator i = _servers.begin(); i != _servers.end(); ++i) {
+ for (vector<ServerDescription*>::const_iterator i = _servers.begin(); i != _servers.end(); ++i) {
f << "server " << (*i)->as_metadata () << "\n";
}
diff --git a/src/lib/config.h b/src/lib/config.h
index 14b541ee6..840dcdaef 100644
--- a/src/lib/config.h
+++ b/src/lib/config.h
@@ -28,7 +28,7 @@
#include <boost/shared_ptr.hpp>
#include <sigc++/signal.h>
-class Server;
+class ServerDescription;
class Screen;
class Scaler;
class Filter;
@@ -65,7 +65,7 @@ public:
}
/** @return J2K encoding servers to use */
- std::vector<Server*> servers () const {
+ std::vector<ServerDescription*> servers () const {
return _servers;
}
@@ -81,18 +81,22 @@ public:
return _reference_filters;
}
+ /** @return The IP address of a TMS that we can copy DCPs to */
std::string tms_ip () const {
return _tms_ip;
}
+ /** @return The path on a TMS that we should write DCPs to */
std::string tms_path () const {
return _tms_path;
}
+ /** @return User name to log into the TMS with */
std::string tms_user () const {
return _tms_user;
}
+ /** @return Password to log into the TMS with */
std::string tms_password () const {
return _tms_password;
}
@@ -126,7 +130,7 @@ public:
}
/** @param s New list of servers */
- void set_servers (std::vector<Server*> s) {
+ void set_servers (std::vector<ServerDescription*> s) {
_servers = s;
Changed ();
}
@@ -146,21 +150,25 @@ public:
Changed ();
}
+ /** @param i IP address of a TMS that we can copy DCPs to */
void set_tms_ip (std::string i) {
_tms_ip = i;
Changed ();
}
+ /** @param p Path on a TMS that we should write DCPs to */
void set_tms_path (std::string p) {
_tms_path = p;
Changed ();
}
+ /** @param u User name to log into the TMS with */
void set_tms_user (std::string u) {
_tms_user = u;
Changed ();
}
+ /** @param p Password to log into the TMS with */
void set_tms_password (std::string p) {
_tms_password = p;
Changed ();
@@ -188,22 +196,22 @@ private:
int _j2k_bandwidth;
/** J2K encoding servers to use */
- std::vector<Server *> _servers;
-
+ std::vector<ServerDescription *> _servers;
/** Screen definitions */
std::vector<boost::shared_ptr<Screen> > _screens;
-
/** Scaler to use for the "A" part of A/B comparisons */
Scaler const * _reference_scaler;
-
/** Filters to use for the "A" part of A/B comparisons */
std::vector<Filter const *> _reference_filters;
-
+ /** The IP address of a TMS that we can copy DCPs to */
std::string _tms_ip;
+ /** The path on a TMS that we should write DCPs to */
std::string _tms_path;
+ /** User name to log into the TMS with */
std::string _tms_user;
+ /** Password to log into the TMS with */
std::string _tms_password;
-
+ /** Our sound processor */
SoundProcessor const * _sound_processor;
/** Singleton instance, or 0 */
diff --git a/src/lib/dcp_video_frame.cc b/src/lib/dcp_video_frame.cc
index 24cdda2e6..da7133c4b 100644
--- a/src/lib/dcp_video_frame.cc
+++ b/src/lib/dcp_video_frame.cc
@@ -36,6 +36,7 @@
#include <iomanip>
#include <sstream>
#include <iostream>
+#include <fstream>
#include <unistd.h>
#include <errno.h>
#include <boost/array.hpp>
@@ -55,10 +56,6 @@
#include "image.h"
#include "log.h"
-#ifdef DEBUG_HASH
-#include <mhash.h>
-#endif
-
using namespace std;
using namespace boost;
@@ -255,12 +252,6 @@ DCPVideoFrame::encode_locally ()
/* Set event manager to null (openjpeg 1.3 bug) */
_cinfo->event_mgr = 0;
-#ifdef DEBUG_HASH
- md5_data ("J2K in X frame " + lexical_cast<string> (_frame), _image->comps[0].data, size * sizeof (int));
- md5_data ("J2K in Y frame " + lexical_cast<string> (_frame), _image->comps[1].data, size * sizeof (int));
- md5_data ("J2K in Z frame " + lexical_cast<string> (_frame), _image->comps[2].data, size * sizeof (int));
-#endif
-
/* Setup the encoder parameters using the current image and user parameters */
opj_setup_encoder (_cinfo, _parameters, _image);
@@ -271,13 +262,9 @@ DCPVideoFrame::encode_locally ()
throw EncodeError ("jpeg2000 encoding failed");
}
-#ifdef DEBUG_HASH
- md5_data ("J2K out frame " + lexical_cast<string> (_frame), _cio->buffer, cio_tell (_cio));
-#endif
-
{
stringstream s;
- s << "Finished locally-encoded frame " << _frame << " length " << cio_tell (_cio);
+ s << "Finished locally-encoded frame " << _frame;
_log->log (s.str ());
}
@@ -289,19 +276,16 @@ DCPVideoFrame::encode_locally ()
* @return Encoded data.
*/
shared_ptr<EncodedData>
-DCPVideoFrame::encode_remotely (Server const * serv)
+DCPVideoFrame::encode_remotely (ServerDescription const * serv)
{
asio::io_service io_service;
asio::ip::tcp::resolver resolver (io_service);
asio::ip::tcp::resolver::query query (serv->host_name(), boost::lexical_cast<string> (Config::instance()->server_port ()));
asio::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve (query);
- shared_ptr<asio::ip::tcp::socket> socket (new asio::ip::tcp::socket (io_service));
- socket->connect (*endpoint_iterator);
+ Socket socket;
-#ifdef DEBUG_HASH
- _input->hash ("Input for remote encoding (before sending)");
-#endif
+ socket.connect (*endpoint_iterator, 30);
stringstream s;
s << "encode "
@@ -320,29 +304,23 @@ DCPVideoFrame::encode_remotely (Server const * serv)
s << _input->line_size()[i] << " ";
}
- asio::write (*socket, asio::buffer (s.str().c_str(), s.str().length() + 1));
+ socket.write ((uint8_t *) s.str().c_str(), s.str().length() + 1, 30);
for (int i = 0; i < _input->components(); ++i) {
- asio::write (*socket, asio::buffer (_input->data()[i], _input->line_size()[i] * _input->lines(i)));
+ socket.write (_input->data()[i], _input->line_size()[i] * _input->lines(i), 30);
}
- SocketReader reader (socket);
-
char buffer[32];
- reader.read_indefinite ((uint8_t *) buffer, sizeof (buffer));
- reader.consume (strlen (buffer) + 1);
+ socket.read_indefinite ((uint8_t *) buffer, sizeof (buffer), 30);
+ socket.consume (strlen (buffer) + 1);
shared_ptr<EncodedData> e (new RemotelyEncodedData (atoi (buffer)));
/* now read the rest */
- reader.read_definite_and_consume (e->data(), e->size());
-
-#ifdef DEBUG_HASH
- e->hash ("Encoded image (after receiving)");
-#endif
+ socket.read_definite_and_consume (e->data(), e->size(), 30);
{
stringstream s;
- s << "Finished remotely-encoded frame " << _frame << " length " << e->size();
+ s << "Finished remotely-encoded frame " << _frame;
_log->log (s.str ());
}
@@ -367,29 +345,29 @@ EncodedData::write (shared_ptr<const Options> opt, int frame)
fwrite (_data, 1, _size, f);
fclose (f);
+ string const real_j2k = opt->frame_out_path (frame, false);
+
/* Rename the file from foo.j2c.tmp to foo.j2c now that it is complete */
- filesystem::rename (tmp_j2k, opt->frame_out_path (frame, false));
+ filesystem::rename (tmp_j2k, real_j2k);
+
+ /* Write a file containing the hash */
+ string const hash = real_j2k + ".md5";
+ ofstream h (hash.c_str());
+ h << md5_digest (_data, _size) << "\n";
+ h.close ();
}
/** Send this data to a socket.
* @param socket Socket
*/
void
-EncodedData::send (shared_ptr<asio::ip::tcp::socket> socket)
+EncodedData::send (shared_ptr<Socket> socket)
{
stringstream s;
s << _size;
- asio::write (*socket, asio::buffer (s.str().c_str(), s.str().length() + 1));
- asio::write (*socket, asio::buffer (_data, _size));
-}
-
-#ifdef DEBUG_HASH
-void
-EncodedData::hash (string n) const
-{
- md5_data (n, _data, _size);
+ socket->write ((uint8_t *) s.str().c_str(), s.str().length() + 1, 30);
+ socket->write (_data, _size, 30);
}
-#endif
/** @param s Size of data in bytes */
RemotelyEncodedData::RemotelyEncodedData (int s)
diff --git a/src/lib/dcp_video_frame.h b/src/lib/dcp_video_frame.h
index 464d48515..72f885e45 100644
--- a/src/lib/dcp_video_frame.h
+++ b/src/lib/dcp_video_frame.h
@@ -27,7 +27,7 @@
class FilmState;
class Options;
-class Server;
+class ServerDescription;
class Scaler;
class Image;
class Log;
@@ -48,13 +48,9 @@ public:
virtual ~EncodedData () {}
- void send (boost::shared_ptr<boost::asio::ip::tcp::socket>);
+ void send (boost::shared_ptr<Socket> socket);
void write (boost::shared_ptr<const Options>, int);
-#ifdef DEBUG_HASH
- void hash (std::string) const;
-#endif
-
/** @return data */
uint8_t* data () const {
return _data;
@@ -113,7 +109,7 @@ public:
virtual ~DCPVideoFrame ();
boost::shared_ptr<EncodedData> encode_locally ();
- boost::shared_ptr<EncodedData> encode_remotely (Server const *);
+ boost::shared_ptr<EncodedData> encode_remotely (ServerDescription const *);
int frame () const {
return _frame;
diff --git a/src/lib/decoder.cc b/src/lib/decoder.cc
index faee5bece..973582ca4 100644
--- a/src/lib/decoder.cc
+++ b/src/lib/decoder.cc
@@ -29,6 +29,8 @@ extern "C" {
#if (LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR >= 53 && LIBAVFILTER_VERSION_MINOR <= 77) || LIBAVFILTER_VERSION_MAJOR == 3
#include <libavfilter/avcodec.h>
#include <libavfilter/buffersink.h>
+#elif LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15
+#include <libavfilter/vsrc_buffer.h>
#endif
#include <libavformat/avio.h>
}
@@ -67,7 +69,9 @@ Decoder::Decoder (boost::shared_ptr<const FilmState> s, boost::shared_ptr<const
, _video_frame (0)
, _buffer_src_context (0)
, _buffer_sink_context (0)
+#if HAVE_SWRESAMPLE
, _swr_context (0)
+#endif
, _have_setup_video_filters (false)
, _delay_line (0)
, _delay_in_bytes (0)
@@ -83,10 +87,12 @@ Decoder::~Decoder ()
delete _delay_line;
}
+/** Start off a decode processing run */
void
Decoder::process_begin ()
{
if (_fs->audio_sample_rate != dcp_audio_sample_rate (_fs->audio_sample_rate)) {
+#if HAVE_SWRESAMPLE
_swr_context = swr_alloc_set_opts (
0,
audio_channel_layout(),
@@ -99,8 +105,13 @@ Decoder::process_begin ()
);
swr_init (_swr_context);
+#else
+ throw DecodeError ("Cannot resample audio as libswresample is not present");
+#endif
} else {
+#if HAVE_SWRESAMPLE
_swr_context = 0;
+#endif
}
_delay_in_bytes = _fs->audio_delay * _fs->audio_sample_rate * _fs->audio_channels * _fs->bytes_per_sample() / 1000;
@@ -110,9 +121,11 @@ Decoder::process_begin ()
_audio_frames_processed = 0;
}
+/** Finish off a decode processing run */
void
Decoder::process_end ()
{
+#if HAVE_SWRESAMPLE
if (_swr_context) {
int mop = 0;
@@ -139,6 +152,7 @@ Decoder::process_end ()
swr_free (&_swr_context);
}
+#endif
if (_delay_in_bytes < 0) {
uint8_t remainder[-_delay_in_bytes];
@@ -151,20 +165,22 @@ Decoder::process_end ()
in to get it to the right length.
*/
- int const audio_short_by_frames =
- (decoding_frames() * dcp_audio_sample_rate (_fs->audio_sample_rate) / _fs->frames_per_second)
+ int64_t const audio_short_by_frames =
+ ((int64_t) decoding_frames() * dcp_audio_sample_rate (_fs->audio_sample_rate) / _fs->frames_per_second)
- _audio_frames_processed;
- int bytes = audio_short_by_frames * _fs->audio_channels * _fs->bytes_per_sample();
-
- int const silence_size = 64 * 1024;
- uint8_t silence[silence_size];
- memset (silence, 0, silence_size);
-
- while (bytes) {
- int const t = min (bytes, silence_size);
- Audio (silence, t);
- bytes -= t;
+ if (audio_short_by_frames >= 0) {
+ int bytes = audio_short_by_frames * _fs->audio_channels * _fs->bytes_per_sample();
+
+ int const silence_size = 64 * 1024;
+ uint8_t silence[silence_size];
+ memset (silence, 0, silence_size);
+
+ while (bytes) {
+ int const t = min (bytes, silence_size);
+ Audio (silence, t);
+ bytes -= t;
+ }
}
}
@@ -227,10 +243,12 @@ Decoder::process_audio (uint8_t* data, int size)
/* Here's samples per channel */
int const samples = size / _fs->bytes_per_sample();
+#if HAVE_SWRESAMPLE
/* And here's frames (where 1 frame is a collection of samples, 1 for each channel,
so for 5.1 a frame would be 6 samples)
*/
int const frames = samples / _fs->audio_channels;
+#endif
/* Maybe apply gain */
if (_fs->audio_gain != 0) {
@@ -270,6 +288,7 @@ Decoder::process_audio (uint8_t* data, int size)
uint8_t* out_buffer = 0;
/* Maybe sample-rate convert */
+#if HAVE_SWRESAMPLE
if (_swr_context) {
uint8_t const * in[2] = {
@@ -297,6 +316,7 @@ Decoder::process_audio (uint8_t* data, int size)
data = out_buffer;
size = out_frames * _fs->audio_channels * _fs->bytes_per_sample();
}
+#endif
/* Update the number of audio frames we've pushed to the encoder */
_audio_frames_processed += size / (_fs->audio_channels * _fs->bytes_per_sample ());
@@ -339,10 +359,8 @@ Decoder::process_video (AVFrame* frame)
throw DecodeError ("could not push buffer into filter chain.");
}
-#else
+#elif LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15
-#if 0
-
AVRational par;
par.num = sample_aspect_ratio_numerator ();
par.den = sample_aspect_ratio_denominator ();
@@ -351,7 +369,7 @@ Decoder::process_video (AVFrame* frame)
throw DecodeError ("could not push buffer into filter chain.");
}
-#endif
+#else
if (av_buffersrc_write_frame (_buffer_src_context, frame) < 0) {
throw DecodeError ("could not push buffer into filter chain.");
@@ -359,13 +377,13 @@ Decoder::process_video (AVFrame* frame)
#endif
-#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR >= 23 && LIBAVFILTER_VERSION_MINOR <= 61
+#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR >= 15 && LIBAVFILTER_VERSION_MINOR <= 61
while (avfilter_poll_frame (_buffer_sink_context->inputs[0])) {
#else
while (av_buffersink_read (_buffer_sink_context, 0)) {
#endif
-#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 53
+#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR >= 15
int r = avfilter_request_frame (_buffer_sink_context->inputs[0]);
if (r < 0) {
@@ -434,10 +452,7 @@ Decoder::setup_video_filters ()
throw DecodeError ("Could not find buffer src filter");
}
- AVFilter* buffer_sink = avfilter_get_by_name("buffersink");
- if (buffer_sink == 0) {
- throw DecodeError ("Could not create buffer sink filter");
- }
+ AVFilter* buffer_sink = get_sink ();
stringstream a;
a << native_size().width << ":"
@@ -449,12 +464,18 @@ Decoder::setup_video_filters ()
<< sample_aspect_ratio_denominator();
int r;
+
if ((r = avfilter_graph_create_filter (&_buffer_src_context, buffer_src, "in", a.str().c_str(), 0, graph)) < 0) {
throw DecodeError ("could not create buffer source");
}
- enum PixelFormat pixel_formats[] = { pixel_format(), PIX_FMT_NONE };
- if (avfilter_graph_create_filter (&_buffer_sink_context, buffer_sink, "out", 0, pixel_formats, graph) < 0) {
+ AVBufferSinkParams* sink_params = av_buffersink_params_alloc ();
+ PixelFormat* pixel_fmts = new PixelFormat[2];
+ pixel_fmts[0] = pixel_format ();
+ pixel_fmts[1] = PIX_FMT_NONE;
+ sink_params->pixel_fmts = pixel_fmts;
+
+ if (avfilter_graph_create_filter (&_buffer_sink_context, buffer_sink, "out", 0, sink_params, graph) < 0) {
throw DecodeError ("could not create buffer sink.");
}
@@ -471,10 +492,17 @@ Decoder::setup_video_filters ()
inputs->next = 0;
_log->log ("Using filter chain `" + filters + "'");
+
+#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15
+ if (avfilter_graph_parse (graph, filters.c_str(), inputs, outputs, 0) < 0) {
+ throw DecodeError ("could not set up filter graph.");
+ }
+#else
if (avfilter_graph_parse (graph, filters.c_str(), &inputs, &outputs, 0) < 0) {
throw DecodeError ("could not set up filter graph.");
}
-
+#endif
+
if (avfilter_graph_config (graph, 0) < 0) {
throw DecodeError ("could not configure filter graph.");
}
diff --git a/src/lib/decoder.h b/src/lib/decoder.h
index 5c69e12d0..14b25c7b0 100644
--- a/src/lib/decoder.h
+++ b/src/lib/decoder.h
@@ -29,9 +29,11 @@
#include <stdint.h>
#include <boost/shared_ptr.hpp>
#include <sigc++/sigc++.h>
+#ifdef HAVE_SWRESAMPLE
extern "C" {
#include <libswresample/swresample.h>
-}
+}
+#endif
#include "util.h"
class Job;
@@ -132,7 +134,9 @@ private:
AVFilterContext* _buffer_src_context;
AVFilterContext* _buffer_sink_context;
+#if HAVE_SWRESAMPLE
SwrContext* _swr_context;
+#endif
bool _have_setup_video_filters;
DelayLine* _delay_line;
@@ -141,7 +145,7 @@ private:
/* Number of audio frames that we have pushed to the encoder
(at the DCP sample rate).
*/
- int _audio_frames_processed;
+ int64_t _audio_frames_processed;
};
#endif
diff --git a/src/lib/encoder.cc b/src/lib/encoder.cc
index c8eb24c80..62ba922da 100644
--- a/src/lib/encoder.cc
+++ b/src/lib/encoder.cc
@@ -36,6 +36,8 @@ Encoder::Encoder (shared_ptr<const FilmState> s, shared_ptr<const Options> o, Lo
: _fs (s)
, _opt (o)
, _log (l)
+ , _just_skipped (false)
+ , _last_frame (0)
{
}
@@ -58,10 +60,32 @@ Encoder::current_frames_per_second () const
return _history_size / (seconds (now) - seconds (_time_history.back ()));
}
+/** @return true if the last frame to be processed was skipped as it already existed */
+bool
+Encoder::skipping () const
+{
+ boost::mutex::scoped_lock (_history_mutex);
+ return _just_skipped;
+}
+
+/** @return Index of last frame to be successfully encoded */
+int
+Encoder::last_frame () const
+{
+ boost::mutex::scoped_lock (_history_mutex);
+ return _last_frame;
+}
+
+/** Should be called when a frame has been encoded successfully.
+ * @param n Frame index.
+ */
void
-Encoder::frame_done ()
+Encoder::frame_done (int n)
{
boost::mutex::scoped_lock lock (_history_mutex);
+ _just_skipped = false;
+ _last_frame = n;
+
struct timeval tv;
gettimeofday (&tv, 0);
_time_history.push_front (tv);
@@ -69,3 +93,13 @@ Encoder::frame_done ()
_time_history.pop_back ();
}
}
+
+/** Called by a subclass when it has just skipped the processing
+ of a frame because it has already been done.
+*/
+void
+Encoder::frame_skipped ()
+{
+ boost::mutex::scoped_lock lock (_history_mutex);
+ _just_skipped = true;
+}
diff --git a/src/lib/encoder.h b/src/lib/encoder.h
index bed2c0988..539b2912c 100644
--- a/src/lib/encoder.h
+++ b/src/lib/encoder.h
@@ -68,9 +68,12 @@ public:
virtual void process_end () = 0;
float current_frames_per_second () const;
+ bool skipping () const;
+ int last_frame () const;
protected:
- void frame_done ();
+ void frame_done (int n);
+ void frame_skipped ();
/** FilmState of the film that we are encoding */
boost::shared_ptr<const FilmState> _fs;
@@ -79,9 +82,18 @@ protected:
/** Log */
Log* _log;
+ /** Mutex for _time_history, _just_skipped and _last_frame */
mutable boost::mutex _history_mutex;
+ /** List of the times of completion of the last _history_size frames;
+ first is the most recently completed.
+ */
std::list<struct timeval> _time_history;
+ /** Number of frames that we should keep history for */
static int const _history_size;
+ /** true if the last frame we processed was skipped (because it was already done) */
+ bool _just_skipped;
+ /** Index of the last frame to be processed */
+ int _last_frame;
};
#endif
diff --git a/src/lib/exceptions.h b/src/lib/exceptions.h
index 6b567805b..8ef09875b 100644
--- a/src/lib/exceptions.h
+++ b/src/lib/exceptions.h
@@ -77,6 +77,9 @@ public:
class FileError : public StringError
{
public:
+ /** @param m Error message.
+ * @param f Name of the file that this exception concerns.
+ */
FileError (std::string m, std::string f)
: StringError (m)
, _file (f)
@@ -84,11 +87,13 @@ public:
virtual ~FileError () throw () {}
+ /** @return name of the file that this exception concerns */
std::string file () const {
return _file;
}
private:
+ /** name of the file that this exception concerns */
std::string _file;
};
diff --git a/src/lib/ffmpeg_compatibility.cc b/src/lib/ffmpeg_compatibility.cc
new file mode 100644
index 000000000..c47cdf5ce
--- /dev/null
+++ b/src/lib/ffmpeg_compatibility.cc
@@ -0,0 +1,109 @@
+/*
+ Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+
+ This program 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.
+
+ This program 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 this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+extern "C" {
+#include <libavfilter/avfiltergraph.h>
+}
+#include "exceptions.h"
+
+#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15
+
+typedef struct {
+ enum PixelFormat pix_fmt;
+} AVSinkContext;
+
+static int
+avsink_init (AVFilterContext* ctx, const char* args, void* opaque)
+{
+ AVSinkContext* priv = (AVSinkContext *) ctx->priv;
+ if (!opaque) {
+ return AVERROR (EINVAL);
+ }
+
+ *priv = *(AVSinkContext *) opaque;
+ return 0;
+}
+
+static void
+null_end_frame (AVFilterLink *)
+{
+
+}
+
+static int
+avsink_query_formats (AVFilterContext* ctx)
+{
+ AVSinkContext* priv = (AVSinkContext *) ctx->priv;
+ enum PixelFormat pix_fmts[] = {
+ priv->pix_fmt,
+ PIX_FMT_NONE
+ };
+
+ avfilter_set_common_formats (ctx, avfilter_make_format_list ((int *) pix_fmts));
+ return 0;
+}
+
+#endif
+
+AVFilter*
+get_sink ()
+{
+#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15
+ /* XXX does this leak stuff? */
+ AVFilter* buffer_sink = new AVFilter;
+ buffer_sink->name = av_strdup ("avsink");
+ buffer_sink->priv_size = sizeof (AVSinkContext);
+ buffer_sink->init = avsink_init;
+ buffer_sink->query_formats = avsink_query_formats;
+ buffer_sink->inputs = new AVFilterPad[2];
+ AVFilterPad* i0 = const_cast<AVFilterPad*> (&buffer_sink->inputs[0]);
+ i0->name = "default";
+ i0->type = AVMEDIA_TYPE_VIDEO;
+ i0->min_perms = AV_PERM_READ;
+ i0->rej_perms = 0;
+ i0->start_frame = 0;
+ i0->get_video_buffer = 0;
+ i0->get_audio_buffer = 0;
+ i0->end_frame = null_end_frame;
+ i0->draw_slice = 0;
+ i0->filter_samples = 0;
+ i0->poll_frame = 0;
+ i0->request_frame = 0;
+ i0->config_props = 0;
+ const_cast<AVFilterPad*> (&buffer_sink->inputs[1])->name = 0;
+ buffer_sink->outputs = new AVFilterPad[1];
+ const_cast<AVFilterPad*> (&buffer_sink->outputs[0])->name = 0;
+ return buffer_sink;
+#else
+ AVFilter* buffer_sink = avfilter_get_by_name("buffersink");
+ if (buffer_sink == 0) {
+ throw DecodeError ("Could not create buffer sink filter");
+ }
+
+ return buffer_sink;
+#endif
+}
+
+#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15
+AVFilterInOut *
+avfilter_inout_alloc ()
+{
+ return (AVFilterInOut *) av_malloc (sizeof (AVFilterInOut));
+}
+#endif
diff --git a/src/lib/ffmpeg_decoder.cc b/src/lib/ffmpeg_decoder.cc
index c12e6728d..3471ffaab 100644
--- a/src/lib/ffmpeg_decoder.cc
+++ b/src/lib/ffmpeg_decoder.cc
@@ -142,10 +142,18 @@ FFmpegDecoder::setup_audio ()
if (_audio_codec == 0) {
throw DecodeError ("could not find audio decoder");
}
-
+
if (avcodec_open2 (_audio_codec_context, _audio_codec, 0) < 0) {
throw DecodeError ("could not open audio decoder");
}
+
+ /* This is a hack; sometimes it seems that _audio_codec_context->channel_layout isn't set up,
+ so bodge it here. No idea why we should have to do this.
+ */
+
+ if (_audio_codec_context->channel_layout == 0) {
+ _audio_codec_context->channel_layout = av_get_default_channel_layout (audio_channels ());
+ }
}
bool
@@ -200,7 +208,7 @@ FFmpegDecoder::audio_channels () const
if (_audio_codec_context == 0) {
return 0;
}
-
+
return _audio_codec_context->channels;
}
diff --git a/src/lib/film.cc b/src/lib/film.cc
index f8a3b192d..583a15e19 100644
--- a/src/lib/film.cc
+++ b/src/lib/film.cc
@@ -19,6 +19,7 @@
#include <stdexcept>
#include <iostream>
+#include <algorithm>
#include <fstream>
#include <cstdlib>
#include <sstream>
@@ -47,6 +48,7 @@
#include "scaler.h"
#include "decoder_factory.h"
#include "config.h"
+#include "check_hashes_job.h"
using namespace std;
using namespace boost;
@@ -68,7 +70,7 @@ Film::Film (string d, bool must_exist)
filesystem::path p (filesystem::system_complete (d));
filesystem::path result;
- for(filesystem::path::iterator i = p.begin(); i != p.end(); ++i) {
+ for (filesystem::path::iterator i = p.begin(); i != p.end(); ++i) {
if (*i == "..") {
if (filesystem::is_symlink (result) || result.filename() == "..") {
result /= *i;
@@ -88,7 +90,7 @@ Film::Film (string d, bool must_exist)
read_metadata ();
- _log = new Log (_state.file ("log"));
+ _log = new FileLog (_state.file ("log"));
}
/** Copy constructor */
@@ -122,6 +124,10 @@ Film::read_metadata ()
continue;
}
+ if (line[line.size() - 1] == '\r') {
+ line = line.substr (0, line.size() - 1);
+ }
+
size_t const s = line.find (' ');
if (s == string::npos) {
continue;
@@ -429,7 +435,6 @@ Film::j2k_dir () const
filesystem::path p;
-
/* Start with j2c */
p /= "j2c";
@@ -540,7 +545,8 @@ Film::make_dcp (bool transcode, int freq)
JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (fs, o, log ())));
}
}
-
+
+ JobManager::instance()->add (shared_ptr<Job> (new CheckHashesJob (fs, o, log ())));
JobManager::instance()->add (shared_ptr<Job> (new MakeDCPJob (fs, o, log ())));
}
@@ -641,3 +647,18 @@ Film::copy_from_dvd ()
JobManager::instance()->add (j);
}
+int
+Film::encoded_frames () const
+{
+ if (format() == 0) {
+ return 0;
+ }
+
+ int N = 0;
+ for (filesystem::directory_iterator i = filesystem::directory_iterator (j2k_dir ()); i != filesystem::directory_iterator(); ++i) {
+ ++N;
+ this_thread::interruption_point ();
+ }
+
+ return N;
+}
diff --git a/src/lib/film.h b/src/lib/film.h
index 3ff671fbe..cd3b1b8a8 100644
--- a/src/lib/film.h
+++ b/src/lib/film.h
@@ -29,6 +29,7 @@
#include <vector>
#include <inttypes.h>
#include <boost/thread/mutex.hpp>
+#include <boost/thread.hpp>
#include <sigc++/signal.h>
extern "C" {
#include <libavcodec/avcodec.h>
@@ -229,6 +230,8 @@ public:
return _log;
}
+ int encoded_frames () const;
+
/** Emitted when some metadata property has changed */
mutable sigc::signal1<void, Property> Changed;
diff --git a/src/lib/film_state.cc b/src/lib/film_state.cc
index e0ad20417..e472434ce 100644
--- a/src/lib/film_state.cc
+++ b/src/lib/film_state.cc
@@ -165,11 +165,6 @@ FilmState::read_metadata (string k, string v)
} else if (k == "content_digest") {
content_digest = v;
}
-
- /* Itsy bitsy hack: compute digest here if don't have one (for backwards compatibility) */
- if (content_digest.empty() && !content.empty()) {
- content_digest = md5_digest (content_path ());
- }
}
@@ -256,10 +251,13 @@ ContentType
FilmState::content_type () const
{
#if BOOST_FILESYSTEM_VERSION == 3
- string const ext = filesystem::path(content).extension().string();
+ string ext = filesystem::path(content).extension().string();
#else
- string const ext = filesystem::path(content).extension();
+ string ext = filesystem::path(content).extension();
#endif
+
+ transform (ext.begin(), ext.end(), ext.begin(), ::tolower);
+
if (ext == ".tif" || ext == ".tiff" || ext == ".jpg" || ext == ".jpeg" || ext == ".png") {
return STILL;
}
diff --git a/src/lib/format.cc b/src/lib/format.cc
index ff3a5b202..e689aa05d 100644
--- a/src/lib/format.cc
+++ b/src/lib/format.cc
@@ -82,6 +82,7 @@ Format::setup_formats ()
_formats.push_back (new Format (133, Size (1998, 1080), "133-in-flat", "4:3 within Flat"));
_formats.push_back (new Format (137, Size (1480, 1080), "137", "Academy"));
_formats.push_back (new Format (166, Size (1793, 1080), "166", "1.66"));
+ _formats.push_back (new Format (166, Size (1998, 1080), "166-in-flat", "1.66 within Flat"));
_formats.push_back (new Format (178, Size (1998, 1080), "178-in-flat", "16:9 within Flat"));
_formats.push_back (new Format (185, Size (1998, 1080), "185", "Flat"));
_formats.push_back (new Format (239, Size (2048, 858), "239", "Scope"));
diff --git a/src/lib/image.cc b/src/lib/image.cc
index f16bb9f77..620e71aa7 100644
--- a/src/lib/image.cc
+++ b/src/lib/image.cc
@@ -39,10 +39,6 @@ extern "C" {
#include "exceptions.h"
#include "scaler.h"
-#ifdef DEBUG_HASH
-#include <mhash.h>
-#endif
-
using namespace std;
using namespace boost;
@@ -85,33 +81,6 @@ Image::components () const
return 0;
}
-#ifdef DEBUG_HASH
-/** Write a MD5 hash of the image's data to stdout.
- * @param n Title to give the output.
- */
-void
-Image::hash (string n) const
-{
- MHASH ht = mhash_init (MHASH_MD5);
- if (ht == MHASH_FAILED) {
- throw EncodeError ("could not create hash thread");
- }
-
- for (int i = 0; i < components(); ++i) {
- mhash (ht, data()[i], line_size()[i] * lines(i));
- }
-
- uint8_t hash[16];
- mhash_deinit (ht, hash);
-
- printf ("%s: ", n.c_str ());
- for (int i = 0; i < int (mhash_get_block_size (MHASH_MD5)); ++i) {
- printf ("%.2x", hash[i]);
- }
- printf ("\n");
-}
-#endif
-
/** Scale this image to a given size and convert it to RGB.
* @param out_size Output image size in pixels.
* @param scaler Scaler to use.
diff --git a/src/lib/image.h b/src/lib/image.h
index 97ab1d5ff..0161d2b01 100644
--- a/src/lib/image.h
+++ b/src/lib/image.h
@@ -68,10 +68,6 @@ public:
boost::shared_ptr<RGBFrameImage> scale_and_convert_to_rgb (Size, int, Scaler const *) const;
boost::shared_ptr<PostProcessImage> post_process (std::string) const;
-#ifdef DEBUG_HASH
- void hash (std::string) const;
-#endif
-
void make_black ();
PixelFormat pixel_format () const {
diff --git a/src/lib/j2k_still_encoder.cc b/src/lib/j2k_still_encoder.cc
index 5243f0668..8f3339a0a 100644
--- a/src/lib/j2k_still_encoder.cc
+++ b/src/lib/j2k_still_encoder.cc
@@ -67,12 +67,15 @@ J2KStillEncoder::process_video (shared_ptr<Image> yuv, int frame)
if (!boost::filesystem::exists (_opt->frame_out_path (i, false))) {
string const link = _opt->frame_out_path (i, false);
#ifdef DVDOMATIC_POSIX
- symlink (real.c_str(), link.c_str());
+ int const r = symlink (real.c_str(), link.c_str());
+ if (r) {
+ throw EncodeError ("could not create symlink");
+ }
#endif
#ifdef DVDOMATIC_WINDOWS
filesystem::copy_file (real, link);
#endif
}
- frame_done ();
+ frame_done (0);
}
}
diff --git a/src/lib/j2k_wav_encoder.cc b/src/lib/j2k_wav_encoder.cc
index 2f29f9021..9ae01c774 100644
--- a/src/lib/j2k_wav_encoder.cc
+++ b/src/lib/j2k_wav_encoder.cc
@@ -27,6 +27,7 @@
#include <iostream>
#include <boost/thread.hpp>
#include <boost/filesystem.hpp>
+#include <boost/lexical_cast.hpp>
#include <sndfile.h>
#include <openjpeg.h>
#include "j2k_wav_encoder.h"
@@ -126,11 +127,13 @@ J2KWAVEncoder::process_video (shared_ptr<Image> yuv, int frame)
));
_worker_condition.notify_all ();
+ } else {
+ frame_skipped ();
}
}
void
-J2KWAVEncoder::encoder_thread (Server* server)
+J2KWAVEncoder::encoder_thread (ServerDescription* server)
{
/* Number of seconds that we currently wait between attempts
to connect to the server; not relevant for localhost
@@ -190,7 +193,7 @@ J2KWAVEncoder::encoder_thread (Server* server)
if (encoded) {
encoded->write (_opt, vf->frame ());
- frame_done ();
+ frame_done (vf->frame ());
} else {
lock.lock ();
_queue.push_front (vf);
@@ -210,12 +213,12 @@ void
J2KWAVEncoder::process_begin ()
{
for (int i = 0; i < Config::instance()->num_local_encoding_threads (); ++i) {
- _worker_threads.push_back (new boost::thread (boost::bind (&J2KWAVEncoder::encoder_thread, this, (Server *) 0)));
+ _worker_threads.push_back (new boost::thread (boost::bind (&J2KWAVEncoder::encoder_thread, this, (ServerDescription *) 0)));
}
- vector<Server*> servers = Config::instance()->servers ();
+ vector<ServerDescription*> servers = Config::instance()->servers ();
- for (vector<Server*>::iterator i = servers.begin(); i != servers.end(); ++i) {
+ for (vector<ServerDescription*>::iterator i = servers.begin(); i != servers.end(); ++i) {
for (int j = 0; j < (*i)->threads (); ++j) {
_worker_threads.push_back (new boost::thread (boost::bind (&J2KWAVEncoder::encoder_thread, this, *i)));
}
@@ -227,8 +230,11 @@ J2KWAVEncoder::process_end ()
{
boost::mutex::scoped_lock lock (_worker_mutex);
+ _log->log ("Clearing queue of " + lexical_cast<string> (_queue.size ()));
+
/* Keep waking workers until the queue is empty */
while (!_queue.empty ()) {
+ _log->log ("Waking with " + lexical_cast<string> (_queue.size ()));
_worker_condition.notify_all ();
_worker_condition.wait (lock);
}
@@ -237,6 +243,8 @@ J2KWAVEncoder::process_end ()
terminate_worker_threads ();
+ _log->log ("Mopping up " + lexical_cast<string> (_queue.size()));
+
/* The following sequence of events can occur in the above code:
1. a remote worker takes the last image off the queue
2. the loop above terminates
@@ -253,7 +261,7 @@ J2KWAVEncoder::process_end ()
try {
shared_ptr<EncodedData> e = (*i)->encode_locally ();
e->write (_opt, (*i)->frame ());
- frame_done ();
+ frame_done ((*i)->frame ());
} catch (std::exception& e) {
stringstream s;
s << "Local encode failed " << e.what() << ".";
diff --git a/src/lib/j2k_wav_encoder.h b/src/lib/j2k_wav_encoder.h
index 656af8321..1c2f50065 100644
--- a/src/lib/j2k_wav_encoder.h
+++ b/src/lib/j2k_wav_encoder.h
@@ -29,7 +29,7 @@
#include <sndfile.h>
#include "encoder.h"
-class Server;
+class ServerDescription;
class DCPVideoFrame;
class Image;
class Log;
@@ -50,7 +50,7 @@ public:
private:
- void encoder_thread (Server *);
+ void encoder_thread (ServerDescription *);
void close_sound_files ();
void terminate_worker_threads ();
diff --git a/src/lib/job.cc b/src/lib/job.cc
index 0feb73d31..22754eb90 100644
--- a/src/lib/job.cc
+++ b/src/lib/job.cc
@@ -223,7 +223,7 @@ Job::set_error (string e)
_error = e;
}
-/** Set that this job's progress will always be unknown */
+/** Say that this job's progress will always be unknown */
void
Job::set_progress_unknown ()
{
@@ -231,16 +231,18 @@ Job::set_progress_unknown ()
_progress_unknown = true;
}
+/** @return Human-readable status of this job */
string
Job::status () const
{
float const p = overall_progress ();
int const t = elapsed_time ();
+ int const r = remaining_time ();
stringstream s;
- if (!finished () && p >= 0 && t > 10) {
- s << rint (p * 100) << "%; about " << seconds_to_approximate_hms (t / p - t) << " remaining";
- } else if (!finished () && t <= 10) {
+ if (!finished () && p >= 0 && t > 10 && r > 0) {
+ s << rint (p * 100) << "%; " << seconds_to_approximate_hms (r) << " remaining";
+ } else if (!finished () && (t <= 10 || r == 0)) {
s << rint (p * 100) << "%";
} else if (finished_ok ()) {
s << "OK (ran for " << seconds_to_hms (t) << ")";
@@ -250,3 +252,10 @@ Job::status () const
return s.str ();
}
+
+/** @return An estimate of the remaining time for this job, in seconds */
+int
+Job::remaining_time () const
+{
+ return elapsed_time() / overall_progress() - elapsed_time();
+}
diff --git a/src/lib/job.h b/src/lib/job.h
index 2a77f78f7..b39130479 100644
--- a/src/lib/job.h
+++ b/src/lib/job.h
@@ -26,6 +26,7 @@
#include <string>
#include <boost/thread/mutex.hpp>
+#include <boost/enable_shared_from_this.hpp>
#include <sigc++/sigc++.h>
class Log;
@@ -35,7 +36,7 @@ class Options;
/** @class Job
* @brief A parent class to represent long-running tasks which are run in their own thread.
*/
-class Job
+class Job : public boost::enable_shared_from_this<Job>
{
public:
Job (boost::shared_ptr<const FilmState> s, boost::shared_ptr<const Options> o, Log* l);
@@ -70,6 +71,9 @@ public:
protected:
+ virtual int remaining_time () const;
+
+ /** Description of a job's state */
enum State {
NEW, ///< the job hasn't been started yet
RUNNING, ///< the job is running
@@ -80,10 +84,11 @@ protected:
void set_state (State);
void set_error (std::string e);
+ /** FilmState for this job */
boost::shared_ptr<const FilmState> _fs;
+ /** options in use for this job */
boost::shared_ptr<const Options> _opt;
-
- /** A log that this job can write to */
+ /** a log that this job can write to */
Log* _log;
private:
@@ -92,11 +97,15 @@ private:
/** mutex for _state and _error */
mutable boost::mutex _state_mutex;
+ /** current state of the job */
State _state;
+ /** message for an error that has occurred (when state == FINISHED_ERROR) */
std::string _error;
+ /** time that this job was started */
time_t _start_time;
-
+
+ /** mutex for _stack and _progress_unknown */
mutable boost::mutex _progress_mutex;
struct Level {
diff --git a/src/lib/job_manager.cc b/src/lib/job_manager.cc
index 93fdbd27a..a166b5924 100644
--- a/src/lib/job_manager.cc
+++ b/src/lib/job_manager.cc
@@ -41,15 +41,23 @@ void
JobManager::add (shared_ptr<Job> j)
{
boost::mutex::scoped_lock lm (_mutex);
-
_jobs.push_back (j);
}
+void
+JobManager::add_after (shared_ptr<Job> after, shared_ptr<Job> j)
+{
+ boost::mutex::scoped_lock lm (_mutex);
+ list<shared_ptr<Job> >::iterator i = find (_jobs.begin(), _jobs.end(), after);
+ assert (i != _jobs.end ());
+ ++i;
+ _jobs.insert (i, j);
+}
+
list<shared_ptr<Job> >
JobManager::get () const
{
boost::mutex::scoped_lock lm (_mutex);
-
return _jobs;
}
diff --git a/src/lib/job_manager.h b/src/lib/job_manager.h
index f2f5e0057..d1d33cfc2 100644
--- a/src/lib/job_manager.h
+++ b/src/lib/job_manager.h
@@ -38,6 +38,7 @@ class JobManager
public:
void add (boost::shared_ptr<Job>);
+ void add_after (boost::shared_ptr<Job> after, boost::shared_ptr<Job> j);
std::list<boost::shared_ptr<Job> > get () const;
bool work_to_do () const;
diff --git a/src/lib/log.cc b/src/lib/log.cc
index accf3694d..7f1eea206 100644
--- a/src/lib/log.cc
+++ b/src/lib/log.cc
@@ -27,10 +27,8 @@
using namespace std;
-/** @param f Filename to write log to */
-Log::Log (string f)
- : _file (f)
- , _level (VERBOSE)
+Log::Log ()
+ : _level (VERBOSE)
{
}
@@ -45,13 +43,13 @@ Log::log (string m, Level l)
return;
}
- ofstream f (_file.c_str(), fstream::app);
-
time_t t;
time (&t);
string a = ctime (&t);
-
- f << a.substr (0, a.length() - 1) << ": " << m << "\n";
+
+ stringstream s;
+ s << a.substr (0, a.length() - 1) << ": " << m;
+ do_log (s.str ());
}
void
@@ -61,3 +59,18 @@ Log::set_level (Level l)
_level = l;
}
+
+/** @param file Filename to write log to */
+FileLog::FileLog (string file)
+ : _file (file)
+{
+
+}
+
+void
+FileLog::do_log (string m)
+{
+ ofstream f (_file.c_str(), fstream::app);
+ f << m << "\n";
+}
+
diff --git a/src/lib/log.h b/src/lib/log.h
index d4de8ebde..2a242e24c 100644
--- a/src/lib/log.h
+++ b/src/lib/log.h
@@ -17,6 +17,9 @@
*/
+#ifndef DVDOMATIC_LOG_H
+#define DVDOMATIC_LOG_H
+
/** @file src/log.h
* @brief A very simple logging class.
*/
@@ -26,15 +29,11 @@
/** @class Log
* @brief A very simple logging class.
- *
- * This class simply accepts log messages and writes them to a file.
- * Its single nod to complexity is that it has a mutex to prevent
- * multi-thread logging from clashing.
*/
class Log
{
public:
- Log (std::string f);
+ Log ();
enum Level {
STANDARD = 0,
@@ -45,11 +44,26 @@ public:
void set_level (Level l);
-private:
- /** mutex to prevent simultaneous writes to the file */
+protected:
+ /** mutex to protect the log */
boost::mutex _mutex;
- /** filename to write to */
- std::string _file;
+
+private:
+ virtual void do_log (std::string m) = 0;
+
/** level above which to ignore log messages */
Level _level;
};
+
+class FileLog : public Log
+{
+public:
+ FileLog (std::string file);
+
+private:
+ void do_log (std::string m);
+ /** filename to write to */
+ std::string _file;
+};
+
+#endif
diff --git a/src/lib/make_dcp_job.cc b/src/lib/make_dcp_job.cc
index 525f76c0e..8d3547cae 100644
--- a/src/lib/make_dcp_job.cc
+++ b/src/lib/make_dcp_job.cc
@@ -87,9 +87,12 @@ MakeDCPJob::run ()
break;
}
- libdcp::DCP dcp (_fs->dir (_fs->name), _fs->name, _fs->dcp_content_type->libdcp_kind (), rint (_fs->frames_per_second), frames);
+ libdcp::DCP dcp (_fs->dir (_fs->name));
dcp.Progress.connect (sigc::mem_fun (*this, &MakeDCPJob::dcp_progress));
+ shared_ptr<libdcp::CPL> cpl (new libdcp::CPL (_fs->dir (_fs->name), _fs->name, _fs->dcp_content_type->libdcp_kind (), frames, rint (_fs->frames_per_second)));
+ dcp.add_cpl (cpl);
+
descend (0.9);
shared_ptr<libdcp::MonoPictureAsset> pa (
new libdcp::MonoPictureAsset (
@@ -124,7 +127,7 @@ MakeDCPJob::run ()
ascend ();
}
- dcp.add_reel (shared_ptr<libdcp::Reel> (new libdcp::Reel (pa, sa, shared_ptr<libdcp::SubtitleAsset> ())));
+ cpl->add_reel (shared_ptr<libdcp::Reel> (new libdcp::Reel (pa, sa, shared_ptr<libdcp::SubtitleAsset> ())));
dcp.write_xml ();
set_progress (1);
diff --git a/src/lib/server.cc b/src/lib/server.cc
index 8a5b5cfca..f8c4425d9 100644
--- a/src/lib/server.cc
+++ b/src/lib/server.cc
@@ -19,24 +19,30 @@
/** @file src/server.cc
* @brief Class to describe a server to which we can send
- * encoding work.
+ * encoding work, and a class to implement such a server.
*/
#include <string>
#include <vector>
#include <sstream>
+#include <iostream>
#include <boost/algorithm/string.hpp>
#include "server.h"
+#include "util.h"
+#include "scaler.h"
+#include "image.h"
+#include "dcp_video_frame.h"
+#include "config.h"
using namespace std;
using namespace boost;
-/** Create a server from a string of metadata returned from as_metadata().
+/** Create a server description from a string of metadata returned from as_metadata().
* @param v Metadata.
- * @return Server, or 0.
+ * @return ServerDescription, or 0.
*/
-Server *
-Server::create_from_metadata (string v)
+ServerDescription *
+ServerDescription::create_from_metadata (string v)
{
vector<string> b;
split (b, v, is_any_of (" "));
@@ -45,14 +51,152 @@ Server::create_from_metadata (string v)
return 0;
}
- return new Server (b[0], atoi (b[1].c_str ()));
+ return new ServerDescription (b[0], atoi (b[1].c_str ()));
}
/** @return Description of this server as text */
string
-Server::as_metadata () const
+ServerDescription::as_metadata () const
{
stringstream s;
s << _host_name << " " << _threads;
return s.str ();
}
+
+Server::Server (Log* log)
+ : _log (log)
+{
+
+}
+
+int
+Server::process (shared_ptr<Socket> socket)
+{
+ char buffer[128];
+ socket->read_indefinite ((uint8_t *) buffer, sizeof (buffer), 30);
+ socket->consume (strlen (buffer) + 1);
+
+ stringstream s (buffer);
+
+ string command;
+ s >> command;
+ if (command != "encode") {
+ return -1;
+ }
+
+ Size in_size;
+ int pixel_format_int;
+ Size out_size;
+ int padding;
+ string scaler_id;
+ int frame;
+ float frames_per_second;
+ string post_process;
+ int colour_lut_index;
+ int j2k_bandwidth;
+
+ s >> in_size.width >> in_size.height
+ >> pixel_format_int
+ >> out_size.width >> out_size.height
+ >> padding
+ >> scaler_id
+ >> frame
+ >> frames_per_second
+ >> post_process
+ >> colour_lut_index
+ >> j2k_bandwidth;
+
+ PixelFormat pixel_format = (PixelFormat) pixel_format_int;
+ Scaler const * scaler = Scaler::from_id (scaler_id);
+ if (post_process == "none") {
+ post_process = "";
+ }
+
+ shared_ptr<SimpleImage> image (new SimpleImage (pixel_format, in_size));
+
+ for (int i = 0; i < image->components(); ++i) {
+ int line_size;
+ s >> line_size;
+ image->set_line_size (i, line_size);
+ }
+
+ for (int i = 0; i < image->components(); ++i) {
+ socket->read_definite_and_consume (image->data()[i], image->line_size()[i] * image->lines(i), 30);
+ }
+
+ DCPVideoFrame dcp_video_frame (image, out_size, padding, scaler, frame, frames_per_second, post_process, colour_lut_index, j2k_bandwidth, _log);
+ shared_ptr<EncodedData> encoded = dcp_video_frame.encode_locally ();
+ encoded->send (socket);
+
+ return frame;
+}
+
+void
+Server::worker_thread ()
+{
+ while (1) {
+ mutex::scoped_lock lock (_worker_mutex);
+ while (_queue.empty ()) {
+ _worker_condition.wait (lock);
+ }
+
+ shared_ptr<Socket> socket = _queue.front ();
+ _queue.pop_front ();
+
+ lock.unlock ();
+
+ int frame = -1;
+
+ struct timeval start;
+ gettimeofday (&start, 0);
+
+ try {
+ frame = process (socket);
+ } catch (std::exception& e) {
+ cerr << "Error: " << e.what() << "\n";
+ }
+
+ socket.reset ();
+
+ lock.lock ();
+
+ if (frame >= 0) {
+ struct timeval end;
+ gettimeofday (&end, 0);
+ stringstream s;
+ s << "Encoded frame " << frame << " in " << (seconds (end) - seconds (start));
+ _log->log (s.str ());
+ }
+
+ _worker_condition.notify_all ();
+ }
+}
+
+void
+Server::run (int num_threads)
+{
+ stringstream s;
+ s << "Server starting with " << num_threads << " threads.";
+ _log->log (s.str ());
+
+ for (int i = 0; i < num_threads; ++i) {
+ _worker_threads.push_back (new thread (bind (&Server::worker_thread, this)));
+ }
+
+ asio::io_service io_service;
+ asio::ip::tcp::acceptor acceptor (io_service, asio::ip::tcp::endpoint (asio::ip::tcp::v4(), Config::instance()->server_port ()));
+ while (1) {
+ shared_ptr<Socket> socket (new Socket);
+ acceptor.accept (socket->socket ());
+
+ mutex::scoped_lock lock (_worker_mutex);
+
+ /* Wait until the queue has gone down a bit */
+ while (int (_queue.size()) >= num_threads * 2) {
+ _worker_condition.wait (lock);
+ }
+
+ _queue.push_back (socket);
+ _worker_condition.notify_all ();
+ }
+}
diff --git a/src/lib/server.h b/src/lib/server.h
index d06df34e9..32ba8dc4b 100644
--- a/src/lib/server.h
+++ b/src/lib/server.h
@@ -19,21 +19,27 @@
/** @file src/server.h
* @brief Class to describe a server to which we can send
- * encoding work.
+ * encoding work, and a class to implement such a server.
*/
#include <string>
+#include <boost/thread.hpp>
+#include <boost/asio.hpp>
+#include <boost/thread/condition.hpp>
+#include "log.h"
-/** @class Server
+class Socket;
+
+/** @class ServerDescription
* @brief Class to describe a server to which we can send encoding work.
*/
-class Server
+class ServerDescription
{
public:
/** @param h Server host name or IP address in string form.
* @param t Number of threads to use on the server.
*/
- Server (std::string h, int t)
+ ServerDescription (std::string h, int t)
: _host_name (h)
, _threads (t)
{}
@@ -58,7 +64,7 @@ public:
std::string as_metadata () const;
- static Server * create_from_metadata (std::string v);
+ static ServerDescription * create_from_metadata (std::string v);
private:
/** server's host name */
@@ -66,3 +72,21 @@ private:
/** number of threads to use on the server */
int _threads;
};
+
+class Server
+{
+public:
+ Server (Log* log);
+
+ void run (int num_threads);
+
+private:
+ void worker_thread ();
+ int process (boost::shared_ptr<Socket> socket);
+
+ std::vector<boost::thread *> _worker_threads;
+ std::list<boost::shared_ptr<Socket> > _queue;
+ boost::mutex _worker_mutex;
+ boost::condition _worker_condition;
+ Log* _log;
+};
diff --git a/src/lib/tiff_encoder.cc b/src/lib/tiff_encoder.cc
index 2cf238006..19e34741d 100644
--- a/src/lib/tiff_encoder.cc
+++ b/src/lib/tiff_encoder.cc
@@ -73,5 +73,5 @@ TIFFEncoder::process_video (shared_ptr<Image> image, int frame)
TIFFClose (output);
boost::filesystem::rename (tmp_file, _opt->frame_out_path (frame, false));
- frame_done ();
+ frame_done (frame);
}
diff --git a/src/lib/transcode_job.cc b/src/lib/transcode_job.cc
index 652a18441..9113593f0 100644
--- a/src/lib/transcode_job.cc
+++ b/src/lib/transcode_job.cc
@@ -78,7 +78,6 @@ TranscodeJob::run ()
_log->log (s.str ());
throw;
-
}
}
@@ -88,13 +87,30 @@ TranscodeJob::status () const
if (!_encoder) {
return "0%";
}
+
+ if (_encoder->skipping () && !finished ()) {
+ return "skipping already-encoded frames";
+ }
+
float const fps = _encoder->current_frames_per_second ();
if (fps == 0) {
return Job::status ();
}
-
+
stringstream s;
- s << Job::status () << "; about " << fixed << setprecision (1) << fps << " frames per second.";
+
+ s << Job::status () << "; " << fixed << setprecision (1) << fps << " frames per second";
return s.str ();
}
+
+int
+TranscodeJob::remaining_time () const
+{
+ float fps = _encoder->current_frames_per_second ();
+ if (fps == 0) {
+ return 0;
+ }
+
+ return ((_fs->length - _encoder->last_frame()) / fps);
+}
diff --git a/src/lib/transcode_job.h b/src/lib/transcode_job.h
index aa640f697..737f10de9 100644
--- a/src/lib/transcode_job.h
+++ b/src/lib/transcode_job.h
@@ -38,6 +38,9 @@ public:
void run ();
std::string status () const;
+protected:
+ int remaining_time () const;
+
private:
boost::shared_ptr<Encoder> _encoder;
};
diff --git a/src/lib/util.cc b/src/lib/util.cc
index 1478bab2e..935566440 100644
--- a/src/lib/util.cc
+++ b/src/lib/util.cc
@@ -33,6 +33,8 @@
#include <libssh/libssh.h>
#include <signal.h>
#include <boost/algorithm/string.hpp>
+#include <boost/bind.hpp>
+#include <boost/lambda/lambda.hpp>
#include <openjpeg.h>
#include <openssl/md5.h>
#include <magick/MagickCore.h>
@@ -59,10 +61,6 @@ extern "C" {
#include "player_manager.h"
#endif
-#ifdef DEBUG_HASH
-#include <mhash.h>
-#endif
-
using namespace std;
using namespace boost;
@@ -286,88 +284,6 @@ seconds (struct timeval t)
return t.tv_sec + (double (t.tv_usec) / 1e6);
}
-/** @param socket Socket to read from */
-SocketReader::SocketReader (shared_ptr<asio::ip::tcp::socket> socket)
- : _socket (socket)
- , _buffer_data (0)
-{
-
-}
-
-/** Mark some data as being `consumed', so that it will not be returned
- * as data again.
- * @param size Amount of data to consume, in bytes.
- */
-void
-SocketReader::consume (int size)
-{
- assert (_buffer_data >= size);
-
- _buffer_data -= size;
- if (_buffer_data > 0) {
- /* Shift still-valid data to the start of the buffer */
- memmove (_buffer, _buffer + size, _buffer_data);
- }
-}
-
-/** Read a definite amount of data from our socket, and mark
- * it as consumed.
- * @param data Where to put the data.
- * @param size Number of bytes to read.
- */
-void
-SocketReader::read_definite_and_consume (uint8_t* data, int size)
-{
- int const from_buffer = min (_buffer_data, size);
- if (from_buffer > 0) {
- /* Get data from our buffer */
- memcpy (data, _buffer, from_buffer);
- consume (from_buffer);
- /* Update our output state */
- data += from_buffer;
- size -= from_buffer;
- }
-
- /* read() the rest */
- while (size > 0) {
- int const n = asio::read (*_socket, asio::buffer (data, size));
- if (n <= 0) {
- throw NetworkError ("could not read");
- }
-
- data += n;
- size -= n;
- }
-}
-
-/** Read as much data as is available, up to some limit.
- * @param data Where to put the data.
- * @param size Maximum amount of data to read.
- */
-void
-SocketReader::read_indefinite (uint8_t* data, int size)
-{
- assert (size < int (sizeof (_buffer)));
-
- /* Amount of extra data we need to read () */
- int to_read = size - _buffer_data;
- while (to_read > 0) {
- /* read as much of it as we can (into our buffer) */
- int const n = asio::read (*_socket, asio::buffer (_buffer + _buffer_data, to_read));
- if (n <= 0) {
- throw NetworkError ("could not read");
- }
-
- to_read -= n;
- _buffer_data += n;
- }
-
- assert (_buffer_data >= size);
-
- /* copy data into the output buffer */
- assert (size >= _buffer_data);
- memcpy (data, _buffer, size);
-}
#ifdef DVDOMATIC_POSIX
void
@@ -427,28 +343,26 @@ split_at_spaces_considering_quotes (string s)
return out;
}
-#ifdef DEBUG_HASH
-void
-md5_data (string title, void const * data, int size)
+string
+md5_digest (void const * data, int size)
{
- MHASH ht = mhash_init (MHASH_MD5);
- if (ht == MHASH_FAILED) {
- throw EncodeError ("could not create hash thread");
- }
-
- mhash (ht, data, size);
-
- uint8_t hash[16];
- mhash_deinit (ht, hash);
+ MD5_CTX md5_context;
+ MD5_Init (&md5_context);
+ MD5_Update (&md5_context, data, size);
+ unsigned char digest[MD5_DIGEST_LENGTH];
+ MD5_Final (digest, &md5_context);
- printf ("%s [%d]: ", title.c_str (), size);
- for (int i = 0; i < int (mhash_get_block_size (MHASH_MD5)); ++i) {
- printf ("%.2x", hash[i]);
+ stringstream s;
+ for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
+ s << hex << setfill('0') << setw(2) << ((int) digest[i]);
}
- printf ("\n");
+
+ return s.str ();
}
-#endif
+/** @param file File name.
+ * @return MD5 digest of file's contents.
+ */
string
md5_digest (string file)
{
@@ -484,6 +398,9 @@ md5_digest (string file)
return s.str ();
}
+/** @param An arbitrary sampling rate.
+ * @return The appropriate DCP-approved sampling rate (48kHz or 96kHz).
+ */
int
dcp_audio_sample_rate (int fs)
{
@@ -504,6 +421,9 @@ bool operator!= (Crop const & a, Crop const & b)
return !(a == b);
}
+/** @param index Colour LUT index.
+ * @return Human-readable name.
+ */
string
colour_lut_index_to_name (int index)
{
@@ -518,5 +438,165 @@ colour_lut_index_to_name (int index)
return "";
}
-
-
+Socket::Socket ()
+ : _deadline (_io_service)
+ , _socket (_io_service)
+ , _buffer_data (0)
+{
+ _deadline.expires_at (posix_time::pos_infin);
+ check ();
+}
+
+void
+Socket::check ()
+{
+ if (_deadline.expires_at() <= asio::deadline_timer::traits_type::now ()) {
+ _socket.close ();
+ _deadline.expires_at (posix_time::pos_infin);
+ }
+
+ _deadline.async_wait (boost::bind (&Socket::check, this));
+}
+
+/** Blocking connect with timeout.
+ * @param endpoint End-point to connect to.
+ * @param timeout Time-out in seconds.
+ */
+void
+Socket::connect (asio::ip::basic_resolver_entry<asio::ip::tcp> const & endpoint, int timeout)
+{
+ system::error_code ec = asio::error::would_block;
+ _socket.async_connect (endpoint, lambda::var(ec) = lambda::_1);
+ do {
+ _io_service.run_one();
+ } while (ec == asio::error::would_block);
+
+ if (ec || !_socket.is_open ()) {
+ throw NetworkError ("connect timed out");
+ }
+}
+
+/** Blocking write with timeout.
+ * @param data Buffer to write.
+ * @param size Number of bytes to write.
+ * @param timeout Time-out, in seconds.
+ */
+void
+Socket::write (uint8_t const * data, int size, int timeout)
+{
+ _deadline.expires_from_now (posix_time::seconds (timeout));
+ system::error_code ec = asio::error::would_block;
+
+ asio::async_write (_socket, asio::buffer (data, size), lambda::var(ec) = lambda::_1);
+ do {
+ _io_service.run_one ();
+ } while (ec == asio::error::would_block);
+
+ if (ec) {
+ throw NetworkError ("write timed out");
+ }
+}
+
+/** Blocking read with timeout.
+ * @param data Buffer to read to.
+ * @param size Number of bytes to read.
+ * @param timeout Time-out, in seconds.
+ */
+int
+Socket::read (uint8_t* data, int size, int timeout)
+{
+ _deadline.expires_from_now (posix_time::seconds (timeout));
+ system::error_code ec = asio::error::would_block;
+
+ int amount_read = 0;
+
+ _socket.async_read_some (
+ asio::buffer (data, size),
+ (lambda::var(ec) = lambda::_1, lambda::var(amount_read) = lambda::_2)
+ );
+
+ do {
+ _io_service.run_one ();
+ } while (ec == asio::error::would_block);
+
+ if (ec) {
+ amount_read = 0;
+ }
+
+ return amount_read;
+}
+
+/** Mark some data as being `consumed', so that it will not be returned
+ * as data again.
+ * @param size Amount of data to consume, in bytes.
+ */
+void
+Socket::consume (int size)
+{
+ assert (_buffer_data >= size);
+
+ _buffer_data -= size;
+ if (_buffer_data > 0) {
+ /* Shift still-valid data to the start of the buffer */
+ memmove (_buffer, _buffer + size, _buffer_data);
+ }
+}
+
+/** Read a definite amount of data from our socket, and mark
+ * it as consumed.
+ * @param data Where to put the data.
+ * @param size Number of bytes to read.
+ */
+void
+Socket::read_definite_and_consume (uint8_t* data, int size, int timeout)
+{
+ int const from_buffer = min (_buffer_data, size);
+ if (from_buffer > 0) {
+ /* Get data from our buffer */
+ memcpy (data, _buffer, from_buffer);
+ consume (from_buffer);
+ /* Update our output state */
+ data += from_buffer;
+ size -= from_buffer;
+ }
+
+ /* read() the rest */
+ while (size > 0) {
+ int const n = read (data, size, timeout);
+ if (n <= 0) {
+ throw NetworkError ("could not read");
+ }
+
+ data += n;
+ size -= n;
+ }
+}
+
+/** Read as much data as is available, up to some limit.
+ * @param data Where to put the data.
+ * @param size Maximum amount of data to read.
+ */
+void
+Socket::read_indefinite (uint8_t* data, int size, int timeout)
+{
+ assert (size < int (sizeof (_buffer)));
+
+ /* Amount of extra data we need to read () */
+ int to_read = size - _buffer_data;
+ while (to_read > 0) {
+ /* read as much of it as we can (into our buffer) */
+ int const n = read (_buffer + _buffer_data, to_read, timeout);
+ if (n <= 0) {
+ throw NetworkError ("could not read");
+ }
+
+ to_read -= n;
+ _buffer_data += n;
+ }
+
+ assert (_buffer_data >= size);
+
+ /* copy data into the output buffer */
+ assert (size >= _buffer_data);
+ memcpy (data, _buffer, size);
+}
diff --git a/src/lib/util.h b/src/lib/util.h
index 568fe05d0..bc5a00fc4 100644
--- a/src/lib/util.h
+++ b/src/lib/util.h
@@ -46,39 +46,13 @@ extern double seconds (struct timeval);
extern void dvdomatic_setup ();
extern std::vector<std::string> split_at_spaces_considering_quotes (std::string);
extern std::string md5_digest (std::string);
+extern std::string md5_digest (void const *, int);
enum ContentType {
STILL,
VIDEO
};
-#ifdef DEBUG_HASH
-extern void md5_data (std::string, void const *, int);
-#endif
-
-/** @class SocketReader
- * @brief A helper class from reading from sockets.
- *
- * You can probably do this stuff directly in boost, but I'm not sure how.
- */
-class SocketReader
-{
-public:
- SocketReader (boost::shared_ptr<boost::asio::ip::tcp::socket>);
-
- void read_definite_and_consume (uint8_t *, int);
- void read_indefinite (uint8_t *, int);
- void consume (int);
-
-private:
- /** socket we are reading from */
- boost::shared_ptr<boost::asio::ip::tcp::socket> _socket;
- /** a buffer for small reads */
- uint8_t _buffer[256];
- /** amount of valid data in the buffer */
- int _buffer_data;
-};
-
/** @class Size
* @brief Representation of the size of something */
struct Size
@@ -103,19 +77,25 @@ struct Size
int height;
};
+/** A description of the crop of an image or video. */
struct Crop
{
Crop () : left (0), right (0), top (0), bottom (0) {}
-
+
+ /** Number of pixels to remove from the left-hand side */
int left;
+ /** Number of pixels to remove from the right-hand side */
int right;
+ /** Number of pixels to remove from the top */
int top;
+ /** Number of pixels to remove from the bottom */
int bottom;
};
extern bool operator== (Crop const & a, Crop const & b);
extern bool operator!= (Crop const & a, Crop const & b);
+/** A position */
struct Position
{
Position ()
@@ -128,7 +108,9 @@ struct Position
, y (y_)
{}
+ /** x coordinate */
int x;
+ /** y coordinate */
int y;
};
@@ -136,4 +118,44 @@ extern std::string crop_string (Position, Size);
extern int dcp_audio_sample_rate (int);
extern std::string colour_lut_index_to_name (int index);
+/** @class Socket
+ * @brief A class to wrap a boost::asio::ip::tcp::socket with some things
+ * that are useful for DVD-o-matic.
+ *
+ * This class wraps some things that I could not work out how to do with boost;
+ * most notably, sync read/write calls with timeouts, and the ability to peak into
+ * data being read.
+ */
+class Socket
+{
+public:
+ Socket ();
+
+ /** @return Our underlying socket */
+ boost::asio::ip::tcp::socket& socket () {
+ return _socket;
+ }
+
+ void connect (boost::asio::ip::basic_resolver_entry<boost::asio::ip::tcp> const & endpoint, int timeout);
+ void write (uint8_t const * data, int size, int timeout);
+
+ void read_definite_and_consume (uint8_t* data, int size, int timeout);
+ void read_indefinite (uint8_t* data, int size, int timeout);
+ void consume (int amount);
+
+private:
+ void check ();
+ int read (uint8_t* data, int size, int timeout);
+
+ Socket (Socket const &);
+
+ boost::asio::io_service _io_service;
+ boost::asio::deadline_timer _deadline;
+ boost::asio::ip::tcp::socket _socket;
+ /** a buffer for small reads */
+ uint8_t _buffer[256];
+ /** amount of valid data in the buffer */
+ int _buffer_data;
+};
+
#endif
diff --git a/src/lib/wscript b/src/lib/wscript
index b001fff2a..c809226ce 100644
--- a/src/lib/wscript
+++ b/src/lib/wscript
@@ -1,8 +1,3 @@
-def configure(conf):
- if conf.options.debug_hash:
- conf.env.append_value('CXXFLAGS', '-DDEBUG_HASH')
- conf.check_cc(msg = 'Checking for library libmhash', function_name = 'mhash_init', header_name = 'mhash.h', lib = 'mhash', uselib_store = 'MHASH')
-
def build(bld):
obj = bld(features = 'cxx cxxshlib')
obj.name = 'libdvdomatic'
@@ -10,11 +5,10 @@ def build(bld):
obj.uselib = 'AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE SWRESAMPLE SNDFILE BOOST_FILESYSTEM BOOST_THREAD OPENJPEG POSTPROC TIFF SIGC++ MAGICK SSH DCP GLIB'
if bld.env.TARGET_WINDOWS:
obj.uselib += ' WINSOCK2'
- if bld.env.DEBUG_HASH:
- obj.uselib += ' MHASH'
obj.source = """
ab_transcode_job.cc
ab_transcoder.cc
+ check_hashes_job.cc
config.cc
copy_from_dvd_job.cc
cross.cc
@@ -28,6 +22,7 @@ def build(bld):
encoder.cc
encoder_factory.cc
examine_content_job.cc
+ ffmpeg_compatibility.cc
ffmpeg_decoder.cc
film.cc
film_state.cc
diff --git a/src/tools/dvdomatic.cc b/src/tools/dvdomatic.cc
index df38e9d69..c42321300 100644
--- a/src/tools/dvdomatic.cc
+++ b/src/tools/dvdomatic.cc
@@ -30,6 +30,7 @@
//#include "gtk/dvd_title_dialog.h"
#include "wx/wx_util.h"
#include "wx/new_film_dialog.h"
+#include "wx/properties_dialog.h"
#include "lib/film.h"
#include "lib/format.h"
#include "lib/config.h"
@@ -125,6 +126,7 @@ enum {
ID_file_new = 1,
ID_file_open,
ID_file_save,
+ ID_file_properties,
ID_file_quit,
ID_edit_preferences,
ID_jobs_make_dcp,
@@ -144,6 +146,8 @@ setup_menu (wxMenuBar* m)
file->AppendSeparator ();
add_item (file, "&Save", ID_file_save, NEEDS_FILM);
file->AppendSeparator ();
+ add_item (file, "&Properties...", ID_file_properties, NEEDS_FILM);
+ file->AppendSeparator ();
add_item (file, "&Quit", ID_file_quit, ALWAYS);
wxMenu* edit = new wxMenu;
@@ -188,6 +192,7 @@ public:
Connect (ID_file_new, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::file_new));
Connect (ID_file_open, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::file_open));
Connect (ID_file_save, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::file_save));
+ Connect (ID_file_properties, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::file_properties));
Connect (ID_file_quit, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::file_quit));
Connect (ID_edit_preferences, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::edit_preferences));
Connect (ID_jobs_make_dcp, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::jobs_make_dcp));
@@ -285,6 +290,13 @@ public:
{
film->write_metadata ();
}
+
+ void file_properties (wxCommandEvent &)
+ {
+ PropertiesDialog* d = new PropertiesDialog (this, film);
+ d->ShowModal ();
+ d->Destroy ();
+ }
void file_quit (wxCommandEvent &)
{
diff --git a/src/tools/servomatic.cc b/src/tools/servomatic.cc
deleted file mode 100644
index a9c45b3df..000000000
--- a/src/tools/servomatic.cc
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
-
- This program 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.
-
- This program 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 this program; if not, write to the Free Software
- Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-
-*/
-
-#include <iostream>
-#include <stdexcept>
-#include <sstream>
-#include <cstring>
-#include <vector>
-#include <unistd.h>
-#include <errno.h>
-#include <boost/array.hpp>
-#include <boost/asio.hpp>
-#include <boost/algorithm/string.hpp>
-#include <boost/thread.hpp>
-#include <boost/thread/mutex.hpp>
-#include <boost/thread/condition.hpp>
-#include "config.h"
-#include "dcp_video_frame.h"
-#include "exceptions.h"
-#include "util.h"
-#include "config.h"
-#include "scaler.h"
-#include "image.h"
-#include "log.h"
-
-#define BACKLOG 8
-
-using namespace std;
-using namespace boost;
-
-static vector<thread *> worker_threads;
-
-static std::list<shared_ptr<asio::ip::tcp::socket> > queue;
-static mutex worker_mutex;
-static condition worker_condition;
-static Log log_ ("servomatic.log");
-
-int
-process (shared_ptr<asio::ip::tcp::socket> socket)
-{
- SocketReader reader (socket);
-
- char buffer[128];
- reader.read_indefinite ((uint8_t *) buffer, sizeof (buffer));
- reader.consume (strlen (buffer) + 1);
-
- stringstream s (buffer);
-
- string command;
- s >> command;
- if (command != "encode") {
- return -1;
- }
-
- Size in_size;
- int pixel_format_int;
- Size out_size;
- int padding;
- string scaler_id;
- int frame;
- float frames_per_second;
- string post_process;
- int colour_lut_index;
- int j2k_bandwidth;
-
- s >> in_size.width >> in_size.height
- >> pixel_format_int
- >> out_size.width >> out_size.height
- >> padding
- >> scaler_id
- >> frame
- >> frames_per_second
- >> post_process
- >> colour_lut_index
- >> j2k_bandwidth;
-
- PixelFormat pixel_format = (PixelFormat) pixel_format_int;
- Scaler const * scaler = Scaler::from_id (scaler_id);
- if (post_process == "none") {
- post_process = "";
- }
-
- shared_ptr<SimpleImage> image (new SimpleImage (pixel_format, in_size));
-
- for (int i = 0; i < image->components(); ++i) {
- int line_size;
- s >> line_size;
- image->set_line_size (i, line_size);
- }
-
- for (int i = 0; i < image->components(); ++i) {
- reader.read_definite_and_consume (image->data()[i], image->line_size()[i] * image->lines(i));
- }
-
-#ifdef DEBUG_HASH
- image->hash ("Image for encoding (as received by server)");
-#endif
-
- DCPVideoFrame dcp_video_frame (image, out_size, padding, scaler, frame, frames_per_second, post_process, colour_lut_index, j2k_bandwidth, &log_);
- shared_ptr<EncodedData> encoded = dcp_video_frame.encode_locally ();
- encoded->send (socket);
-
-#ifdef DEBUG_HASH
- encoded->hash ("Encoded image (as made by server and as sent back)");
-#endif
-
- return frame;
-}
-
-void
-worker_thread ()
-{
- while (1) {
- mutex::scoped_lock lock (worker_mutex);
- while (queue.empty ()) {
- worker_condition.wait (lock);
- }
-
- shared_ptr<asio::ip::tcp::socket> socket = queue.front ();
- queue.pop_front ();
-
- lock.unlock ();
-
- int frame = -1;
-
- struct timeval start;
- gettimeofday (&start, 0);
-
- try {
- frame = process (socket);
- } catch (std::exception& e) {
- cerr << "Error: " << e.what() << "\n";
- }
-
- socket.reset ();
-
- lock.lock ();
-
- if (frame >= 0) {
- struct timeval end;
- gettimeofday (&end, 0);
- cout << "Encoded frame " << frame << " in " << (seconds (end) - seconds (start)) << "\n";
- }
-
- worker_condition.notify_all ();
- }
-}
-
-int
-main ()
-{
- Scaler::setup_scalers ();
-
- int const num_threads = Config::instance()->num_local_encoding_threads ();
-
- for (int i = 0; i < num_threads; ++i) {
- worker_threads.push_back (new thread (worker_thread));
- }
-
- asio::io_service io_service;
- asio::ip::tcp::acceptor acceptor (io_service, asio::ip::tcp::endpoint (asio::ip::tcp::v4(), Config::instance()->server_port ()));
- while (1) {
- shared_ptr<asio::ip::tcp::socket> socket (new asio::ip::tcp::socket (io_service));
- acceptor.accept (*socket);
-
- mutex::scoped_lock lock (worker_mutex);
-
- /* Wait until the queue has gone down a bit */
- while (int (queue.size()) >= num_threads * 2) {
- worker_condition.wait (lock);
- }
-
- queue.push_back (socket);
- worker_condition.notify_all ();
- }
-
- return 0;
-}
diff --git a/src/tools/servomatic_cli.cc b/src/tools/servomatic_cli.cc
new file mode 100644
index 000000000..f8e713193
--- /dev/null
+++ b/src/tools/servomatic_cli.cc
@@ -0,0 +1,94 @@
+/*
+ Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+
+ This program 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.
+
+ This program 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 this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "lib/server.h"
+#include <iostream>
+#include <stdexcept>
+#include <sstream>
+#include <cstring>
+#include <vector>
+#include <unistd.h>
+#include <errno.h>
+#include <getopt.h>
+#include <boost/array.hpp>
+#include <boost/asio.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/thread.hpp>
+#include <boost/thread/mutex.hpp>
+#include <boost/thread/condition.hpp>
+#include "config.h"
+#include "dcp_video_frame.h"
+#include "exceptions.h"
+#include "util.h"
+#include "config.h"
+#include "scaler.h"
+#include "image.h"
+#include "log.h"
+#include "version.h"
+
+using namespace std;
+
+static void
+help (string n)
+{
+ cerr << "Syntax: " << n << " [OPTION]\n"
+ << " -v, --version show DVD-o-matic version\n"
+ << " -h, --help show this help\n"
+ << " -t, --threads number of parallel encoding threads to use\n";
+}
+
+int
+main (int argc, char* argv[])
+{
+ int num_threads = Config::instance()->num_local_encoding_threads ();
+
+ int option_index = 0;
+ while (1) {
+ static struct option long_options[] = {
+ { "version", no_argument, 0, 'v'},
+ { "help", no_argument, 0, 'h'},
+ { "threads", required_argument, 0, 't'},
+ { 0, 0, 0, 0 }
+ };
+
+ int c = getopt_long (argc, argv, "vht:", long_options, &option_index);
+
+ if (c == -1) {
+ break;
+ }
+
+ switch (c) {
+ case 'v':
+ cout << "dvdomatic version " << dvdomatic_version << " " << dvdomatic_git_commit << "\n";
+ exit (EXIT_SUCCESS);
+ case 'h':
+ help (argv[0]);
+ exit (EXIT_SUCCESS);
+ case 't':
+ num_threads = atoi (optarg);
+ break;
+ }
+ }
+
+ Scaler::setup_scalers ();
+ FileLog log ("servomatic.log");
+ Server server (&log);
+ server.run (num_threads);
+ return 0;
+}
diff --git a/src/tools/servomatic_gui.cc b/src/tools/servomatic_gui.cc
new file mode 100644
index 000000000..610ba8005
--- /dev/null
+++ b/src/tools/servomatic_gui.cc
@@ -0,0 +1,151 @@
+/*
+ Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+
+ This program 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.
+
+ This program 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 this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <boost/thread.hpp>
+#include <wx/taskbar.h>
+#include <wx/icon.h>
+#include "wx_util.h"
+#include "lib/util.h"
+#include "lib/server.h"
+#include "lib/config.h"
+
+using namespace std;
+using namespace boost;
+
+enum {
+ ID_status = 1,
+ ID_quit,
+ ID_timer
+};
+
+class MemoryLog : public Log
+{
+public:
+
+ string get () const {
+ boost::mutex::scoped_lock (_mutex);
+ return _log;
+ }
+
+private:
+ void do_log (string m)
+ {
+ _log = m;
+ }
+
+ string _log;
+};
+
+static MemoryLog memory_log;
+
+class StatusDialog : public wxDialog
+{
+public:
+ StatusDialog ()
+ : wxDialog (0, wxID_ANY, _("DVD-o-matic encode server"), wxDefaultPosition, wxSize (600, 80), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
+ , _timer (this, ID_timer)
+ {
+ _sizer = new wxFlexGridSizer (1, 6, 6);
+ _sizer->AddGrowableCol (0, 1);
+
+ _text = new wxTextCtrl (this, wxID_ANY, _(""), wxDefaultPosition, wxDefaultSize, wxTE_READONLY);
+ _sizer->Add (_text, 1, wxEXPAND);
+
+ SetSizer (_sizer);
+ _sizer->Layout ();
+
+ Connect (ID_timer, wxEVT_TIMER, wxTimerEventHandler (StatusDialog::update));
+ _timer.Start (1000);
+ }
+
+private:
+ void update (wxTimerEvent &)
+ {
+ _text->ChangeValue (std_to_wx (memory_log.get ()));
+ _sizer->Layout ();
+ }
+
+ wxFlexGridSizer* _sizer;
+ wxTextCtrl* _text;
+ wxTimer _timer;
+};
+
+class TaskBarIcon : public wxTaskBarIcon
+{
+public:
+ TaskBarIcon ()
+ {
+ wxIcon icon (std_to_wx ("taskbar_icon"));
+ SetIcon (icon, std_to_wx ("DVD-o-matic encode server"));
+
+ Connect (ID_status, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (TaskBarIcon::status));
+ Connect (ID_quit, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (TaskBarIcon::quit));
+ }
+
+ wxMenu* CreatePopupMenu ()
+ {
+ wxMenu* menu = new wxMenu;
+ menu->Append (ID_status, std_to_wx ("Status..."));
+ menu->Append (ID_quit, std_to_wx ("Quit"));
+ return menu;
+ }
+
+private:
+ void status (wxCommandEvent &)
+ {
+ StatusDialog* d = new StatusDialog;
+ d->Show ();
+ }
+
+ void quit (wxCommandEvent &)
+ {
+ wxTheApp->ExitMainLoop ();
+ }
+};
+
+class App : public wxApp
+{
+public:
+ App ()
+ : wxApp ()
+ , _thread (0)
+ {}
+
+private:
+
+ bool OnInit ()
+ {
+ dvdomatic_setup ();
+
+ new TaskBarIcon;
+
+ _thread = new thread (bind (&App::main_thread, this));
+ return true;
+ }
+
+ void main_thread ()
+ {
+ Server server (&memory_log);
+ server.run (Config::instance()->num_local_encoding_threads ());
+ }
+
+ boost::thread* _thread;
+};
+
+IMPLEMENT_APP (App)
diff --git a/src/tools/servomatictest.cc b/src/tools/servomatictest.cc
index 0f37e73a5..d6804c981 100644
--- a/src/tools/servomatictest.cc
+++ b/src/tools/servomatictest.cc
@@ -47,12 +47,8 @@ process_video (shared_ptr<Image> image, int frame)
shared_ptr<DCPVideoFrame> local (new DCPVideoFrame (image, Size (1024, 1024), 0, Scaler::from_id ("bicubic"), frame, 24, "", 0, 250000000, &log_));
shared_ptr<DCPVideoFrame> remote (new DCPVideoFrame (image, Size (1024, 1024), 0, Scaler::from_id ("bicubic"), frame, 24, "", 0, 250000000, &log_));
-#if defined(DEBUG_HASH)
- cout << "Frame " << frame << ":\n";
-#else
cout << "Frame " << frame << ": ";
cout.flush ();
-#endif
shared_ptr<EncodedData> local_encoded = local->encode_locally ();
shared_ptr<EncodedData> remote_encoded;
@@ -64,11 +60,6 @@ process_video (shared_ptr<Image> image, int frame)
remote_error = e.what ();
}
-#if defined(DEBUG_HASH)
- cout << "Frame " << frame << ": ";
- cout.flush ();
-#endif
-
if (!remote_error.empty ()) {
cout << "\033[0;31mnetwork problem: " << remote_error << "\033[0m\n";
return;
diff --git a/src/tools/wscript b/src/tools/wscript
index be3d44e6d..048bdff07 100644
--- a/src/tools/wscript
+++ b/src/tools/wscript
@@ -1,5 +1,5 @@
def build(bld):
- for t in ['makedcp', 'fixlengths', 'servomatic']:
+ for t in ['makedcp', 'fixlengths', 'servomatic_cli']:
obj = bld(features = 'cxx cxxprogram')
obj.uselib = 'BOOST_THREAD'
obj.includes = ['..']
@@ -9,7 +9,7 @@ def build(bld):
if not bld.env.DISABLE_GUI:
# p = ['dvdomatic', 'alignomatic']
- p = ['dvdomatic']
+ p = ['dvdomatic', 'servomatic_gui']
if not bld.env.DISABLE_PLAYER:
p.append('playomatic')
for t in p:
diff --git a/src/wscript b/src/wscript
index 2ebeba210..3f17b3e6c 100644
--- a/src/wscript
+++ b/src/wscript
@@ -1,5 +1,4 @@
def configure(conf):
- conf.recurse('lib')
if not conf.env.DISABLE_GUI:
conf.recurse('wx')
diff --git a/src/wx/config_dialog.cc b/src/wx/config_dialog.cc
index ebf5be460..b0bd6f2ee 100644
--- a/src/wx/config_dialog.cc
+++ b/src/wx/config_dialog.cc
@@ -145,7 +145,7 @@ ConfigDialog::ConfigDialog (wxWindow* parent)
_colour_lut->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (ConfigDialog::colour_lut_changed), 0, this);
_j2k_bandwidth->SetRange (50, 250);
- _j2k_bandwidth->SetValue (config->j2k_bandwidth() / 1e6);
+ _j2k_bandwidth->SetValue (rint ((double) config->j2k_bandwidth() / 1e6));
_j2k_bandwidth->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (ConfigDialog::j2k_bandwidth_changed), 0, this);
_reference_scaler->SetSelection (Scaler::as_index (config->reference_scaler ()));
@@ -155,8 +155,8 @@ ConfigDialog::ConfigDialog (wxWindow* parent)
_reference_filters->SetLabel (std_to_wx (p.first + " " + p.second));
_reference_filters_button->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (ConfigDialog::edit_reference_filters_clicked), 0, this);
- vector<Server*> servers = config->servers ();
- for (vector<Server*>::iterator i = servers.begin(); i != servers.end(); ++i) {
+ vector<ServerDescription*> servers = config->servers ();
+ for (vector<ServerDescription*>::iterator i = servers.begin(); i != servers.end(); ++i) {
add_server_to_control (*i);
}
@@ -170,7 +170,7 @@ ConfigDialog::ConfigDialog (wxWindow* parent)
server_selection_changed (ev);
wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
- overall_sizer->Add (table, 1, wxEXPAND);
+ overall_sizer->Add (table, 1, wxEXPAND | wxALL, 6);
wxSizer* buttons = CreateSeparatedButtonSizer (wxOK);
if (buttons) {
@@ -225,7 +225,7 @@ ConfigDialog::j2k_bandwidth_changed (wxCommandEvent &)
}
void
-ConfigDialog::add_server_to_control (Server* s)
+ConfigDialog::add_server_to_control (ServerDescription* s)
{
wxListItem item;
int const n = _servers->GetItemCount ();
@@ -240,11 +240,11 @@ ConfigDialog::add_server_clicked (wxCommandEvent &)
{
ServerDialog* d = new ServerDialog (this, 0);
d->ShowModal ();
- Server* s = d->server ();
+ ServerDescription* s = d->server ();
d->Destroy ();
add_server_to_control (s);
- vector<Server*> o = Config::instance()->servers ();
+ vector<ServerDescription*> o = Config::instance()->servers ();
o.push_back (s);
Config::instance()->set_servers (o);
}
@@ -262,22 +262,15 @@ ConfigDialog::edit_server_clicked (wxCommandEvent &)
item.SetColumn (0);
_servers->GetItem (item);
- vector<Server*> servers = Config::instance()->servers ();
- vector<Server*>::iterator j = servers.begin();
- while (j != servers.end() && (*j)->host_name() != wx_to_std (item.GetText ())) {
- ++j;
- }
-
- if (j == servers.end()) {
- return;
- }
+ vector<ServerDescription*> servers = Config::instance()->servers ();
+ assert (i >= 0 && i < int (servers.size ()));
- ServerDialog* d = new ServerDialog (this, *j);
+ ServerDialog* d = new ServerDialog (this, servers[i]);
d->ShowModal ();
d->Destroy ();
- _servers->SetItem (i, 0, std_to_wx ((*j)->host_name ()));
- _servers->SetItem (i, 1, std_to_wx (boost::lexical_cast<string> ((*j)->threads ())));
+ _servers->SetItem (i, 0, std_to_wx (servers[i]->host_name ()));
+ _servers->SetItem (i, 1, std_to_wx (boost::lexical_cast<string> (servers[i]->threads ())));
}
void
@@ -287,6 +280,10 @@ ConfigDialog::remove_server_clicked (wxCommandEvent &)
if (i >= 0) {
_servers->DeleteItem (i);
}
+
+ vector<ServerDescription*> o = Config::instance()->servers ();
+ o.erase (o.begin() + i);
+ Config::instance()->set_servers (o);
}
void
diff --git a/src/wx/config_dialog.h b/src/wx/config_dialog.h
index c9ca8034f..b1d3eb84d 100644
--- a/src/wx/config_dialog.h
+++ b/src/wx/config_dialog.h
@@ -26,7 +26,7 @@
#include <wx/listctrl.h>
class Screen;
-class Server;
+class ServerDescription;
/** @class ConfigDialog
* @brief A dialogue to edit DVD-o-matic configuration.
@@ -52,7 +52,7 @@ private:
void remove_server_clicked (wxCommandEvent &);
void server_selection_changed (wxListEvent &);
- void add_server_to_control (Server *);
+ void add_server_to_control (ServerDescription *);
wxTextCtrl* _tms_ip;
wxTextCtrl* _tms_path;
diff --git a/src/wx/dcp_range_dialog.cc b/src/wx/dcp_range_dialog.cc
index 572d0c628..aed6808cb 100644
--- a/src/wx/dcp_range_dialog.cc
+++ b/src/wx/dcp_range_dialog.cc
@@ -69,7 +69,7 @@ DCPRangeDialog::DCPRangeDialog (wxWindow* p, Film* f)
_n_frames->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (DCPRangeDialog::n_frames_changed), 0, this);
wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
- overall_sizer->Add (table);
+ overall_sizer->Add (table, 0, wxALL, 6);
wxSizer* buttons = CreateSeparatedButtonSizer (wxOK);
if (buttons) {
diff --git a/src/wx/film_editor.cc b/src/wx/film_editor.cc
index f35996644..6de3af9e7 100644
--- a/src/wx/film_editor.cc
+++ b/src/wx/film_editor.cc
@@ -132,7 +132,7 @@ FilmEditor::FilmEditor (Film* f, wxWindow* parent)
video_control (add_label_to_sizer (_sizer, this, "Frames Per Second"));
_frames_per_second = new wxStaticText (this, wxID_ANY, wxT (""));
- _sizer->Add (video_control (_frames_per_second));
+ _sizer->Add (video_control (_frames_per_second), 1, wxALIGN_CENTER_VERTICAL);
video_control (add_label_to_sizer (_sizer, this, "Original Size"));
_original_size = new wxStaticText (this, wxID_ANY, wxT (""));
@@ -648,11 +648,24 @@ FilmEditor::audio_gain_calculate_button_clicked (wxCommandEvent &)
{
GainCalculatorDialog* d = new GainCalculatorDialog (this);
d->ShowModal ();
+
+ if (d->wanted_fader() == 0 || d->actual_fader() == 0) {
+ d->Destroy ();
+ return;
+ }
+
_audio_gain->SetValue (
Config::instance()->sound_processor()->db_for_fader_change (
d->wanted_fader (),
d->actual_fader ()
)
);
+
+ /* This appears to be necessary, as the change is not signalled,
+ I think.
+ */
+ wxCommandEvent dummy;
+ audio_gain_changed (dummy);
+
d->Destroy ();
}
diff --git a/src/wx/film_viewer.cc b/src/wx/film_viewer.cc
index 8398b8162..0d17baf83 100644
--- a/src/wx/film_viewer.cc
+++ b/src/wx/film_viewer.cc
@@ -47,6 +47,7 @@ public:
{
}
+ /** Handle a paint event */
void paint_event (wxPaintEvent& ev)
{
if (_current_image != _pending_image) {
@@ -67,6 +68,7 @@ public:
}
}
+ /** Handle a size event */
void size_event (wxSizeEvent &)
{
if (!_image) {
@@ -101,6 +103,7 @@ public:
}
}
+ /** Clear our thumbnail image */
void clear ()
{
delete _bitmap;
@@ -237,6 +240,7 @@ FilmViewer::set_film (Film* f)
}
_film->Changed.connect (sigc::mem_fun (*this, &FilmViewer::film_changed));
+ film_changed (Film::CROP);
film_changed (Film::THUMBS);
_thumb_panel->refresh ();
setup_visibility ();
diff --git a/src/wx/filter_dialog.cc b/src/wx/filter_dialog.cc
index 9ec169395..028d082b4 100644
--- a/src/wx/filter_dialog.cc
+++ b/src/wx/filter_dialog.cc
@@ -32,7 +32,7 @@ FilterDialog::FilterDialog (wxWindow* parent, vector<Filter const *> const & f)
, _filters (new FilterView (this, f))
{
wxBoxSizer* sizer = new wxBoxSizer (wxVERTICAL);
- sizer->Add (_filters, 1, wxEXPAND);
+ sizer->Add (_filters, 1, wxEXPAND | wxALL, 6);
_filters->ActiveChanged.connect (sigc::mem_fun (*this, &FilterDialog::active_changed));
diff --git a/src/wx/gain_calculator_dialog.cc b/src/wx/gain_calculator_dialog.cc
index 431a4672d..3f07faf06 100644
--- a/src/wx/gain_calculator_dialog.cc
+++ b/src/wx/gain_calculator_dialog.cc
@@ -38,7 +38,7 @@ GainCalculatorDialog::GainCalculatorDialog (wxWindow* parent)
table->Add (_actual, 1, wxEXPAND);
wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
- overall_sizer->Add (table, 1, wxEXPAND);
+ overall_sizer->Add (table, 1, wxEXPAND | wxALL, 6);
wxSizer* buttons = CreateSeparatedButtonSizer (wxOK);
if (buttons) {
@@ -52,11 +52,19 @@ GainCalculatorDialog::GainCalculatorDialog (wxWindow* parent)
float
GainCalculatorDialog::wanted_fader () const
{
+ if (_wanted->GetValue().IsEmpty()) {
+ return 0;
+ }
+
return lexical_cast<float> (wx_to_std (_wanted->GetValue ()));
}
float
GainCalculatorDialog::actual_fader () const
{
+ if (_actual->GetValue().IsEmpty()) {
+ return 0;
+ }
+
return lexical_cast<float> (wx_to_std (_actual->GetValue ()));
}
diff --git a/src/wx/job_manager_view.cc b/src/wx/job_manager_view.cc
index c9e120135..1d5c855ea 100644
--- a/src/wx/job_manager_view.cc
+++ b/src/wx/job_manager_view.cc
@@ -67,15 +67,21 @@ JobManagerView::update ()
{
list<shared_ptr<Job> > jobs = JobManager::instance()->get ();
+ int index = 0;
+
for (list<shared_ptr<Job> >::iterator i = jobs.begin(); i != jobs.end(); ++i) {
if (_job_records.find (*i) == _job_records.end ()) {
- add_label_to_sizer (_table, _panel, (*i)->name ());
+ wxStaticText* m = new wxStaticText (_panel, wxID_ANY, std_to_wx ((*i)->name ()));
+ _table->Insert (index, m, 0, wxALIGN_CENTER_VERTICAL | wxALL, 6);
+
JobRecord r;
r.gauge = new wxGauge (_panel, wxID_ANY, 100);
- _table->Add (r.gauge, 1, wxEXPAND | wxLEFT | wxRIGHT);
+ _table->Insert (index + 1, r.gauge, 1, wxEXPAND | wxLEFT | wxRIGHT);
+
r.informed_of_finish = false;
- r.message = add_label_to_sizer (_table, _panel, "", 1);
+ r.message = new wxStaticText (_panel, wxID_ANY, std_to_wx (""));
+ _table->Insert (index + 2, r.message, 1, wxALIGN_CENTER_VERTICAL | wxALL, 6);
_job_records[*i] = r;
}
@@ -112,6 +118,8 @@ JobManagerView::update ()
_job_records[*i].informed_of_finish = true;
}
+
+ index += 3;
}
_table->Layout ();
diff --git a/src/wx/job_wrapper.cc b/src/wx/job_wrapper.cc
index 4c037ae28..ad83aa271 100644
--- a/src/wx/job_wrapper.cc
+++ b/src/wx/job_wrapper.cc
@@ -36,11 +36,7 @@ JobWrapper::make_dcp (wxWindow* parent, Film* film, bool transcode)
film->make_dcp (transcode);
} catch (BadSettingError& e) {
stringstream s;
- if (e.setting() == "dcp_long_name") {
- s << "Could not make DCP: long name is invalid (" << e.what() << ")";
- } else {
- s << "Bad setting for " << e.setting() << "(" << e.what() << ")";
- }
+ s << "Bad setting for " << e.setting() << "(" << e.what() << ")";
error_dialog (parent, s.str ());
} catch (std::exception& e) {
stringstream s;
diff --git a/src/wx/properties_dialog.cc b/src/wx/properties_dialog.cc
new file mode 100644
index 000000000..67f1fc91b
--- /dev/null
+++ b/src/wx/properties_dialog.cc
@@ -0,0 +1,89 @@
+/*
+ Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+
+ This program 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.
+
+ This program 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 this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <iomanip>
+#include <boost/lexical_cast.hpp>
+#include <boost/bind.hpp>
+#include "lib/film.h"
+#include "lib/config.h"
+#include "properties_dialog.h"
+#include "wx_util.h"
+
+using namespace std;
+using namespace boost;
+
+PropertiesDialog::PropertiesDialog (wxWindow* parent, Film* film)
+ : wxDialog (parent, wxID_ANY, _("Film Properties"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
+ , _film (film)
+{
+ wxFlexGridSizer* table = new wxFlexGridSizer (2, 3, 6);
+
+ add_label_to_sizer (table, this, "Frames");
+ _frames = new wxStaticText (this, wxID_ANY, std_to_wx (""));
+ table->Add (_frames, 1, wxALIGN_CENTER_VERTICAL);
+
+ add_label_to_sizer (table, this, "Disk space required for frames");
+ _disk_for_frames = new wxStaticText (this, wxID_ANY, std_to_wx (""));
+ table->Add (_disk_for_frames, 1, wxALIGN_CENTER_VERTICAL);
+
+ add_label_to_sizer (table, this, "Total disk space required");
+ _total_disk = new wxStaticText (this, wxID_ANY, std_to_wx (""));
+ table->Add (_total_disk, 1, wxALIGN_CENTER_VERTICAL);
+
+ add_label_to_sizer (table, this, "Frames already encoded");
+ _encoded = new ThreadedStaticText (this, "counting...", boost::bind (&PropertiesDialog::frames_already_encoded, this));
+ table->Add (_encoded, 1, wxALIGN_CENTER_VERTICAL);
+
+ _frames->SetLabel (std_to_wx (lexical_cast<string> (_film->length ())));
+ double const disk = ((double) Config::instance()->j2k_bandwidth() / 8) * _film->length() / (_film->frames_per_second () * 1073741824);
+ stringstream s;
+ s << fixed << setprecision (1) << disk << "Gb";
+ _disk_for_frames->SetLabel (std_to_wx (s.str ()));
+
+ stringstream t;
+ t << fixed << setprecision (1) << (disk * 2) << "Gb";
+ _total_disk->SetLabel (std_to_wx (t.str ()));
+
+ wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
+ overall_sizer->Add (table, 0, wxALL, 6);
+
+ wxSizer* buttons = CreateSeparatedButtonSizer (wxOK);
+ if (buttons) {
+ overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
+ }
+
+ SetSizer (overall_sizer);
+ overall_sizer->SetSizeHints (this);
+}
+
+string
+PropertiesDialog::frames_already_encoded () const
+{
+ stringstream u;
+ try {
+ u << _film->encoded_frames ();
+ } catch (thread_interrupted &) {
+ return "";
+ }
+
+ if (_film->length()) {
+ u << " (" << (_film->encoded_frames() * 100 / _film->length()) << "%)";
+ }
+ return u.str ();
+}
diff --git a/src/wx/properties_dialog.h b/src/wx/properties_dialog.h
new file mode 100644
index 000000000..f72c83419
--- /dev/null
+++ b/src/wx/properties_dialog.h
@@ -0,0 +1,39 @@
+/*
+ Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+
+ This program 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.
+
+ This program 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 this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <wx/wx.h>
+
+class Film;
+class ThreadedStaticText;
+
+class PropertiesDialog : public wxDialog
+{
+public:
+ PropertiesDialog (wxWindow *, Film *);
+
+private:
+ std::string frames_already_encoded () const;
+
+ Film* _film;
+ wxStaticText* _frames;
+ wxStaticText* _disk_for_frames;
+ wxStaticText* _total_disk;
+ ThreadedStaticText* _encoded;
+};
+
diff --git a/src/wx/server_dialog.cc b/src/wx/server_dialog.cc
index 0ae34b1fc..7b394a484 100644
--- a/src/wx/server_dialog.cc
+++ b/src/wx/server_dialog.cc
@@ -21,13 +21,13 @@
#include "server_dialog.h"
#include "wx_util.h"
-ServerDialog::ServerDialog (wxWindow* parent, Server* server)
+ServerDialog::ServerDialog (wxWindow* parent, ServerDescription* server)
: wxDialog (parent, wxID_ANY, wxString (_("Server")))
{
if (server) {
_server = server;
} else {
- _server = new Server ("localhost", 1);
+ _server = new ServerDescription ("localhost", 1);
}
wxFlexGridSizer* table = new wxFlexGridSizer (2, 4, 4);
@@ -49,7 +49,7 @@ ServerDialog::ServerDialog (wxWindow* parent, Server* server)
_threads->SetValue (_server->threads ());
wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
- overall_sizer->Add (table, 1, wxEXPAND);
+ overall_sizer->Add (table, 1, wxEXPAND | wxALL, 6);
wxSizer* buttons = CreateSeparatedButtonSizer (wxOK);
if (buttons) {
@@ -73,7 +73,7 @@ ServerDialog::threads_changed (wxCommandEvent &)
_server->set_threads (_threads->GetValue ());
}
-Server *
+ServerDescription *
ServerDialog::server () const
{
return _server;
diff --git a/src/wx/server_dialog.h b/src/wx/server_dialog.h
index 05630c377..0912fd60f 100644
--- a/src/wx/server_dialog.h
+++ b/src/wx/server_dialog.h
@@ -20,20 +20,20 @@
#include <wx/wx.h>
#include <wx/spinctrl.h>
-class Server;
+class ServerDescription;
class ServerDialog : public wxDialog
{
public:
- ServerDialog (wxWindow *, Server *);
+ ServerDialog (wxWindow *, ServerDescription *);
- Server* server () const;
+ ServerDescription* server () const;
private:
void host_changed (wxCommandEvent &);
void threads_changed (wxCommandEvent &);
- Server* _server;
+ ServerDescription* _server;
wxTextCtrl* _host;
wxSpinCtrl* _threads;
};
diff --git a/src/wx/wscript b/src/wx/wscript
index 348a9cb0a..38107bb54 100644
--- a/src/wx/wscript
+++ b/src/wx/wscript
@@ -22,6 +22,7 @@ def build(bld):
server_dialog.cc
new_film_dialog.cc
dir_picker_ctrl.cc
+ properties_dialog.cc
"""
# alignment.cc
diff --git a/src/wx/wx_util.cc b/src/wx/wx_util.cc
index 7655fe60d..4277ed12d 100644
--- a/src/wx/wx_util.cc
+++ b/src/wx/wx_util.cc
@@ -18,13 +18,21 @@
*/
/** @file src/wx/wx_util.cc
- * @brief Some utility functions.
+ * @brief Some utility functions and classes.
*/
+#include <boost/thread.hpp>
#include "wx_util.h"
using namespace std;
+using namespace boost;
+/** Add a wxStaticText to a wxSizer, aligning it at vertical centre.
+ * @param s Sizer to add to.
+ * @param p Parent window for the wxStaticText.
+ * @param t Text for the wxStaticText.
+ * @param prop Properties to pass when calling Add() on the wxSizer.
+ */
wxStaticText *
add_label_to_sizer (wxSizer* s, wxWindow* p, string t, int prop)
{
@@ -33,6 +41,10 @@ add_label_to_sizer (wxSizer* s, wxWindow* p, string t, int prop)
return m;
}
+/** Pop up an error dialogue box.
+ * @param parent Parent.
+ * @param m Message.
+ */
void
error_dialog (wxWindow* parent, string m)
{
@@ -41,14 +53,56 @@ error_dialog (wxWindow* parent, string m)
d->Destroy ();
}
+/** @param s wxWidgets string.
+ * @return Corresponding STL string.
+ */
string
wx_to_std (wxString s)
{
return string (s.mb_str ());
}
+/** @param s STL string.
+ * @return Corresponding wxWidgets string.
+ */
wxString
std_to_wx (string s)
{
return wxString (s.c_str(), wxConvUTF8);
}
+
+int const ThreadedStaticText::_update_event_id = 10000;
+
+/** @param parent Parent for the wxStaticText.
+ * @param initial Initial text for the wxStaticText while the computation is being run.
+ * @param fn Function which works out what the wxStaticText content should be and returns it.
+ */
+ThreadedStaticText::ThreadedStaticText (wxWindow* parent, string initial, function<string ()> fn)
+ : wxStaticText (parent, wxID_ANY, std_to_wx (initial))
+{
+ Connect (_update_event_id, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (ThreadedStaticText::thread_finished), 0, this);
+ _thread = new thread (bind (&ThreadedStaticText::run, this, fn));
+}
+
+ThreadedStaticText::~ThreadedStaticText ()
+{
+ _thread->interrupt ();
+ _thread->join ();
+ delete _thread;
+}
+
+/** Run our thread and post the result to the GUI thread via AddPendingEvent */
+void
+ThreadedStaticText::run (function<string ()> fn)
+{
+ wxCommandEvent ev (wxEVT_COMMAND_TEXT_UPDATED, _update_event_id);
+ ev.SetString (std_to_wx (fn ()));
+ GetEventHandler()->AddPendingEvent (ev);
+}
+
+/** Called in the GUI thread when our worker thread has finished */
+void
+ThreadedStaticText::thread_finished (wxCommandEvent& ev)
+{
+ SetLabel (ev.GetString ());
+}
diff --git a/src/wx/wx_util.h b/src/wx/wx_util.h
index d0b838c36..12a6e8837 100644
--- a/src/wx/wx_util.h
+++ b/src/wx/wx_util.h
@@ -18,12 +18,35 @@
*/
#include <wx/wx.h>
+#include <boost/function.hpp>
+#include <boost/thread.hpp>
/** @file src/wx/wx_util.h
- * @brief Some utility functions.
+ * @brief Some utility functions and classes.
*/
extern void error_dialog (wxWindow *, std::string);
extern wxStaticText* add_label_to_sizer (wxSizer *, wxWindow *, std::string, int prop = 0);
extern std::string wx_to_std (wxString);
extern wxString std_to_wx (std::string);
+
+/** @class ThreadedStaticText
+ *
+ * @brief A wxStaticText whose content is computed in a separate thread, to avoid holding
+ * up the GUI while work is done.
+ */
+class ThreadedStaticText : public wxStaticText
+{
+public:
+ ThreadedStaticText (wxWindow* parent, std::string initial, boost::function<std::string ()> fn);
+ ~ThreadedStaticText ();
+
+private:
+ void run (boost::function<std::string ()> fn);
+ void thread_finished (wxCommandEvent& ev);
+
+ /** Thread to do our work in */
+ boost::thread* _thread;
+
+ static const int _update_event_id;
+};