Merge master.
authorCarl Hetherington <cth@carlh.net>
Tue, 11 Feb 2014 12:04:27 +0000 (12:04 +0000)
committerCarl Hetherington <cth@carlh.net>
Tue, 11 Feb 2014 12:04:27 +0000 (12:04 +0000)
16 files changed:
1  2 
src/lib/ffmpeg.cc
src/lib/ffmpeg_content.h
src/lib/image.cc
src/lib/job.cc
src/lib/player.cc
src/lib/transcode_job.cc
src/lib/util.cc
src/lib/util.h
src/lib/wscript
src/tools/server_test.cc
src/wx/audio_plot.cc
src/wx/film_viewer.cc
src/wx/timeline.cc
test/resampler_test.cc
test/wscript
wscript

diff --combined src/lib/ffmpeg.cc
index 4bf9415234e3d54b844e8881e56a75dd30558492,fae9baa2b9da70cef4086e424fd34261472de473..5fc33348923c85d584af16fb980c6f5f50ad2dd1
@@@ -26,6 -26,7 +26,7 @@@ extern "C" 
  #include "ffmpeg.h"
  #include "ffmpeg_content.h"
  #include "exceptions.h"
+ #include "util.h"
  
  #include "i18n.h"
  
@@@ -85,7 -86,7 +86,7 @@@ FFmpeg::setup_general (
        av_register_all ();
  
        _file_group.set_paths (_ffmpeg_content->paths ());
-       _avio_buffer = static_cast<uint8_t*> (av_malloc (_avio_buffer_size));
+       _avio_buffer = static_cast<uint8_t*> (wrapped_av_malloc (_avio_buffer_size));
        _avio_context = avio_alloc_context (_avio_buffer, _avio_buffer_size, 0, this, avio_read_wrapper, 0, avio_seek_wrapper);
        _format_context = avformat_alloc_context ();
        _format_context->pb = _avio_context;
@@@ -146,7 -147,8 +147,8 @@@ voi
  FFmpeg::setup_video ()
  {
        boost::mutex::scoped_lock lm (_mutex);
-       
+       assert (_video_stream >= 0);
        AVCodecContext* context = _format_context->streams[_video_stream]->codec;
        AVCodec* codec = avcodec_find_decoder (context->codec_id);
  
@@@ -191,10 -193,6 +193,10 @@@ FFmpeg::video_codec_context () cons
  AVCodecContext *
  FFmpeg::audio_codec_context () const
  {
 +      if (!_ffmpeg_content->audio_stream ()) {
 +              return 0;
 +      }
 +      
        return _ffmpeg_content->audio_stream()->stream(_format_context)->codec;
  }
  
diff --combined src/lib/ffmpeg_content.h
index d9037b0d8ca2f70ee38ee3edc56e9c703c4dd874,b1f2abceaa37d46a382a2e2607dcf45e930b6291..e637faf47643be48b0f3353bad5d440cb5231b62
@@@ -86,6 -86,8 +86,8 @@@ private
        /* Constructor for tests */
        FFmpegAudioStream ()
                : FFmpegStream ("", 0)
+               , frame_rate (0)
+               , channels (0)
                , mapping (1)
        {}
  };
@@@ -134,13 -136,13 +136,13 @@@ public
        std::string technical_summary () const;
        std::string information () const;
        void as_xml (xmlpp::Node *) const;
 -      Time full_length () const;
 +      DCPTime full_length () const;
  
        std::string identifier () const;
        
        /* AudioContent */
        int audio_channels () const;
 -      AudioContent::Frame audio_length () const;
 +      AudioFrame audio_length () const;
        int content_audio_frame_rate () const;
        int output_audio_frame_rate () const;
        AudioMapping audio_mapping () const;
diff --combined src/lib/image.cc
index e5f626c24762bbb2e57be8e1bd06a1cada3b9efc,4722563c45d532f397bb5231adf3ffdc548c4835..78e8bbb2a9cb6fbb48410cc873b9c72c0a60ad3e
@@@ -31,7 -31,6 +31,7 @@@ extern "C" 
  #include "image.h"
  #include "exceptions.h"
  #include "scaler.h"
 +#include "timer.h"
  
  using std::string;
  using std::min;
@@@ -95,9 -94,9 +95,9 @@@ Image::crop_scale_window (Crop crop, li
        libdcp::Size cropped_size = crop.apply (size ());
  
        struct SwsContext* scale_context = sws_getContext (
 -              cropped_size.width, cropped_size.height, pixel_format(),
 -              inter_size.width, inter_size.height, out_format,
 -              scaler->ffmpeg_id (), 0, 0, 0
 +                      cropped_size.width, cropped_size.height, pixel_format(),
 +                      inter_size.width, inter_size.height, out_format,
 +                      scaler->ffmpeg_id (), 0, 0, 0
                );
  
        uint8_t* scale_in_data[components()];
@@@ -376,18 -375,8 +376,18 @@@ Image::make_black (
  void
  Image::alpha_blend (shared_ptr<const Image> other, Position<int> position)
  {
 -      /* Only implemented for RGBA onto RGB24 so far */
 -      assert (_pixel_format == PIX_FMT_RGB24 && other->pixel_format() == PIX_FMT_RGBA);
 +      int this_bpp = 0;
 +      int other_bpp = 0;
 +
 +      if (_pixel_format == PIX_FMT_BGRA && other->pixel_format() == PIX_FMT_RGBA) {
 +              this_bpp = 4;
 +              other_bpp = 4;
 +      } else if (_pixel_format == PIX_FMT_RGB24 && other->pixel_format() == PIX_FMT_RGBA) {
 +              this_bpp = 3;
 +              other_bpp = 4;
 +      } else {
 +              assert (false);
 +      }
  
        int start_tx = position.x;
        int start_ox = 0;
        }
  
        for (int ty = start_ty, oy = start_oy; ty < size().height && oy < other->size().height; ++ty, ++oy) {
 -              uint8_t* tp = data()[0] + ty * stride()[0] + position.x * 3;
 +              uint8_t* tp = data()[0] + ty * stride()[0] + position.x * this_bpp;
                uint8_t* op = other->data()[0] + oy * other->stride()[0];
                for (int tx = start_tx, ox = start_ox; tx < size().width && ox < other->size().width; ++tx, ++ox) {
                        float const alpha = float (op[3]) / 255;
                        tp[0] = (tp[0] * (1 - alpha)) + op[0] * alpha;
                        tp[1] = (tp[1] * (1 - alpha)) + op[1] * alpha;
                        tp[2] = (tp[2] * (1 - alpha)) + op[2] * alpha;
 -                      tp += 3;
 -                      op += 4;
 +                      tp += this_bpp;
 +                      op += other_bpp;
                }
        }
  }
@@@ -509,13 -498,13 +509,13 @@@ Image::Image (AVPixelFormat p, libdcp::
  void
  Image::allocate ()
  {
-       _data = (uint8_t **) av_malloc (4 * sizeof (uint8_t *));
+       _data = (uint8_t **) wrapped_av_malloc (4 * sizeof (uint8_t *));
        _data[0] = _data[1] = _data[2] = _data[3] = 0;
        
-       _line_size = (int *) av_malloc (4 * sizeof (int));
+       _line_size = (int *) wrapped_av_malloc (4 * sizeof (int));
        _line_size[0] = _line_size[1] = _line_size[2] = _line_size[3] = 0;
        
-       _stride = (int *) av_malloc (4 * sizeof (int));
+       _stride = (int *) wrapped_av_malloc (4 * sizeof (int));
        _stride[0] = _stride[1] = _stride[2] = _stride[3] = 0;
  
        for (int i = 0; i < components(); ++i) {
                   seem to mind.  The nasty + 1 in this malloc makes sure there is always a byte
                   for that instruction to read safely.
                */
-               _data[i] = (uint8_t *) av_malloc (_stride[i] * lines (i) + 1);
+               _data[i] = (uint8_t *) wrapped_av_malloc (_stride[i] * lines (i) + 1);
        }
  }
  
diff --combined src/lib/job.cc
index 05a90524c9818615112b77464da5a56bd154ec69,66fa3755dfb69d3c751a15cde8c868d083927339..b543a043f0550cc176af9874834633dc678a9128
@@@ -68,9 -68,6 +68,6 @@@ Job::run_wrapper (
  
        } catch (libdcp::FileError& e) {
                
-               set_progress (1);
-               set_state (FINISHED_ERROR);
-               
                string m = String::compose (_("An error occurred whilst handling the file %1."), boost::filesystem::path (e.filename()).leaf());
  
                try {
                }
  
                set_error (e.what(), m);
-       } catch (OpenFileError& e) {
                set_progress (1);
                set_state (FINISHED_ERROR);
+               
+       } catch (OpenFileError& e) {
  
                set_error (
                        String::compose (_("Could not open %1"), e.file().string()),
                        String::compose (_("DCP-o-matic could not open the file %1.  Perhaps it does not exist or is in an unexpected format."), e.file().string())
                        );
  
+               set_progress (1);
+               set_state (FINISHED_ERROR);
        } catch (boost::thread_interrupted &) {
  
                set_state (FINISHED_CANCELLED);
-               
-       } catch (std::exception& e) {
  
+       } catch (std::bad_alloc& e) {
+               set_error (_("Out of memory"), _("There was not enough memory to do this."));
                set_progress (1);
                set_state (FINISHED_ERROR);
+               
+       } catch (std::exception& e) {
                set_error (
                        e.what (),
                        _("It is not known what caused this error.  The best idea is to report the problem to the DCP-o-matic mailing list (carl@dcpomatic.com)")
                        );
  
-       } catch (...) {
                set_progress (1);
                set_state (FINISHED_ERROR);
+               
+       } catch (...) {
                set_error (
                        _("Unknown error"),
                        _("It is not known what caused this error.  The best idea is to report the problem to the DCP-o-matic mailing list (carl@dcpomatic.com)")
                        );
  
+               set_progress (1);
+               set_state (FINISHED_ERROR);
        }
  }
  
@@@ -198,7 -204,7 +204,7 @@@ Job::set_state (State s
        }
  }
  
 -/** @return Time (in seconds) that this sub-job has been running */
 +/** @return DCPTime (in seconds) that this sub-job has been running */
  int
  Job::elapsed_time () const
  {
diff --combined src/lib/player.cc
index 3e6a1598d17e76559c64b7ca0cde2837ec905b64,59db923be79503d4425122f24c320690e3d961ed..48c75078e12a59c2bdfabab7f5a91bbf1ea085f0
@@@ -18,7 -18,6 +18,7 @@@
  */
  
  #include <stdint.h>
 +#include <algorithm>
  #include "player.h"
  #include "film.h"
  #include "ffmpeg_decoder.h"
  #include "sndfile_decoder.h"
  #include "sndfile_content.h"
  #include "subtitle_content.h"
 +#include "subrip_decoder.h"
 +#include "subrip_content.h"
  #include "playlist.h"
  #include "job.h"
  #include "image.h"
  #include "ratio.h"
 -#include "resampler.h"
  #include "log.h"
  #include "scaler.h"
 +#include "render_subtitles.h"
  
  using std::list;
  using std::cout;
@@@ -48,20 -45,71 +48,20 @@@ using std::map
  using boost::shared_ptr;
  using boost::weak_ptr;
  using boost::dynamic_pointer_cast;
 +using boost::optional;
  
  class Piece
  {
  public:
 -      Piece (shared_ptr<Content> c)
 -              : content (c)
 -              , video_position (c->position ())
 -              , audio_position (c->position ())
 -              , repeat_to_do (0)
 -              , repeat_done (0)
 -      {}
 -      
 -      Piece (shared_ptr<Content> c, shared_ptr<Decoder> d)
 +      Piece (shared_ptr<Content> c, shared_ptr<Decoder> d, FrameRateChange f)
                : content (c)
                , decoder (d)
 -              , video_position (c->position ())
 -              , audio_position (c->position ())
 -              , repeat_to_do (0)
 -              , repeat_done (0)
 +              , frc (f)
        {}
  
 -      /** Set this piece to repeat a video frame a given number of times */
 -      void set_repeat (IncomingVideo video, int num)
 -      {
 -              repeat_video = video;
 -              repeat_to_do = num;
 -              repeat_done = 0;
 -      }
 -
 -      void reset_repeat ()
 -      {
 -              repeat_video.image.reset ();
 -              repeat_to_do = 0;
 -              repeat_done = 0;
 -      }
 -
 -      bool repeating () const
 -      {
 -              return repeat_done != repeat_to_do;
 -      }
 -
 -      void repeat (Player* player)
 -      {
 -              player->process_video (
 -                      repeat_video.weak_piece,
 -                      repeat_video.image,
 -                      repeat_video.eyes,
 -                      repeat_done > 0,
 -                      repeat_video.frame,
 -                      (repeat_done + 1) * (TIME_HZ / player->_film->video_frame_rate ())
 -                      );
 -
 -              ++repeat_done;
 -      }
 -      
        shared_ptr<Content> content;
        shared_ptr<Decoder> decoder;
 -      /** Time of the last video we emitted relative to the start of the DCP */
 -      Time video_position;
 -      /** Time of the last audio we emitted relative to the start of the DCP */
 -      Time audio_position;
 -
 -      IncomingVideo repeat_video;
 -      int repeat_to_do;
 -      int repeat_done;
 +      FrameRateChange frc;
  };
  
  Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p)
        , _audio_position (0)
        , _audio_merger (f->audio_channels(), bind (&Film::time_to_audio_frames, f.get(), _1), bind (&Film::audio_frames_to_time, f.get(), _1))
        , _last_emit_was_black (false)
 +      , _just_did_inaccurate_seek (false)
 +      , _approximate_size (false)
  {
        _playlist_changed_connection = _playlist->Changed.connect (bind (&Player::playlist_changed, this));
        _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Player::content_changed, this, _1, _2, _3));
@@@ -102,162 -148,111 +102,162 @@@ Player::pass (
                setup_pieces ();
        }
  
 -      Time earliest_t = TIME_MAX;
 -      shared_ptr<Piece> earliest;
 -      enum {
 -              VIDEO,
 -              AUDIO
 -      } type = VIDEO;
 +      /* Interrogate all our pieces to find the one with the earliest decoded data */
 +
 +      shared_ptr<Piece> earliest_piece;
 +      shared_ptr<Decoded> earliest_decoded;
 +      DCPTime earliest_time = TIME_MAX;
 +      DCPTime earliest_audio = TIME_MAX;
  
        for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -              if ((*i)->decoder->done ()) {
 -                      continue;
 -              }
  
 -              shared_ptr<VideoDecoder> vd = dynamic_pointer_cast<VideoDecoder> ((*i)->decoder);
 -              shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 +              DCPTime const offset = (*i)->content->position() - (*i)->content->trim_start();
 +              
 +              bool done = false;
 +              shared_ptr<Decoded> dec;
 +              while (!done) {
 +                      dec = (*i)->decoder->peek ();
 +                      if (!dec) {
 +                              /* Decoder has nothing else to give us */
 +                              break;
 +                      }
  
 -              if (_video && vd) {
 -                      if ((*i)->video_position < earliest_t) {
 -                              earliest_t = (*i)->video_position;
 -                              earliest = *i;
 -                              type = VIDEO;
 +                      dec->set_dcp_times (_film->video_frame_rate(), _film->audio_frame_rate(), (*i)->frc, offset);
 +                      DCPTime const t = dec->dcp_time - offset;
 +                      if (t >= ((*i)->content->full_length() - (*i)->content->trim_end ())) {
 +                              /* In the end-trimmed part; decoder has nothing else to give us */
 +                              dec.reset ();
 +                              done = true;
 +                      } else if (t >= (*i)->content->trim_start ()) {
 +                              /* Within the un-trimmed part; everything's ok */
 +                              done = true;
 +                      } else {
 +                              /* Within the start-trimmed part; get something else */
 +                              (*i)->decoder->consume ();
                        }
                }
  
 -              if (_audio && ad && ad->has_audio ()) {
 -                      if ((*i)->audio_position < earliest_t) {
 -                              earliest_t = (*i)->audio_position;
 -                              earliest = *i;
 -                              type = AUDIO;
 -                      }
 +              if (!dec) {
 +                      continue;
                }
 -      }
  
 -      if (!earliest) {
 +              if (dec->dcp_time < earliest_time) {
 +                      earliest_piece = *i;
 +                      earliest_decoded = dec;
 +                      earliest_time = dec->dcp_time;
 +              }
 +
 +              if (dynamic_pointer_cast<DecodedAudio> (dec) && dec->dcp_time < earliest_audio) {
 +                      earliest_audio = dec->dcp_time;
 +              }
 +      }
 +              
 +      if (!earliest_piece) {
                flush ();
                return true;
        }
  
 -      switch (type) {
 -      case VIDEO:
 -              if (earliest_t > _video_position) {
 -                      emit_black ();
 -              } else {
 -                      if (earliest->repeating ()) {
 -                              earliest->repeat (this);
 +      if (earliest_audio != TIME_MAX) {
 +              TimedAudioBuffers<DCPTime> tb = _audio_merger.pull (max (int64_t (0), earliest_audio));
 +              Audio (tb.audio, tb.time);
 +              /* This assumes that the audio_frames_to_time conversion is exact
 +                 so that there are no accumulated errors caused by rounding.
 +              */
 +              _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
 +      }
 +
 +      /* Emit the earliest thing */
 +
 +      shared_ptr<DecodedVideo> dv = dynamic_pointer_cast<DecodedVideo> (earliest_decoded);
 +      shared_ptr<DecodedAudio> da = dynamic_pointer_cast<DecodedAudio> (earliest_decoded);
 +      shared_ptr<DecodedImageSubtitle> dis = dynamic_pointer_cast<DecodedImageSubtitle> (earliest_decoded);
 +      shared_ptr<DecodedTextSubtitle> dts = dynamic_pointer_cast<DecodedTextSubtitle> (earliest_decoded);
 +
 +      /* Will be set to false if we shouldn't consume the peeked DecodedThing */
 +      bool consume = true;
 +
 +      if (dv && _video) {
 +
 +              if (_just_did_inaccurate_seek) {
 +
 +                      /* Just emit; no subtlety */
 +                      emit_video (earliest_piece, dv);
 +                      step_video_position (dv);
 +                      
 +              } else if (dv->dcp_time > _video_position) {
 +
 +                      /* Too far ahead */
 +
 +                      list<shared_ptr<Piece> >::iterator i = _pieces.begin();
 +                      while (i != _pieces.end() && ((*i)->content->position() >= _video_position || _video_position >= (*i)->content->end())) {
 +                              ++i;
 +                      }
 +
 +                      if (i == _pieces.end() || !_last_incoming_video.video || !_have_valid_pieces) {
 +                              /* We're outside all video content */
 +                              emit_black ();
 +                              _statistics.video.black++;
                        } else {
 -                              earliest->decoder->pass ();
 +                              /* We're inside some video; repeat the frame */
 +                              _last_incoming_video.video->dcp_time = _video_position;
 +                              emit_video (_last_incoming_video.weak_piece, _last_incoming_video.video);
 +                              step_video_position (_last_incoming_video.video);
 +                              _statistics.video.repeat++;
                        }
 -              }
 -              break;
  
 -      case AUDIO:
 -              if (earliest_t > _audio_position) {
 -                      emit_silence (_film->time_to_audio_frames (earliest_t - _audio_position));
 +                      consume = false;
 +
 +              } else if (dv->dcp_time == _video_position) {
 +                      /* We're ok */
 +                      emit_video (earliest_piece, dv);
 +                      step_video_position (dv);
 +                      _statistics.video.good++;
                } else {
 -                      earliest->decoder->pass ();
 -
 -                      if (earliest->decoder->done()) {
 -                              shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (earliest->content);
 -                              assert (ac);
 -                              shared_ptr<Resampler> re = resampler (ac, false);
 -                              if (re) {
 -                                      shared_ptr<const AudioBuffers> b = re->flush ();
 -                                      if (b->frames ()) {
 -                                              process_audio (earliest, b, ac->audio_length ());
 -                                      }
 -                              }
 -                      }
 +                      /* Too far behind: skip */
 +                      _statistics.video.skip++;
                }
 -              break;
 -      }
  
 -      if (_audio) {
 -              boost::optional<Time> audio_done_up_to;
 -              for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -                      if ((*i)->decoder->done ()) {
 -                              continue;
 -                      }
 +              _just_did_inaccurate_seek = false;
  
 -                      shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 -                      if (ad && ad->has_audio ()) {
 -                              audio_done_up_to = min (audio_done_up_to.get_value_or (TIME_MAX), (*i)->audio_position);
 -                      }
 -              }
 +      } else if (da && _audio) {
  
 -              if (audio_done_up_to) {
 -                      TimedAudioBuffers<Time> tb = _audio_merger.pull (audio_done_up_to.get ());
 -                      Audio (tb.audio, tb.time);
 -                      _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
 +              if (da->dcp_time > _audio_position) {
 +                      /* Too far ahead */
 +                      emit_silence (da->dcp_time - _audio_position);
 +                      consume = false;
 +                      _statistics.audio.silence += (da->dcp_time - _audio_position);
 +              } else if (da->dcp_time == _audio_position) {
 +                      /* We're ok */
 +                      emit_audio (earliest_piece, da);
 +                      _statistics.audio.good += da->data->frames();
 +              } else {
 +                      /* Too far behind: skip */
 +                      _statistics.audio.skip += da->data->frames();
                }
 -      }
                
 +      } else if (dis && _video) {
 +              _image_subtitle.piece = earliest_piece;
 +              _image_subtitle.subtitle = dis;
 +              update_subtitle_from_image ();
 +      } else if (dts && _video) {
 +              _text_subtitle.piece = earliest_piece;
 +              _text_subtitle.subtitle = dts;
 +              update_subtitle_from_text ();
 +      }
 +
 +      if (consume) {
 +              earliest_piece->decoder->consume ();
 +      }                       
 +      
        return false;
  }
  
 -/** @param extra Amount of extra time to add to the content frame's time (for repeat) */
  void
 -Player::process_video (weak_ptr<Piece> weak_piece, shared_ptr<const Image> image, Eyes eyes, bool same, VideoContent::Frame frame, Time extra)
 +Player::emit_video (weak_ptr<Piece> weak_piece, shared_ptr<DecodedVideo> video)
  {
        /* Keep a note of what came in so that we can repeat it if required */
        _last_incoming_video.weak_piece = weak_piece;
 -      _last_incoming_video.image = image;
 -      _last_incoming_video.eyes = eyes;
 -      _last_incoming_video.same = same;
 -      _last_incoming_video.frame = frame;
 -      _last_incoming_video.extra = extra;
 +      _last_incoming_video.video = video;
        
        shared_ptr<Piece> piece = weak_piece.lock ();
        if (!piece) {
        shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
        assert (content);
  
 -      FrameRateConversion frc (content->video_frame_rate(), _film->video_frame_rate());
 -      if (frc.skip && (frame % 2) == 1) {
 -              return;
 -      }
 +      FrameRateChange frc (content->video_frame_rate(), _film->video_frame_rate());
  
 -      Time const relative_time = (frame * frc.factor() * TIME_HZ / _film->video_frame_rate());
 -      if (content->trimmed (relative_time)) {
 -              return;
 -      }
 -
 -      Time const time = content->position() + relative_time + extra - content->trim_start ();
        float const ratio = content->ratio() ? content->ratio()->ratio() : content->video_size_after_crop().ratio();
 -      libdcp::Size const image_size = fit_ratio_within (ratio, _video_container_size);
 +      libdcp::Size image_size = fit_ratio_within (ratio, _video_container_size);
 +      if (_approximate_size) {
 +              image_size.width &= ~3;
 +              image_size.height &= ~3;
 +      }
  
        shared_ptr<PlayerImage> pi (
                new PlayerImage (
 -                      image,
 +                      video->image,
                        content->crop(),
                        image_size,
                        _video_container_size,
                        )
                );
        
 -      if (_film->with_subtitles () && _out_subtitle.image && time >= _out_subtitle.from && time <= _out_subtitle.to) {
 +      if (
 +              _film->with_subtitles () &&
 +              _out_subtitle.image &&
 +              video->dcp_time >= _out_subtitle.from && video->dcp_time <= _out_subtitle.to
 +              ) {
  
                Position<int> const container_offset (
                        (_video_container_size.width - image_size.width) / 2,
 -                      (_video_container_size.height - image_size.width) / 2
 +                      (_video_container_size.height - image_size.height) / 2
                        );
  
                pi->set_subtitle (_out_subtitle.image, _out_subtitle.position + container_offset);
        _last_video = piece->content;
  #endif
  
 -      Video (pi, eyes, content->colour_conversion(), same, time);
 -
 +      Video (pi, video->eyes, content->colour_conversion(), video->same, video->dcp_time);
 +      
        _last_emit_was_black = false;
 -      _video_position = piece->video_position = (time + TIME_HZ / _film->video_frame_rate());
 +}
  
 -      if (frc.repeat > 1 && !piece->repeating ()) {
 -              piece->set_repeat (_last_incoming_video, frc.repeat - 1);
 +void
 +Player::step_video_position (shared_ptr<DecodedVideo> video)
 +{
 +      /* This is a bit of a hack; don't update _video_position if EYES_RIGHT is on its way */
 +      if (video->eyes != EYES_LEFT) {
 +              /* This assumes that the video_frames_to_time conversion is exact
 +                 so that there are no accumulated errors caused by rounding.
 +              */
 +              _video_position += _film->video_frames_to_time (1);
        }
  }
  
  void
 -Player::process_audio (weak_ptr<Piece> weak_piece, shared_ptr<const AudioBuffers> audio, AudioContent::Frame frame)
 +Player::emit_audio (weak_ptr<Piece> weak_piece, shared_ptr<DecodedAudio> audio)
  {
        shared_ptr<Piece> piece = weak_piece.lock ();
        if (!piece) {
  
        /* Gain */
        if (content->audio_gain() != 0) {
 -              shared_ptr<AudioBuffers> gain (new AudioBuffers (audio));
 +              shared_ptr<AudioBuffers> gain (new AudioBuffers (audio->data));
                gain->apply_gain (content->audio_gain ());
 -              audio = gain;
 -      }
 -
 -      /* Resample */
 -      if (content->content_audio_frame_rate() != content->output_audio_frame_rate()) {
 -              shared_ptr<Resampler> r = resampler (content, true);
 -              pair<shared_ptr<const AudioBuffers>, AudioContent::Frame> ro = r->run (audio, frame);
 -              audio = ro.first;
 -              frame = ro.second;
 -      }
 -      
 -      Time const relative_time = _film->audio_frames_to_time (frame);
 -
 -      if (content->trimmed (relative_time)) {
 -              return;
 +              audio->data = gain;
        }
  
 -      Time time = content->position() + (content->audio_delay() * TIME_HZ / 1000) + relative_time - content->trim_start ();
 -      
        /* Remap channels */
 -      shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->frames()));
 +      shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->data->frames()));
        dcp_mapped->make_silent ();
 -
        AudioMapping map = content->audio_mapping ();
        for (int i = 0; i < map.content_channels(); ++i) {
                for (int j = 0; j < _film->audio_channels(); ++j) {
                        if (map.get (i, static_cast<libdcp::Channel> (j)) > 0) {
                                dcp_mapped->accumulate_channel (
 -                                      audio.get(),
 +                                      audio->data.get(),
                                        i,
                                        static_cast<libdcp::Channel> (j),
                                        map.get (i, static_cast<libdcp::Channel> (j))
                }
        }
  
 -      audio = dcp_mapped;
 +      audio->data = dcp_mapped;
  
 -      /* We must cut off anything that comes before the start of all time */
 -      if (time < 0) {
 -              int const frames = - time * _film->audio_frame_rate() / TIME_HZ;
 -              if (frames >= audio->frames ()) {
 +      /* Delay */
 +      audio->dcp_time += content->audio_delay() * TIME_HZ / 1000;
 +      if (audio->dcp_time < 0) {
 +              int const frames = - audio->dcp_time * _film->audio_frame_rate() / TIME_HZ;
 +              if (frames >= audio->data->frames ()) {
                        return;
                }
  
 -              shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->channels(), audio->frames() - frames));
 -              trimmed->copy_from (audio.get(), audio->frames() - frames, frames, 0);
 +              shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->data->channels(), audio->data->frames() - frames));
 +              trimmed->copy_from (audio->data.get(), audio->data->frames() - frames, frames, 0);
  
 -              audio = trimmed;
 -              time = 0;
 +              audio->data = trimmed;
 +              audio->dcp_time = 0;
        }
  
 -      _audio_merger.push (audio, time);
 -      piece->audio_position += _film->audio_frames_to_time (audio->frames ());
 +      _audio_merger.push (audio->data, audio->dcp_time);
  }
  
  void
  Player::flush ()
  {
 -      TimedAudioBuffers<Time> tb = _audio_merger.flush ();
 +      TimedAudioBuffers<DCPTime> tb = _audio_merger.flush ();
        if (_audio && tb.audio) {
                Audio (tb.audio, tb.time);
                _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
        }
  
        while (_audio && _audio_position < _video_position) {
 -              emit_silence (_film->time_to_audio_frames (_video_position - _audio_position));
 +              emit_silence (_video_position - _audio_position);
        }
        
  }
   *  @return true on error
   */
  void
 -Player::seek (Time t, bool accurate)
 +Player::seek (DCPTime t, bool accurate)
  {
        if (!_have_valid_pieces) {
                setup_pieces ();
        }
  
        for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -              shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> ((*i)->content);
 -              if (!vc) {
 -                      continue;
 -              }
 -
                /* s is the offset of t from the start position of this content */
 -              Time s = t - vc->position ();
 -              s = max (static_cast<Time> (0), s);
 -              s = min (vc->length_after_trim(), s);
 +              DCPTime s = t - (*i)->content->position ();
 +              s = max (static_cast<DCPTime> (0), s);
 +              s = min ((*i)->content->length_after_trim(), s);
  
 -              /* Hence set the piece positions to the `global' time */
 -              (*i)->video_position = (*i)->audio_position = vc->position() + s;
 +              /* Convert this to the content time */
 +              ContentTime ct = (s + (*i)->content->trim_start()) * (*i)->frc.speed_up;
  
                /* And seek the decoder */
 -              dynamic_pointer_cast<VideoDecoder>((*i)->decoder)->seek (
 -                      vc->time_to_content_video_frames (s + vc->trim_start ()), accurate
 -                      );
 -
 -              (*i)->reset_repeat ();
 +              (*i)->decoder->seek (ct, accurate);
        }
  
 -      _video_position = _audio_position = t;
 +      _video_position = time_round_up (t, TIME_HZ / _film->video_frame_rate());
 +      _audio_position = time_round_up (t, TIME_HZ / _film->audio_frame_rate());
 +
 +      _audio_merger.clear (_audio_position);
  
 -      /* XXX: don't seek audio because we don't need to... */
 +      if (!accurate) {
 +              /* We just did an inaccurate seek, so it's likely that the next thing seen
 +                 out of pass() will be a fair distance from _{video,audio}_position.  Setting
 +                 this flag stops pass() from trying to fix that: we assume that if it
 +                 was an inaccurate seek then the caller does not care too much about
 +                 inserting black/silence to keep the time tidy.
 +              */
 +              _just_did_inaccurate_seek = true;
 +      }
  }
  
  void
  Player::setup_pieces ()
  {
        list<shared_ptr<Piece> > old_pieces = _pieces;
 -
        _pieces.clear ();
  
        ContentList content = _playlist->content ();
 -      sort (content.begin(), content.end(), ContentSorter ());
  
        for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
  
+               if (!(*i)->paths_valid ()) {
+                       continue;
+               }
++              
 +              shared_ptr<Decoder> decoder;
 +              optional<FrameRateChange> frc;
 +
 +              /* Work out a FrameRateChange for the best overlap video for this content, in case we need it below */
 +              DCPTime best_overlap_t = 0;
 +              shared_ptr<VideoContent> best_overlap;
 +              for (ContentList::iterator j = content.begin(); j != content.end(); ++j) {
 +                      shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*j);
 +                      if (!vc) {
 +                              continue;
 +                      }
 +                      
 +                      DCPTime const overlap = max (vc->position(), (*i)->position()) - min (vc->end(), (*i)->end());
 +                      if (overlap > best_overlap_t) {
 +                              best_overlap = vc;
 +                              best_overlap_t = overlap;
 +                      }
 +              }
  
 -              shared_ptr<Piece> piece (new Piece (*i));
 -
 -              /* XXX: into content? */
 +              optional<FrameRateChange> best_overlap_frc;
 +              if (best_overlap) {
 +                      best_overlap_frc = FrameRateChange (best_overlap->video_frame_rate(), _film->video_frame_rate ());
 +              } else {
 +                      /* No video overlap; e.g. if the DCP is just audio */
 +                      best_overlap_frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ());
 +              }
  
 +              /* FFmpeg */
                shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i);
                if (fc) {
 -                      shared_ptr<FFmpegDecoder> fd (new FFmpegDecoder (_film, fc, _video, _audio));
 -                      
 -                      fd->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, 0));
 -                      fd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2));
 -                      fd->Subtitle.connect (bind (&Player::process_subtitle, this, weak_ptr<Piece> (piece), _1, _2, _3, _4));
 -
 -                      fd->seek (fc->time_to_content_video_frames (fc->trim_start ()), true);
 -                      piece->decoder = fd;
 +                      decoder.reset (new FFmpegDecoder (_film, fc, _video, _audio));
 +                      frc = FrameRateChange (fc->video_frame_rate(), _film->video_frame_rate());
                }
 -              
 +
 +              /* ImageContent */
                shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i);
                if (ic) {
 -                      bool reusing = false;
 -                      
                        /* See if we can re-use an old ImageDecoder */
                        for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) {
                                shared_ptr<ImageDecoder> imd = dynamic_pointer_cast<ImageDecoder> ((*j)->decoder);
                                if (imd && imd->content() == ic) {
 -                                      piece = *j;
 -                                      reusing = true;
 +                                      decoder = imd;
                                }
                        }
  
 -                      if (!reusing) {
 -                              shared_ptr<ImageDecoder> id (new ImageDecoder (_film, ic));
 -                              id->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, 0));
 -                              piece->decoder = id;
 +                      if (!decoder) {
 +                              decoder.reset (new ImageDecoder (_film, ic));
                        }
 +
 +                      frc = FrameRateChange (ic->video_frame_rate(), _film->video_frame_rate());
                }
  
 +              /* SndfileContent */
                shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i);
                if (sc) {
 -                      shared_ptr<AudioDecoder> sd (new SndfileDecoder (_film, sc));
 -                      sd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2));
 +                      decoder.reset (new SndfileDecoder (_film, sc));
 +                      frc = best_overlap_frc;
 +              }
  
 -                      piece->decoder = sd;
 +              /* SubRipContent */
 +              shared_ptr<const SubRipContent> rc = dynamic_pointer_cast<const SubRipContent> (*i);
 +              if (rc) {
 +                      decoder.reset (new SubRipDecoder (_film, rc));
 +                      frc = best_overlap_frc;
                }
  
 -              _pieces.push_back (piece);
 +              ContentTime st = (*i)->trim_start() * frc->speed_up;
 +              decoder->seek (st, true);
 +              
 +              _pieces.push_back (shared_ptr<Piece> (new Piece (*i, decoder, frc.get ())));
        }
  
        _have_valid_pieces = true;
 +
 +      /* The Piece for the _last_incoming_video will no longer be valid */
 +      _last_incoming_video.video.reset ();
 +
 +      _video_position = _audio_position = 0;
  }
  
  void
@@@ -553,8 -533,7 +557,8 @@@ Player::content_changed (weak_ptr<Conte
                property == SubtitleContentProperty::SUBTITLE_SCALE
                ) {
  
 -              update_subtitle ();
 +              update_subtitle_from_image ();
 +              update_subtitle_from_text ();
                Changed (frequent);
  
        } else if (
  
        } else if (property == ContentProperty::PATH) {
  
+               _have_valid_pieces = false;
                Changed (frequent);
        }
  }
@@@ -596,6 -576,29 +601,6 @@@ Player::set_video_container_size (libdc
                );
  }
  
 -shared_ptr<Resampler>
 -Player::resampler (shared_ptr<AudioContent> c, bool create)
 -{
 -      map<shared_ptr<AudioContent>, shared_ptr<Resampler> >::iterator i = _resamplers.find (c);
 -      if (i != _resamplers.end ()) {
 -              return i->second;
 -      }
 -
 -      if (!create) {
 -              return shared_ptr<Resampler> ();
 -      }
 -
 -      _film->log()->log (
 -              String::compose (
 -                      "Creating new resampler for %1 to %2 with %3 channels", c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()
 -                      )
 -              );
 -      
 -      shared_ptr<Resampler> r (new Resampler (c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()));
 -      _resamplers[c] = r;
 -      return r;
 -}
 -
  void
  Player::emit_black ()
  {
  }
  
  void
 -Player::emit_silence (OutputAudioFrame most)
 +Player::emit_silence (DCPTime most)
  {
        if (most == 0) {
                return;
        }
        
 -      OutputAudioFrame N = min (most, _film->audio_frame_rate() / 2);
 -      shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), N));
 +      DCPTime t = min (most, TIME_HZ / 2);
 +      shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), t * _film->audio_frame_rate() / TIME_HZ));
        silence->make_silent ();
        Audio (silence, _audio_position);
 -      _audio_position += _film->audio_frames_to_time (N);
 +      
 +      _audio_position += t;
  }
  
  void
@@@ -637,14 -639,26 +642,14 @@@ Player::film_changed (Film::Property p
  }
  
  void
 -Player::process_subtitle (weak_ptr<Piece> weak_piece, shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to)
 -{
 -      _in_subtitle.piece = weak_piece;
 -      _in_subtitle.image = image;
 -      _in_subtitle.rect = rect;
 -      _in_subtitle.from = from;
 -      _in_subtitle.to = to;
 -
 -      update_subtitle ();
 -}
 -
 -void
 -Player::update_subtitle ()
 +Player::update_subtitle_from_image ()
  {
 -      shared_ptr<Piece> piece = _in_subtitle.piece.lock ();
 +      shared_ptr<Piece> piece = _image_subtitle.piece.lock ();
        if (!piece) {
                return;
        }
  
 -      if (!_in_subtitle.image) {
 +      if (!_image_subtitle.subtitle->image) {
                _out_subtitle.image.reset ();
                return;
        }
        shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (piece->content);
        assert (sc);
  
 -      dcpomatic::Rect<double> in_rect = _in_subtitle.rect;
 +      dcpomatic::Rect<double> in_rect = _image_subtitle.subtitle->rect;
        libdcp::Size scaled_size;
  
        in_rect.x += sc->subtitle_x_offset ();
        _out_subtitle.position.x = rint (_video_container_size.width * (in_rect.x + (in_rect.width * (1 - sc->subtitle_scale ()) / 2)));
        _out_subtitle.position.y = rint (_video_container_size.height * (in_rect.y + (in_rect.height * (1 - sc->subtitle_scale ()) / 2)));
        
 -      _out_subtitle.image = _in_subtitle.image->scale (
 +      _out_subtitle.image = _image_subtitle.subtitle->image->scale (
                scaled_size,
                Scaler::from_id ("bicubic"),
 -              _in_subtitle.image->pixel_format (),
 +              _image_subtitle.subtitle->image->pixel_format (),
                true
                );
 -
 -      /* XXX: hack */
 -      Time from = _in_subtitle.from;
 -      Time to = _in_subtitle.to;
 -      shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (piece->content);
 -      if (vc) {
 -              from = rint (from * vc->video_frame_rate() / _film->video_frame_rate());
 -              to = rint (to * vc->video_frame_rate() / _film->video_frame_rate());
 -      }
        
 -      _out_subtitle.from = from + piece->content->position ();
 -      _out_subtitle.to = to + piece->content->position ();
 +      _out_subtitle.from = _image_subtitle.subtitle->dcp_time + piece->content->position ();
 +      _out_subtitle.to = _image_subtitle.subtitle->dcp_time_to + piece->content->position ();
  }
  
  /** Re-emit the last frame that was emitted, using current settings for crop, ratio, scaler and subtitles.
  bool
  Player::repeat_last_video ()
  {
 -      if (!_last_incoming_video.image || !_have_valid_pieces) {
 +      if (!_last_incoming_video.video || !_have_valid_pieces) {
                return false;
        }
  
 -      process_video (
 +      emit_video (
                _last_incoming_video.weak_piece,
 -              _last_incoming_video.image,
 -              _last_incoming_video.eyes,
 -              _last_incoming_video.same,
 -              _last_incoming_video.frame,
 -              _last_incoming_video.extra
 +              _last_incoming_video.video
                );
  
        return true;
  }
  
 +void
 +Player::update_subtitle_from_text ()
 +{
 +      if (_text_subtitle.subtitle->subs.empty ()) {
 +              _out_subtitle.image.reset ();
 +              return;
 +      }
 +
 +      render_subtitles (_text_subtitle.subtitle->subs, _video_container_size, _out_subtitle.image, _out_subtitle.position);
 +}
 +
 +void
 +Player::set_approximate_size ()
 +{
 +      _approximate_size = true;
 +}
 +                            
  PlayerImage::PlayerImage (
        shared_ptr<const Image> in,
        Crop crop,
@@@ -747,10 -757,10 +752,10 @@@ PlayerImage::set_subtitle (shared_ptr<c
  }
  
  shared_ptr<Image>
 -PlayerImage::image ()
 +PlayerImage::image (AVPixelFormat format, bool aligned)
  {
 -      shared_ptr<Image> out = _in->crop_scale_window (_crop, _inter_size, _out_size, _scaler, PIX_FMT_RGB24, false);
 -
 +      shared_ptr<Image> out = _in->crop_scale_window (_crop, _inter_size, _out_size, _scaler, format, aligned);
 +      
        Position<int> const container_offset ((_out_size.width - _inter_size.width) / 2, (_out_size.height - _inter_size.width) / 2);
  
        if (_subtitle_image) {
  
        return out;
  }
 +
 +void
 +PlayerStatistics::dump (shared_ptr<Log> log) const
 +{
 +      log->log (String::compose ("Video: %1 good %2 skipped %3 black %4 repeat", video.good, video.skip, video.black, video.repeat));
 +      log->log (String::compose ("Audio: %1 good %2 skipped %3 silence", audio.good, audio.skip, audio.silence));
 +}
 +
 +PlayerStatistics const &
 +Player::statistics () const
 +{
 +      return _statistics;
 +}
diff --combined src/lib/transcode_job.cc
index 289259369d028652aa2fc684f7aabc4997c30971,5c195ee1b781227637068f58ff62bf5425ca1612..46fc97fb31b74bdf14f6093dd0d973e34fa78709
@@@ -100,17 -100,20 +100,20 @@@ TranscodeJob::status () cons
  int
  TranscodeJob::remaining_time () const
  {
-       if (!_transcoder) {
+       /* _transcoder might be destroyed by the job-runner thread */
+       shared_ptr<Transcoder> t = _transcoder;
+       
+       if (!t) {
                return 0;
        }
        
-       float fps = _transcoder->current_encoding_rate ();
+       float fps = t->current_encoding_rate ();
  
        if (fps == 0) {
                return 0;
        }
  
        /* Compute approximate proposed length here, as it's only here that we need it */
-       VideoFrame const left = _film->time_to_video_frames (_film->length ()) - _transcoder->video_frames_out();
 -      OutputVideoFrame const left = _film->time_to_video_frames (_film->length ()) - t->video_frames_out();
++      VideoFrame const left = _film->time_to_video_frames (_film->length ()) - t->video_frames_out();
        return left / fps;
  }
diff --combined src/lib/util.cc
index ef203c2bdc82fae52119f0acef4b4d4c2ba27764,b2f8c4470e5af5ee5117489870f4899bd38367bb..418d7b3e0f6123810b2f482df524473d5abe2d55
@@@ -27,6 -27,7 +27,7 @@@
  #include <iostream>
  #include <fstream>
  #include <climits>
+ #include <stdexcept>
  #ifdef DCPOMATIC_POSIX
  #include <execinfo.h>
  #include <cxxabi.h>
@@@ -47,7 -48,6 +48,7 @@@
  #include <openssl/md5.h>
  #include <magick/MagickCore.h>
  #include <magick/version.h>
 +#include <pangomm/init.h>
  #include <libdcp/version.h>
  #include <libdcp/util.h>
  #include <libdcp/signer_chain.h>
@@@ -93,7 -93,9 +94,9 @@@ using std::istream
  using std::numeric_limits;
  using std::pair;
  using std::cout;
+ using std::bad_alloc;
  using std::streampos;
+ using std::set_terminate;
  using boost::shared_ptr;
  using boost::thread;
  using boost::lexical_cast;
@@@ -272,6 -274,33 +275,33 @@@ LONG WINAPI exception_handler(struct _E
  }
  #endif
  
+ /* From http://stackoverflow.com/questions/2443135/how-do-i-find-where-an-exception-was-thrown-in-c */
+ void
+ terminate ()
+ {
+       static bool tried_throw = false;
+       try {
+               // try once to re-throw currently active exception
+               if (!tried_throw++) {
+                       throw;
+               }
+       }
+       catch (const std::exception &e) {
+               std::cerr << __FUNCTION__ << " caught unhandled exception. what(): "
+                         << e.what() << std::endl;
+       }
+       catch (...) {
+               std::cerr << __FUNCTION__ << " caught unknown/unhandled exception." 
+                         << std::endl;
+       }
+ #ifdef DCPOMATIC_POSIX
+       stacktrace (cout, 50);
+ #endif
+       abort();
+ }
  /** Call the required functions to set up DCP-o-matic's static arrays, etc.
   *  Must be called from the UI thread, if there is one.
   */
@@@ -308,9 -337,10 +338,11 @@@ dcpomatic_setup (
        boost::filesystem::path lib = app_contents ();
        lib /= "lib";
        setenv ("LTDL_LIBRARY_PATH", lib.c_str (), 1);
- #endif        
+ #endif
+       set_terminate (terminate);
  
 +      Pango::init ();
        libdcp::init ();
        
        Ratio::setup_ratios ();
@@@ -751,7 -781,7 +783,7 @@@ ensure_ui_thread (
   *  @return Equivalent number of audio frames for `v'.
   */
  int64_t
 -video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second)
 +video_frames_to_audio_frames (VideoFrame v, float audio_sample_rate, float frames_per_second)
  {
        return ((int64_t) v * audio_sample_rate / frames_per_second);
  }
@@@ -776,7 -806,7 +808,7 @@@ audio_channel_name (int c
        return channels[c];
  }
  
 -FrameRateConversion::FrameRateConversion (float source, int dcp)
 +FrameRateChange::FrameRateChange (float source, int dcp)
        : skip (false)
        , repeat (1)
        , change_speed (false)
                repeat = round (dcp / source);
        }
  
 -      change_speed = !about_equal (source * factor(), dcp);
 +      speed_up = dcp / (source * factor());
 +      change_speed = !about_equal (speed_up, 1.0);
  
        if (!skip && repeat == 1 && !change_speed) {
                description = _("Content and DCP have the same rate.\n");
@@@ -920,9 -949,13 +952,19 @@@ fit_ratio_within (float ratio, libdcp::
        return libdcp::Size (full_frame.width, rint (full_frame.width / ratio));
  }
  
 -              
 +DCPTime
 +time_round_up (DCPTime t, DCPTime nearest)
 +{
 +      DCPTime const a = t + nearest - 1;
 +      return a - (a % nearest);
 +}
++
+ void *
+ wrapped_av_malloc (size_t s)
+ {
+       void* p = av_malloc (s);
+       if (!p) {
+               throw bad_alloc ();
+       }
+       return p;
+ }
diff --combined src/lib/util.h
index a84e7e4cf815522ae65ffa6cf52bebbf46849210,a229bbfc9df05ff0aab8a74ef00df6ac55183311..d3e6a67de9e1fd053783c65afd8effaef22ef001
@@@ -79,9 -79,9 +79,9 @@@ extern std::string tidy_for_filename (s
  extern boost::shared_ptr<const libdcp::Signer> make_signer ();
  extern libdcp::Size fit_ratio_within (float ratio, libdcp::Size);
  
 -struct FrameRateConversion
 +struct FrameRateChange
  {
 -      FrameRateConversion (float, int);
 +      FrameRateChange (float, int);
  
        /** @return factor by which to multiply a source frame rate
            to get the effective rate after any skip or repeat has happened.
         */
        bool change_speed;
  
 +      /** Amount by which the video is being sped-up in the DCP; e.g. for a
 +       *  24fps source in a 25fps DCP this would be 25/24.
 +       */
 +      float speed_up;
 +
        std::string description;
  };
  
  extern int dcp_audio_frame_rate (int);
  extern int stride_round_up (int, int const *, int);
 +extern DCPTime time_round_up (DCPTime, DCPTime);
  extern std::multimap<std::string, std::string> read_key_value (std::istream& s);
  extern int get_required_int (std::multimap<std::string, std::string> const & kv, std::string k);
  extern float get_required_float (std::multimap<std::string, std::string> const & kv, std::string k);
  extern std::string get_required_string (std::multimap<std::string, std::string> const & kv, std::string k);
  extern int get_optional_int (std::multimap<std::string, std::string> const & kv, std::string k);
  extern std::string get_optional_string (std::multimap<std::string, std::string> const & kv, std::string k);
+ extern void* wrapped_av_malloc (size_t);
  
  /** @class Socket
   *  @brief A class to wrap a boost::asio::ip::tcp::socket with some things
@@@ -166,7 -161,7 +167,7 @@@ private
        int _timeout;
  };
  
 -extern int64_t video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second);
 +extern int64_t video_frames_to_audio_frames (VideoFrame v, float audio_sample_rate, float frames_per_second);
  
  class LocaleGuard
  {
diff --combined src/lib/wscript
index 8a20618c0641c91e30b82bdce72047926478ba3c,4921afaee788e3b8d0db44d605587feaa270bb9b..a5b069184024ba981c09f8a2e4e65f26749af194
@@@ -41,7 -41,6 +41,7 @@@ sources = ""
            player.cc
            playlist.cc
            ratio.cc
 +          render_subtitles.cc
            resampler.cc
            scp_dcp_job.cc
            scaler.cc
@@@ -51,9 -50,6 +51,9 @@@
            sndfile_content.cc
            sndfile_decoder.cc
            sound_processor.cc
 +          subrip.cc
 +          subrip_content.cc
 +          subrip_decoder.cc
            subtitle_content.cc
            subtitle_decoder.cc
            timer.cc
@@@ -69,7 -65,7 +69,7 @@@
            """
  
  def build(bld):
-     if bld.env.STATIC:
+     if bld.env.BUILD_STATIC:
          obj = bld(features = 'cxx cxxstlib')
      else:
          obj = bld(features = 'cxx cxxshlib')
@@@ -80,7 -76,7 +80,7 @@@
                   AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE SWRESAMPLE 
                   BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2
                   SNDFILE OPENJPEG POSTPROC TIFF MAGICK SSH DCP CXML GLIB LZMA XML++
 -                 CURL ZIP QUICKMAIL
 +                 CURL ZIP QUICKMAIL PANGOMM CAIROMM
                   """
  
      obj.source = sources + ' version.cc'
@@@ -88,7 -84,7 +88,7 @@@
      if bld.env.TARGET_WINDOWS:
          obj.uselib += ' WINSOCK2 BFD DBGHELP IBERTY SHLWAPI MSWSOCK BOOST_LOCALE'
          obj.source += ' stack.cpp'
-     if bld.env.STATIC:
+     if bld.env.BUILD_STATIC:
          obj.uselib += ' XML++'
  
      obj.target = 'dcpomatic'
diff --combined src/tools/server_test.cc
index 38e4704b751e8882437b8d605e5fbb1415151f21,039088862ef8bf7801af1d76da482ff85c425373..9b725cb86df7f15fdb34d1449dbbe6a803ae056c
@@@ -47,15 -47,10 +47,15 @@@ static shared_ptr<FileLog> log_ (new Fi
  static int frame = 0;
  
  void
 -process_video (shared_ptr<PlayerImage> image, Eyes eyes, ColourConversion conversion, Time)
 +process_video (shared_ptr<PlayerImage> image, Eyes eyes, ColourConversion conversion, DCPTime)
  {
 -      shared_ptr<DCPVideoFrame> local  (new DCPVideoFrame (image->image(), frame, eyes, conversion, film->video_frame_rate(), 250000000, RESOLUTION_2K, log_));
 -      shared_ptr<DCPVideoFrame> remote (new DCPVideoFrame (image->image(), frame, eyes, conversion, film->video_frame_rate(), 250000000, RESOLUTION_2K, log_));
 +      shared_ptr<DCPVideoFrame> local  (
 +              new DCPVideoFrame (image->image (PIX_FMT_RGB24, false), frame, eyes, conversion, film->video_frame_rate(), 250000000, RESOLUTION_2K, log_)
 +              );
 +      
 +      shared_ptr<DCPVideoFrame> remote (
 +              new DCPVideoFrame (image->image (PIX_FMT_RGB24, false), frame, eyes, conversion, film->video_frame_rate(), 250000000, RESOLUTION_2K, log_)
 +              );
  
        cout << "Frame " << frame << ": ";
        cout.flush ();
@@@ -142,14 -137,14 +142,14 @@@ main (int argc, char* argv[]
  
        dcpomatic_setup ();
  
-       server = new ServerDescription (server_host, 1);
-       film.reset (new Film (film_dir));
-       film->read_metadata ();
-       shared_ptr<Player> player = film->make_player ();
-       player->disable_audio ();
        try {
+               server = new ServerDescription (server_host, 1);
+               film.reset (new Film (film_dir));
+               film->read_metadata ();
+               
+               shared_ptr<Player> player = film->make_player ();
+               player->disable_audio ();
                player->Video.connect (boost::bind (process_video, _1, _2, _3, _5));
                bool done = false;
                while (!done) {
diff --combined src/wx/audio_plot.cc
index 96de34d406744960db60c4aa072a0673699c9c2d,fd261925518b3c058605bf4f04951895e073ec2f..0868d931a8be1d38dc7b95e0c154afce4e544a51
@@@ -145,7 -145,7 +145,7 @@@ AudioPlot::paint (
        gc->SetPen (*wxLIGHT_GREY_PEN);
        gc->StrokePath (grid);
  
 -      gc->DrawText (_("Time"), data_width, _height - _y_origin + db_label_height / 2);
 +      gc->DrawText (_("DCPTime"), data_width, _height - _y_origin + db_label_height / 2);
        
        if (_type_visible[AudioPoint::PEAK]) {
                for (int c = 0; c < MAX_AUDIO_CHANNELS; ++c) {
@@@ -259,7 -259,9 +259,9 @@@ AudioPlot::plot_rms (wxGraphicsPath& pa
                        p += pow (*j, 2);
                }
  
-               p = sqrt (p / smoothing.size ());
+               if (smoothing.size() > 0) {
+                       p = sqrt (p / smoothing.size ());
+               }
  
                path.AddLineToPoint (_db_label_width + i * _x_scale, y_for_linear (p));
        }
diff --combined src/wx/film_viewer.cc
index a4a293918df44cedec1b4c5ddc0a4f3ab5587d07,e24583d6cfa478ea62a6aefa7740d2fa1c022db2..deee65a5fca4dc07f7d401f0234386d9937257cf
@@@ -36,7 -36,6 +36,7 @@@
  #include "lib/player.h"
  #include "lib/video_content.h"
  #include "lib/video_decoder.h"
 +#include "lib/timer.h"
  #include "film_viewer.h"
  #include "wx_util.h"
  
@@@ -46,6 -45,7 +46,7 @@@ using std::min
  using std::max;
  using std::cout;
  using std::list;
+ using std::bad_alloc;
  using std::make_pair;
  using boost::shared_ptr;
  using boost::dynamic_pointer_cast;
@@@ -127,9 -127,15 +128,16 @@@ FilmViewer::set_film (shared_ptr<Film> 
                return;
        }
  
-       _player = f->make_player ();
+       try {
+               _player = f->make_player ();
+       } catch (bad_alloc) {
+               error_dialog (this, _("There is not enough free memory to do that."));
+               _film.reset ();
+               return;
+       }
+       
        _player->disable_audio ();
 +      _player->set_approximate_size ();
        _player->Video.connect (boost::bind (&FilmViewer::process_video, this, _1, _2, _5));
        _player->Changed.connect (boost::bind (&FilmViewer::player_changed, this, _1));
  
@@@ -166,7 -172,7 +174,7 @@@ FilmViewer::timer (
        
        fetch_next_frame ();
  
 -      Time const len = _film->length ();
 +      DCPTime const len = _film->length ();
  
        if (len) {
                int const new_slider_position = 4096 * _player->video_position() / len;
@@@ -213,14 -219,8 +221,14 @@@ voi
  FilmViewer::slider_moved ()
  {
        if (_film && _player) {
 -              _player->seek (_slider->GetValue() * _film->length() / 4096, false);
 -              fetch_next_frame ();
 +              try {
 +                      _player->seek (_slider->GetValue() * _film->length() / 4096, false);
 +                      fetch_next_frame ();
 +              } catch (OpenFileError& e) {
 +                      /* There was a problem opening a content file; we'll let this slide as it
 +                         probably means a missing content file, which we're already taking care of.
 +                      */
 +              }
        }
  }
  
@@@ -259,13 -259,6 +267,13 @@@ FilmViewer::calculate_sizes (
        _out_size.width = max (64, _out_size.width);
        _out_size.height = max (64, _out_size.height);
  
 +      /* The player will round its image down to the nearest 4 pixels
 +         to speed up its scale, so do similar here to avoid black borders
 +         around things.  This is a bit of a hack.
 +      */
 +      _out_size.width &= ~3;
 +      _out_size.height &= ~3;
 +
        _player->set_video_container_size (_out_size);
  }
  
@@@ -290,24 -283,20 +298,24 @@@ FilmViewer::check_play_state (
  }
  
  void
 -FilmViewer::process_video (shared_ptr<PlayerImage> image, Eyes eyes, Time t)
 +FilmViewer::process_video (shared_ptr<PlayerImage> image, Eyes eyes, DCPTime t)
  {
        if (eyes == EYES_RIGHT) {
                return;
        }
 -      
 -      _frame = image->image ();
 +
 +      /* Going via BGRA here makes the scaler faster then using RGB24 directly (about
 +         twice on x86 Linux).
 +      */
 +      shared_ptr<Image> im = image->image (PIX_FMT_BGRA, true);
 +      _frame = im->scale (im->size(), Scaler::from_id ("fastbilinear"), PIX_FMT_RGB24, false);
        _got_frame = true;
  
        set_position_text (t);
  }
  
  void
 -FilmViewer::set_position_text (Time t)
 +FilmViewer::set_position_text (DCPTime t)
  {
        if (!_film) {
                _frame_number->SetLabel ("0");
@@@ -390,19 -379,13 +398,19 @@@ FilmViewer::back_clicked (
           We want to see the one before it, so we need to go back 2.
        */
  
 -      Time p = _player->video_position() - _film->video_frames_to_time (2);
 +      DCPTime p = _player->video_position() - _film->video_frames_to_time (2);
        if (p < 0) {
                p = 0;
        }
        
 -      _player->seek (p, true);
 -      fetch_next_frame ();
 +      try {
 +              _player->seek (p, true);
 +              fetch_next_frame ();
 +      } catch (OpenFileError& e) {
 +              /* There was a problem opening a content file; we'll let this slide as it
 +                 probably means a missing content file, which we're already taking care of.
 +              */
 +      }
  }
  
  void
diff --combined src/wx/timeline.cc
index 2119e781322f2cedae4e3083ffd20b7ead4ffda2,4e306c4998a526081e4629f79c03ba5d4fcaa63d..ac26c77a9125d7ab31c2a34620831d980b5d6bc4
@@@ -39,7 -39,7 +39,7 @@@ using boost::optional
  class View : public boost::noncopyable
  {
  public:
 -      View (Timeline& t)
 +      View (DCPTimeline& t)
                : _timeline (t)
        {
  
  protected:
        virtual void do_paint (wxGraphicsContext *) = 0;
        
 -      int time_x (Time t) const
 +      int time_x (DCPTime t) const
        {
                return _timeline.tracks_position().x + t * _timeline.pixels_per_time_unit();
        }
        
 -      Timeline& _timeline;
 +      DCPTimeline& _timeline;
  
  private:
        dcpomatic::Rect<int> _last_paint_bbox;
@@@ -80,7 -80,7 +80,7 @@@
  class ContentView : public View
  {
  public:
 -      ContentView (Timeline& tl, shared_ptr<Content> c)
 +      ContentView (DCPTimeline& tl, shared_ptr<Content> c)
                : View (tl)
                , _content (c)
                , _track (0)
@@@ -139,8 -139,8 +139,8 @@@ private
                        return;
                }
  
 -              Time const position = cont->position ();
 -              Time const len = cont->length_after_trim ();
 +              DCPTime const position = cont->position ();
 +              DCPTime const len = cont->length_after_trim ();
  
                wxColour selected (colour().Red() / 2, colour().Green() / 2, colour().Blue() / 2);
  
  class AudioContentView : public ContentView
  {
  public:
 -      AudioContentView (Timeline& tl, shared_ptr<Content> c)
 +      AudioContentView (DCPTimeline& tl, shared_ptr<Content> c)
                : ContentView (tl, c)
        {}
        
@@@ -222,7 -222,7 +222,7 @@@ private
  class VideoContentView : public ContentView
  {
  public:
 -      VideoContentView (Timeline& tl, shared_ptr<Content> c)
 +      VideoContentView (DCPTimeline& tl, shared_ptr<Content> c)
                : ContentView (tl, c)
        {}
  
@@@ -243,10 -243,10 +243,10 @@@ private
        }
  };
  
 -class TimeAxisView : public View
 +class DCPTimeAxisView : public View
  {
  public:
 -      TimeAxisView (Timeline& tl, int y)
 +      DCPTimeAxisView (DCPTimeline& tl, int y)
                : View (tl)
                , _y (y)
        {}
@@@ -291,7 -291,7 +291,7 @@@ private
                path.AddLineToPoint (_timeline.width(), _y);
                gc->StrokePath (path);
  
 -              Time t = 0;
 +              DCPTime t = 0;
                while ((t * _timeline.pixels_per_time_unit()) < _timeline.width()) {
                        wxGraphicsPath path = gc->CreatePath ();
                        path.MoveToPoint (time_x (t), _y - 4);
@@@ -326,11 -326,11 +326,11 @@@ private
  };
  
  
 -Timeline::Timeline (wxWindow* parent, FilmEditor* ed, shared_ptr<Film> film)
 +DCPTimeline::DCPTimeline (wxWindow* parent, FilmEditor* ed, shared_ptr<Film> film)
        : wxPanel (parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)
        , _film_editor (ed)
        , _film (film)
 -      , _time_axis_view (new TimeAxisView (*this, 32))
 +      , _time_axis_view (new DCPTimeAxisView (*this, 32))
        , _tracks (0)
        , _pixels_per_time_unit (0)
        , _left_down (false)
        SetDoubleBuffered (true);
  #endif        
  
 -      Bind (wxEVT_PAINT,      boost::bind (&Timeline::paint,       this));
 -      Bind (wxEVT_LEFT_DOWN,  boost::bind (&Timeline::left_down,   this, _1));
 -      Bind (wxEVT_LEFT_UP,    boost::bind (&Timeline::left_up,     this, _1));
 -      Bind (wxEVT_RIGHT_DOWN, boost::bind (&Timeline::right_down,  this, _1));
 -      Bind (wxEVT_MOTION,     boost::bind (&Timeline::mouse_moved, this, _1));
 -      Bind (wxEVT_SIZE,       boost::bind (&Timeline::resized,     this));
 +      Bind (wxEVT_PAINT,      boost::bind (&DCPTimeline::paint,       this));
 +      Bind (wxEVT_LEFT_DOWN,  boost::bind (&DCPTimeline::left_down,   this, _1));
 +      Bind (wxEVT_LEFT_UP,    boost::bind (&DCPTimeline::left_up,     this, _1));
 +      Bind (wxEVT_RIGHT_DOWN, boost::bind (&DCPTimeline::right_down,  this, _1));
 +      Bind (wxEVT_MOTION,     boost::bind (&DCPTimeline::mouse_moved, this, _1));
 +      Bind (wxEVT_SIZE,       boost::bind (&DCPTimeline::resized,     this));
  
        playlist_changed ();
  
        SetMinSize (wxSize (640, tracks() * track_height() + 96));
  
 -      _playlist_connection = film->playlist()->Changed.connect (bind (&Timeline::playlist_changed, this));
 +      _playlist_connection = film->playlist()->Changed.connect (bind (&DCPTimeline::playlist_changed, this));
  }
  
  void
 -Timeline::paint ()
 +DCPTimeline::paint ()
  {
        wxPaintDC dc (this);
  
  }
  
  void
 -Timeline::playlist_changed ()
 +DCPTimeline::playlist_changed ()
  {
        ensure_ui_thread ();
        
  }
  
  void
 -Timeline::assign_tracks ()
 +DCPTimeline::assign_tracks ()
  {
        for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
                shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
  }
  
  int
 -Timeline::tracks () const
 +DCPTimeline::tracks () const
  {
        return _tracks;
  }
  
  void
 -Timeline::setup_pixels_per_time_unit ()
 +DCPTimeline::setup_pixels_per_time_unit ()
  {
        shared_ptr<const Film> film = _film.lock ();
-       if (!film) {
+       if (!film || film->length() == 0) {
                return;
        }
  
  }
  
  shared_ptr<View>
 -Timeline::event_to_view (wxMouseEvent& ev)
 +DCPTimeline::event_to_view (wxMouseEvent& ev)
  {
        ViewList::iterator i = _views.begin();
        Position<int> const p (ev.GetX(), ev.GetY());
  }
  
  void
 -Timeline::left_down (wxMouseEvent& ev)
 +DCPTimeline::left_down (wxMouseEvent& ev)
  {
        shared_ptr<View> view = event_to_view (ev);
        shared_ptr<ContentView> content_view = dynamic_pointer_cast<ContentView> (view);
  }
  
  void
 -Timeline::left_up (wxMouseEvent& ev)
 +DCPTimeline::left_up (wxMouseEvent& ev)
  {
        _left_down = false;
  
  }
  
  void
 -Timeline::mouse_moved (wxMouseEvent& ev)
 +DCPTimeline::mouse_moved (wxMouseEvent& ev)
  {
        if (!_left_down) {
                return;
  }
  
  void
 -Timeline::right_down (wxMouseEvent& ev)
 +DCPTimeline::right_down (wxMouseEvent& ev)
  {
        shared_ptr<View> view = event_to_view (ev);
        shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (view);
  }
  
  void
 -Timeline::set_position_from_event (wxMouseEvent& ev)
 +DCPTimeline::set_position_from_event (wxMouseEvent& ev)
  {
        wxPoint const p = ev.GetPosition();
  
                return;
        }
        
 -      Time new_position = _down_view_position + (p.x - _down_point.x) / _pixels_per_time_unit;
 +      DCPTime new_position = _down_view_position + (p.x - _down_point.x) / _pixels_per_time_unit;
        
        if (_snap) {
                
                bool first = true;
 -              Time nearest_distance = TIME_MAX;
 -              Time nearest_new_position = TIME_MAX;
 +              DCPTime nearest_distance = TIME_MAX;
 +              DCPTime nearest_new_position = TIME_MAX;
                
                /* Find the nearest content edge; this is inefficient */
                for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
                        
                        {
                                /* Snap starts to ends */
 -                              Time const d = abs (cv->content()->end() - new_position);
 +                              DCPTime const d = abs (cv->content()->end() - new_position);
                                if (first || d < nearest_distance) {
                                        nearest_distance = d;
                                        nearest_new_position = cv->content()->end();
                        
                        {
                                /* Snap ends to starts */
 -                              Time const d = abs (cv->content()->position() - (new_position + _down_view->content()->length_after_trim()));
 +                              DCPTime const d = abs (cv->content()->position() - (new_position + _down_view->content()->length_after_trim()));
                                if (d < nearest_distance) {
                                        nearest_distance = d;
                                        nearest_new_position = cv->content()->position() - _down_view->content()->length_after_trim ();
  }
  
  void
 -Timeline::force_redraw (dcpomatic::Rect<int> const & r)
 +DCPTimeline::force_redraw (dcpomatic::Rect<int> const & r)
  {
        RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
  }
  
  shared_ptr<const Film>
 -Timeline::film () const
 +DCPTimeline::film () const
  {
        return _film.lock ();
  }
  
  void
 -Timeline::resized ()
 +DCPTimeline::resized ()
  {
        setup_pixels_per_time_unit ();
  }
  
  void
 -Timeline::clear_selection ()
 +DCPTimeline::clear_selection ()
  {
        for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
                shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
        }
  }
  
 -Timeline::ContentViewList
 -Timeline::selected_views () const
 +DCPTimeline::ContentViewList
 +DCPTimeline::selected_views () const
  {
        ContentViewList sel;
        
  }
  
  ContentList
 -Timeline::selected_content () const
 +DCPTimeline::selected_content () const
  {
        ContentList sel;
        ContentViewList views = selected_views ();
diff --combined test/resampler_test.cc
index 5bee3603b2e8c71df95ab07cf5681a9dd8478a25,9247159a7065b1a46ecf06d49f74f8cfdbab6695..3be251b3a75129c0daa83f8182ab19f7df51dc42
@@@ -33,15 -33,14 +33,14 @@@ resampler_test_one (int from, int to
        int total_out = 0;
  
        /* 3 hours */
-       int64_t const N = from * 60 * 60 * 3;
+       int64_t const N = int64_t (from) * 60 * 60 * 3;
 -      
++              
 +      /* XXX: no longer checks anything */
-       
        for (int64_t i = 0; i < N; i += 1000) {
                shared_ptr<AudioBuffers> a (new AudioBuffers (1, 1000));
                a->make_silent ();
 -              pair<shared_ptr<const AudioBuffers>, AudioContent::Frame> r = resamp.run (a, i);
 -              BOOST_CHECK_EQUAL (r.second, total_out);
 -              total_out += r.first->frames ();
 +              shared_ptr<const AudioBuffers> r = resamp.run (a);
 +              total_out += r->frames ();
        }
  }     
                
diff --combined test/wscript
index 24daa7762001dc3d0ee764d58030cd4f6625a180,676f471049cfcf835e5da2bde766ef3ee053f687..ec8dfd42c7efb53780e591308f2dd5319ff3fbff
@@@ -10,9 -10,9 +10,9 @@@ def configure(conf)
                                """, msg = 'Checking for boost unit testing library', lib = 'boost_unit_test_framework%s' % boost_test_suffix, uselib_store = 'BOOST_TEST')
  
  def build(bld):
 -    obj = bld(features = 'cxx cxxprogram')
 +    obj = bld(features='cxx cxxprogram')
      obj.name   = 'unit-tests'
-     obj.uselib = 'BOOST_TEST DCP OPENJPEG AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE POSTPROC CXML'
+     obj.uselib = 'BOOST_TEST BOOST_THREAD DCP OPENJPEG AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE POSTPROC CXML'
      obj.use    = 'libdcpomatic'
      obj.source = """
                   4k_test.cc
@@@ -27,7 -27,6 +27,7 @@@
                   ffmpeg_dcp_test.cc
                   ffmpeg_examiner_test.cc
                   ffmpeg_pts_offset.cc
 +                 ffmpeg_seek_test.cc
                   file_group_test.cc
                   film_metadata_test.cc
                   frame_rate_test.cc
                   pixel_formats_test.cc
                   play_test.cc
                   ratio_test.cc
 +                 repeat_frame_test.cc
                   recover_test.cc
                   resampler_test.cc
                   scaling_test.cc
 +                 seek_zero_test.cc
                   silence_padding_test.cc
 +                 skip_frame_test.cc
                   stream_test.cc
 +                 subrip_test.cc
                   test.cc
                   threed_test.cc
                   util_test.cc
  
      obj.target = 'unit-tests'
      obj.install_path = ''
 +
 +    obj = bld(features='cxx cxxprogram')
 +    obj.name   = 'long-unit-tests'
 +    obj.uselib = 'BOOST_TEST DCP OPENJPEG AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE POSTPROC CXML'
 +    obj.use    = 'libdcpomatic'
 +    obj.source = """
 +                 test.cc
 +                 long_ffmpeg_seek_test.cc
 +                 """
 +
 +    obj.target = 'long-unit-tests'
 +    obj.install_path = ''
diff --combined wscript
index 6c97047fcdf763ef7fceb72e424928d692801159,50d89f43443e321c289bfdd754347c837364a71e..86cddf7f2a39ff4d355d9ea8ebbe721566b12d78
+++ b/wscript
@@@ -3,37 -3,176 +3,176 @@@ import o
  import sys
  
  APPNAME = 'dcpomatic'
- VERSION = '1.64.0devel'
+ VERSION = '1.64.11devel'
  
  def options(opt):
      opt.load('compiler_cxx')
      opt.load('winres')
  
-     opt.add_option('--enable-debug', action='store_true', default=False, help='build with debugging information and without optimisation')
-     opt.add_option('--disable-gui', action='store_true', default=False, help='disable building of GUI tools')
-     opt.add_option('--target-windows', action='store_true', default=False, help='set up to do a cross-compile to Windows')
-     opt.add_option('--static', action='store_true', default=False, help='build statically, and link statically to libdcp and FFmpeg')
-     opt.add_option('--magickpp-config', action='store', default='Magick++-config', help='path to Magick++-config')
-     opt.add_option('--wx-config', action='store', default='wx-config', help='path to wx-config')
+     opt.add_option('--enable-debug',      action='store_true', default=False, help='build with debugging information and without optimisation')
+     opt.add_option('--disable-gui',       action='store_true', default=False, help='disable building of GUI tools')
+     opt.add_option('--target-windows',    action='store_true', default=False, help='set up to do a cross-compile to make a Windows package')
+     opt.add_option('--target-debian',     action='store_true', default=False, help='set up to compile for a Debian/Ubuntu package')
+     opt.add_option('--target-centos',     action='store_true', default=False, help='set up to compile for a Centos package')
+     opt.add_option('--magickpp-config',   action='store',      default='Magick++-config', help='path to Magick++-config')
+     opt.add_option('--wx-config',         action='store',      default='wx-config', help='path to wx-config')
      opt.add_option('--address-sanitizer', action='store_true', default=False, help='build with address sanitizer')
+     opt.add_option('--install-prefix',                         default=None,  help='prefix of where DCP-o-matic will be installed')
  
- def pkg_config_args(conf):
-     if conf.env.STATIC:
-         return '--cflags'
+ def static_ffmpeg(conf):
+     conf.check_cfg(package='libavformat', args='--cflags', uselib_store='AVFORMAT', mandatory=True)
+     conf.env.STLIB_AVFORMAT = ['avformat']
+     conf.check_cfg(package='libavfilter', args='--cflags', uselib_store='AVFILTER', mandatory=True)
+     conf.env.STLIB_AVFILTER = ['avfilter', 'swresample']
+     conf.check_cfg(package='libavcodec', args='--cflags', uselib_store='AVCODEC', mandatory=True)
+     conf.env.STLIB_AVCODEC = ['avcodec']
+     conf.env.LIB_AVCODEC = ['z']
+     conf.check_cfg(package='libavutil', args='--cflags', uselib_store='AVUTIL', mandatory=True)
+     conf.env.STLIB_AVUTIL = ['avutil']
+     conf.check_cfg(package='libswscale', args='--cflags', uselib_store='SWSCALE', mandatory=True)
+     conf.env.STLIB_SWSCALE = ['swscale']
+     conf.check_cfg(package='libswresample', args='--cflags', uselib_store='SWRESAMPLE', mandatory=True)
+     conf.env.STLIB_SWRESAMPLE = ['swresample']
+     conf.check_cfg(package='libpostproc', args='--cflags', uselib_store='POSTPROC', mandatory=True)
+     conf.env.STLIB_POSTPROC = ['postproc']
+ def dynamic_ffmpeg(conf):
+     conf.check_cfg(package='libavformat', args='--cflags --libs', uselib_store='AVFORMAT', mandatory=True)
+     conf.check_cfg(package='libavfilter', args='--cflags --libs', uselib_store='AVFILTER', mandatory=True)
+     conf.check_cfg(package='libavcodec', args='--cflags --libs', uselib_store='AVCODEC', mandatory=True)
+     conf.check_cfg(package='libavutil', args='--cflags --libs', uselib_store='AVUTIL', mandatory=True)
+     conf.check_cfg(package='libswscale', args='--cflags --libs', uselib_store='SWSCALE', mandatory=True)
+     conf.check_cfg(package='libswresample', args='--cflags --libs', uselib_store='SWRESAMPLE', mandatory=True)
+     conf.check_cfg(package='libpostproc', args='--cflags --libs', uselib_store='POSTPROC', mandatory=True)
+ def static_openjpeg(conf):
+     conf.check_cfg(package='libopenjpeg', args='--cflags', atleast_version='1.5.0', uselib_store='OPENJPEG', mandatory=True)
+     conf.check_cfg(package='libopenjpeg', args='--cflags', max_version='1.5.1', mandatory=True)
+     conf.env.STLIB_OPENJPEG = ['openjpeg']
+ def dynamic_openjpeg(conf):
+     conf.check_cfg(package='libopenjpeg', args='--cflags --libs', atleast_version='1.5.0', uselib_store='OPENJPEG', mandatory=True)
+     conf.check_cfg(package='libopenjpeg', args='--cflags --libs', max_version='1.5.1', mandatory=True)
+ def static_dcp(conf, static_boost, static_xmlpp, static_xmlsec, static_ssh):
+     conf.check_cfg(package='libdcp', atleast_version='0.92', args='--cflags', uselib_store='DCP', mandatory=True)
+     conf.env.DEFINES_DCP = [f.replace('\\', '') for f in conf.env.DEFINES_DCP]
+     conf.env.STLIB_DCP = ['dcp', 'asdcp-libdcp', 'kumu-libdcp']
+     conf.env.LIB_DCP = ['glibmm-2.4', 'ssl', 'crypto', 'bz2', 'xslt']
+     if static_boost:
+         conf.env.STLIB_DCP.append('boost_system')
+     if static_xmlpp:
+         conf.env.STLIB_DCP.append('xml++-2.6')
+     else:
+         conf.env.LIB_DCP.append('xml++-2.6')
+     if static_xmlsec:
+         conf.env.STLIB_DCP.append('xmlsec1-openssl')
+         conf.env.STLIB_DCP.append('xmlsec1')
      else:
-         return '--cflags --libs'
+         conf.env.LIB_DCP.append('xmlsec1-openssl')
+         conf.env.LIB_DCP.append('xmlsec1')
+     if static_ssh:
+         conf.env.STLIB_DCP.append('ssh')
+     else:
+         conf.env.LIB_DCP.append('ssh')
+ def dynamic_dcp(conf):
+     conf.check_cfg(package='libdcp', atleast_version='0.92', args='--cflags --libs', uselib_store='DCP', mandatory=True)
+     conf.env.DEFINES_DCP = [f.replace('\\', '') for f in conf.env.DEFINES_DCP]
+ def dynamic_ssh(conf):
+     conf.check_cc(fragment="""
+                            #include <libssh/libssh.h>\n
+                            int main () {\n
+                            ssh_session s = ssh_new ();\n
+                            return 0;\n
+                            }
+                            """, msg='Checking for library libssh', mandatory=True, lib='ssh', uselib_store='SSH')
+ def dynamic_boost(conf, lib_suffix, thread):
+     conf.check_cxx(fragment="""
+                             #include <boost/version.hpp>\n
+                             #if BOOST_VERSION < 104500\n
+                             #error boost too old\n
+                             #endif\n
+                             int main(void) { return 0; }\n
+                             """,
+                    mandatory=True,
+                    msg='Checking for boost library >= 1.45',
+                    okmsg='yes',
+                    errmsg='too old\nPlease install boost version 1.45 or higher.')
+     conf.check_cxx(fragment="""
+                           #include <boost/thread.hpp>\n
+                           int main() { boost::thread t (); }\n
+                           """, msg='Checking for boost threading library',
+                           libpath='/usr/local/lib',
+                             lib=[thread, 'boost_system%s' % lib_suffix],
+                             uselib_store='BOOST_THREAD')
+     conf.check_cxx(fragment="""
+                           #include <boost/filesystem.hpp>\n
+                           int main() { boost::filesystem::copy_file ("a", "b"); }\n
+                           """, msg='Checking for boost filesystem library',
+                             libpath='/usr/local/lib',
+                             lib=['boost_filesystem%s' % lib_suffix, 'boost_system%s' % lib_suffix],
+                             uselib_store='BOOST_FILESYSTEM')
+     conf.check_cxx(fragment="""
+                           #include <boost/date_time.hpp>\n
+                           int main() { boost::gregorian::day_clock::local_day(); }\n
+                           """, msg='Checking for boost datetime library',
+                             libpath='/usr/local/lib',
+                             lib=['boost_date_time%s' % lib_suffix, 'boost_system%s' % lib_suffix],
+                             uselib_store='BOOST_DATETIME')
+     conf.check_cxx(fragment="""
+                           #include <boost/signals2.hpp>\n
+                           int main() { boost::signals2::signal<void (int)> x; }\n
+                           """,
+                             msg='Checking for boost signals2 library',
+                             uselib_store='BOOST_SIGNALS2')
+ def static_boost(conf, lib_suffix):
+     conf.env.STLIB_BOOST_THREAD = ['boost_thread']
+     conf.env.STLIB_BOOST_FILESYSTEM = ['boost_filesystem%s' % lib_suffix]
+     conf.env.STLIB_BOOST_DATETIME = ['boost_date_time%s' % lib_suffix, 'boost_system%s' % lib_suffix]
+     conf.env.STLIB_BOOST_SIGNALS2 = ['boost_signals2']
+ def dynamic_quickmail(conf):
+     conf.check_cxx(fragment="""
+                             #include <quickmail.h>
+                             int main(void) { quickmail_initialize (); }
+                             """,
+                    mandatory=True,
+                    msg='Checking for libquickmail',
+                    libpath='/usr/local/lib',
+                    lib=['quickmail', 'curl'],
+                    uselib_store='QUICKMAIL')
  
  def configure(conf):
      conf.load('compiler_cxx')
      if conf.options.target_windows:
          conf.load('winres')
  
+     # conf.options -> conf.env
      conf.env.TARGET_WINDOWS = conf.options.target_windows
      conf.env.DISABLE_GUI = conf.options.disable_gui
-     conf.env.STATIC = conf.options.static
+     conf.env.TARGET_DEBIAN = conf.options.target_debian
+     conf.env.TARGET_CENTOS = conf.options.target_centos
      conf.env.VERSION = VERSION
      conf.env.TARGET_OSX = sys.platform == 'darwin'
      conf.env.TARGET_LINUX = not conf.env.TARGET_WINDOWS and not conf.env.TARGET_OSX
+     # true if we should build dcpomatic/libdcpomatic/libdcpomatic-wx statically
+     conf.env.BUILD_STATIC = conf.options.target_debian or conf.options.target_centos
+     if conf.options.install_prefix is None:
+         conf.env.INSTALL_PREFIX = conf.env.PREFIX
+     else:
+         conf.env.INSTALL_PREFIX = conf.options.install_prefix
  
      # Common CXXFLAGS
      conf.env.append_value('CXXFLAGS', ['-D__STDC_CONSTANT_MACROS', '-D__STDC_LIMIT_MACROS', '-msse', '-mfpmath=sse', '-ffast-math', '-fno-strict-aliasing',
          conf.env.append_value('CXXFLAGS', ['-fsanitize=address', '-fno-omit-frame-pointer'])
          conf.env.append_value('LINKFLAGS', ['-fsanitize=address'])
  
-     # Windows-specific
+     #
+     # Platform-specific CFLAGS hacks and other tinkering
+     #
+     # Windows
      if conf.env.TARGET_WINDOWS:
          conf.env.append_value('CXXFLAGS', ['-DDCPOMATIC_WINDOWS', '-DWIN32_LEAN_AND_MEAN', '-DBOOST_USE_WINDOWS_H', '-DUNICODE', '-DBOOST_THREAD_PROVIDES_GENERIC_SHARED_MUTEX_ON_WIN'])
          wxrc = os.popen('wx-config --rescomp').read().split()[1:]
          boost_lib_suffix = '-mt'
          boost_thread = 'boost_thread_win32-mt'
  
-     # POSIX-specific
+     # POSIX
      if conf.env.TARGET_LINUX or conf.env.TARGET_OSX:
          conf.env.append_value('CXXFLAGS', '-DDCPOMATIC_POSIX')
          conf.env.append_value('CXXFLAGS', '-DPOSIX_LOCALE_PREFIX="%s/share/locale"' % conf.env['PREFIX'])
          boost_thread = 'boost_thread'
          conf.env.append_value('LINKFLAGS', '-pthread')
  
-     # Linux-specific
+     # Linux
      if conf.env.TARGET_LINUX:
          conf.env.append_value('CXXFLAGS', '-DDCPOMATIC_LINUX')
+     if conf.env.TARGET_DEBIAN:
          # libxml2 seems to be linked against this on Ubuntu but it doesn't mention it in its .pc file
          conf.check_cfg(package='liblzma', args='--cflags --libs', uselib_store='LZMA', mandatory=True)
-         if not conf.env.DISABLE_GUI:
-             if conf.env.STATIC:
-                 conf.check_cfg(package='gtk+-2.0', args='--cflags --libs', uselib_store='GTK', mandatory=True)
-             else:
-                 # On Linux we need to be able to include <gtk/gtk.h> to check GTK's version
-                 conf.check_cfg(package='gtk+-2.0', args='--cflags', uselib_store='GTK', mandatory=True)
-     # OSX-specific
+         
+     if not conf.env.DISABLE_GUI and conf.env.TARGET_LINUX:
+         conf.check_cfg(package='gtk+-2.0', args='--cflags --libs', uselib_store='GTK', mandatory=True)
+     # OSX
      if conf.env.TARGET_OSX:
          conf.env.append_value('CXXFLAGS', '-DDCPOMATIC_OSX')
          conf.env.append_value('LINKFLAGS', '-headerpad_max_install_names')
  
-     # Dependencies which are dynamically linked everywhere except --static
-     # Get libs only when we are dynamically linking
-     conf.check_cfg(package='libdcp',        atleast_version='0.92', args=pkg_config_args(conf), uselib_store='DCP',  mandatory=True)
-     # Remove erroneous escaping of quotes from xmlsec1 defines
-     conf.env.DEFINES_DCP = [f.replace('\\', '') for f in conf.env.DEFINES_DCP]
-     conf.check_cfg(package='libcxml',       atleast_version='0.08', args=pkg_config_args(conf), uselib_store='CXML', mandatory=True)
-     conf.check_cfg(package='libavformat',   args=pkg_config_args(conf), uselib_store='AVFORMAT',   mandatory=True)
-     conf.check_cfg(package='libavfilter',   args=pkg_config_args(conf), uselib_store='AVFILTER',   mandatory=True)
-     conf.check_cfg(package='libavcodec',    args=pkg_config_args(conf), uselib_store='AVCODEC',    mandatory=True)
-     conf.check_cfg(package='libavutil',     args=pkg_config_args(conf), uselib_store='AVUTIL',     mandatory=True)
-     conf.check_cfg(package='libswscale',    args=pkg_config_args(conf), uselib_store='SWSCALE',    mandatory=True)
-     conf.check_cfg(package='libswresample', args=pkg_config_args(conf), uselib_store='SWRESAMPLE', mandatory=True)
-     conf.check_cfg(package='libpostproc',   args=pkg_config_args(conf), uselib_store='POSTPROC',   mandatory=True)
-     conf.check_cfg(package='libopenjpeg',   args=pkg_config_args(conf), atleast_version='1.5.0', uselib_store='OPENJPEG', mandatory=True)
-     conf.check_cfg(package='libopenjpeg',   args=pkg_config_args(conf), max_version='1.5.1', mandatory=True)
-     if conf.env.STATIC:
-         # This is hackio grotesquio for static builds (ie for .deb packages).  We need to link some things
-         # statically and some dynamically, or things get horribly confused and the dynamic linker (I think)
-         # crashes.  These calls do what the check_cfg calls would have done, but specify the
-         # different bits as static or dynamic as required.  It'll break if you look at it funny, but
-         # I think anyone else who builds would do so dynamically.
-         conf.env.STLIB_CXML       = ['cxml']
-         conf.env.STLIB_DCP        = ['dcp', 'asdcp-libdcp', 'kumu-libdcp']
-         conf.env.LIB_DCP          = ['glibmm-2.4', 'xml++-2.6', 'ssl', 'crypto', 'bz2', 'xmlsec1', 'xmlsec1-openssl', 'xslt']
-         conf.env.STLIB_CXML       = ['cxml']
-         conf.env.STLIB_AVFORMAT   = ['avformat']
-         conf.env.STLIB_AVFILTER   = ['avfilter', 'swresample']
-         conf.env.STLIB_AVCODEC    = ['avcodec']
-         conf.env.LIB_AVCODEC      = ['z']
-         conf.env.STLIB_AVUTIL     = ['avutil']
-         conf.env.STLIB_SWSCALE    = ['swscale']
-         conf.env.STLIB_POSTPROC   = ['postproc']
-         conf.env.STLIB_SWRESAMPLE = ['swresample']
-         conf.env.STLIB_OPENJPEG   = ['openjpeg']
-         conf.env.STLIB_QUICKMAIL  = ['quickmail']
-     else:
-         conf.check_cxx(fragment="""
-                             #include <quickmail.h>
-                             int main(void) { quickmail_initialize (); }
-                             """,
-                        mandatory=True,
-                        msg='Checking for libquickmail',
-                        libpath='/usr/local/lib',
-                        lib=['quickmail', 'curl'],
-                        uselib_store='QUICKMAIL')
+     #
+     # Dependencies.
+     # There's probably a neater way of expressing these, but I've gone for brute force for now.
+     #
  
-     # Dependencies which are always dynamically linked
-     conf.check_cfg(package='sndfile', args='--cflags --libs', uselib_store='SNDFILE', mandatory=True)
-     conf.check_cfg(package='glib-2.0', args='--cflags --libs', uselib_store='GLIB', mandatory=True)
-     conf.check_cfg(package= '', path=conf.options.magickpp_config, args='--cppflags --cxxflags --libs', uselib_store='MAGICK', mandatory=True)
-     conf.check_cfg(package='libxml++-2.6', args='--cflags --libs', uselib_store='XML++', mandatory=True)
-     conf.check_cfg(package='libcurl', args='--cflags --libs', uselib_store='CURL', mandatory=True)
-     conf.check_cfg(package='libzip', args='--cflags --libs', uselib_store='ZIP', mandatory=True)
-     conf.check_cfg(package='pangomm-1.4', args='--cflags --libs', uselib_store='PANGOMM', mandatory=True)
-     conf.check_cfg(package='cairomm-1.0', args='--cflags --libs', uselib_store='CAIROMM', mandatory=True)
+     if conf.env.TARGET_DEBIAN:
+         conf.check_cfg(package='libcxml', atleast_version='0.08', args='--cflags', uselib_store='CXML', mandatory=True)
+         conf.env.STLIB_CXML = ['cxml']
+         conf.check_cfg(package='libxml++-2.6', args='--cflags --libs', uselib_store='XML++', mandatory=True)
+         conf.check_cfg(package='libcurl', args='--cflags --libs', uselib_store='CURL', mandatory=True)
+         conf.env.STLIB_QUICKMAIL = ['quickmail']
+         static_ffmpeg(conf)
+         static_openjpeg(conf)
+         static_dcp(conf, False, False, False, False)
+         dynamic_boost(conf, boost_lib_suffix, boost_thread)
  
-     conf.check_cxx(fragment="""
-                             #include <boost/version.hpp>\n
-                             #if BOOST_VERSION < 104500\n
-                             #error boost too old\n
-                             #endif\n
-                             int main(void) { return 0; }\n
-                             """,
-                    mandatory=True,
-                    msg='Checking for boost library >= 1.45',
-                    okmsg='yes',
-                    errmsg='too old\nPlease install boost version 1.45 or higher.')
+     if conf.env.TARGET_CENTOS:
+         conf.check_cfg(package='libcxml', atleast_version='0.08', args='--cflags --libs-only-L', uselib_store='CXML', mandatory=True)
+         conf.env.STLIB_CXML = ['cxml', 'boost_filesystem']
+         conf.check_cfg(package='libcurl', args='--cflags --libs-only-L', uselib_store='CURL', mandatory=True)
+         conf.env.STLIB_CURL = ['curl']
+         conf.env.LIB_CURL = ['ssh2', 'idn']
+         conf.env.STLIB_QUICKMAIL = ['quickmail', 'curl']
+         conf.env.LIB_QUICKMAIL = ['ssh2', 'idn']
+         static_ffmpeg(conf)
+         static_openjpeg(conf)
+         static_dcp(conf, True, True, True, True)
+         static_boost(conf, boost_lib_suffix)
  
-     conf.check_cc(fragment="""
-                            #include <libssh/libssh.h>\n
-                            int main () {\n
-                            ssh_session s = ssh_new ();\n
-                            return 0;\n
-                            }
-                            """, msg='Checking for library libssh', mandatory=True, lib='ssh', uselib_store='SSH')
-     conf.check_cxx(fragment="""
-                           #include <boost/thread.hpp>\n
-                           int main() { boost::thread t (); }\n
-                           """, msg='Checking for boost threading library',
-                           libpath='/usr/local/lib',
-                             lib=[boost_thread, 'boost_system%s' % boost_lib_suffix],
-                             uselib_store='BOOST_THREAD')
-     conf.check_cxx(fragment="""
-                           #include <boost/filesystem.hpp>\n
-                           int main() { boost::filesystem::copy_file ("a", "b"); }\n
-                           """, msg='Checking for boost filesystem library',
-                             libpath='/usr/local/lib',
-                             lib=['boost_filesystem%s' % boost_lib_suffix, 'boost_system%s' % boost_lib_suffix],
-                             uselib_store='BOOST_FILESYSTEM')
-     conf.check_cxx(fragment="""
-                           #include <boost/date_time.hpp>\n
-                           int main() { boost::gregorian::day_clock::local_day(); }\n
-                           """, msg='Checking for boost datetime library',
-                             libpath='/usr/local/lib',
-                             lib=['boost_date_time%s' % boost_lib_suffix, 'boost_system%s' % boost_lib_suffix],
-                             uselib_store='BOOST_DATETIME')
-     # Only Windows versions require boost::locale, which is handy, as it was only introduced in
-     # boost 1.48 and so isn't (easily) available on old Ubuntus.
      if conf.env.TARGET_WINDOWS:
+         conf.check_cfg(package='libxml++-2.6', args='--cflags --libs', uselib_store='XML++', mandatory=True)
+         conf.check_cfg(package='libcurl', args='--cflags --libs', uselib_store='CURL', mandatory=True)
          conf.check_cxx(fragment="""
                                #include <boost/locale.hpp>\n
                                int main() { std::locale::global (boost::locale::generator().generate ("")); }\n
                                  libpath='/usr/local/lib',
                                  lib=['boost_locale%s' % boost_lib_suffix, 'boost_system%s' % boost_lib_suffix],
                                  uselib_store='BOOST_LOCALE')
+         dynamic_quickmail(conf)
+         dynamic_boost(conf, boost_lib_suffix, boost_thread)
+         dynamic_ffmpeg(conf)
+         dynamic_openjpeg(conf)
+         dynamic_dcp(conf)
+         dynamic_ssh(conf)
  
-     conf.check_cxx(fragment="""
-                           #include <boost/signals2.hpp>\n
-                           int main() { boost::signals2::signal<void (int)> x; }\n
-                           """,
-                             msg='Checking for boost signals2 library',
-                             uselib_store='BOOST_SIGNALS2')
+     # Not packaging; just a straight build
+     if not conf.env.TARGET_WINDOWS and not conf.env.TARGET_DEBIAN and not conf.env.TARGET_CENTOS:
+         conf.check_cfg(package='libcxml', atleast_version='0.08', args='--cflags --libs', uselib_store='CXML', mandatory=True)
+         conf.check_cfg(package='libxml++-2.6', args='--cflags --libs', uselib_store='XML++', mandatory=True)
+         conf.check_cfg(package='libcurl', args='--cflags --libs', uselib_store='CURL', mandatory=True)
+         dynamic_quickmail(conf)
+         dynamic_boost(conf, boost_lib_suffix, boost_thread)
+         dynamic_ffmpeg(conf)
+         dynamic_dcp(conf)
+         dynamic_openjpeg(conf)
+         dynamic_ssh(conf)
+     # Dependencies which are always dynamically linked
+     conf.check_cfg(package='sndfile', args='--cflags --libs', uselib_store='SNDFILE', mandatory=True)
+     conf.check_cfg(package='glib-2.0', args='--cflags --libs', uselib_store='GLIB', mandatory=True)
+     conf.check_cfg(package= '', path=conf.options.magickpp_config, args='--cppflags --cxxflags --libs', uselib_store='MAGICK', mandatory=True)
+     conf.check_cfg(package='libzip', args='--cflags --libs', uselib_store='ZIP', mandatory=True)
++    conf.check_cfg(package='pangomm-1.4', args='--cflags --libs', uselib_store='PANGOMM', mandatory=True)
++    conf.check_cfg(package='cairomm-1.0', args='--cflags --libs', uselib_store='CAIROMM', mandatory=True)
  
      conf.check_cc(fragment="""
                             #include <glib.h>