Merge master.
authorCarl Hetherington <cth@carlh.net>
Mon, 17 Mar 2014 00:22:52 +0000 (00:22 +0000)
committerCarl Hetherington <cth@carlh.net>
Mon, 17 Mar 2014 00:22:52 +0000 (00:22 +0000)
24 files changed:
1  2 
ChangeLog
src/lib/audio_content.cc
src/lib/audio_content.h
src/lib/config.h
src/lib/content.cc
src/lib/content_factory.cc
src/lib/ffmpeg_content.cc
src/lib/ffmpeg_content.h
src/lib/ffmpeg_decoder.cc
src/lib/film.cc
src/lib/film.h
src/lib/filter_graph.cc
src/lib/image.cc
src/lib/image.h
src/lib/player.cc
src/lib/playlist.cc
src/lib/playlist.h
src/lib/video_content.cc
src/lib/video_content.h
src/lib/writer.cc
src/wx/config_dialog.cc
src/wx/film_viewer.cc
src/wx/video_panel.cc
wscript

diff --combined ChangeLog
index 7c6f4ebdde0727b1691df2d9a2ba67faf39fc009,c4c2c9c631dca1cbd6b2a0f55474a3b8c0920b23..e70b7373e8d8059f212a3211b9aaddbbf3a6303a
+++ b/ChangeLog
@@@ -1,9 -1,48 +1,52 @@@
 +2014-03-07  Carl Hetherington  <cth@carlh.net>
 +
 +      * Add subtitle view.
 +
+ 2014-03-17  Carl Hetherington  <cth@carlh.net>
+       * Improve appearance of config dialog on OS X.
+ 2014-03-15  Carl Hetherington  <cth@carlh.net>
+       * Improve appearance of new film and KDM dialogs on OS X.
+       * Fix KDM dialog to predictably set up its initial range to
+       a week from now.
+       * Remove support for FFmpeg post-processing filters as they apparently
+       do not support > 8bpp.  I don't think they are worth the pain of
+       quantizing and then telling the user what has happened.
+ 2014-03-12  Carl Hetherington  <cth@carlh.net>
+       * Version 1.66.1 released.
+ 2014-03-12  Carl Hetherington  <cth@carlh.net>
+       * Hopefully fix i18n on OS X (#324).
+ 2014-03-10  Carl Hetherington  <cth@carlh.net>
+       * Version 1.66.0 released.
+ 2014-03-09  Carl Hetherington  <cth@carlh.net>
+       * Version 1.65.2 released.
+ 2014-03-09  Carl Hetherington  <cth@carlh.net>
+       * Restore old behaviour of "no-stretch" mode with crop.
+       * Fix display of no-scale display mode in the player.
  2014-03-08  Carl Hetherington  <cth@carlh.net>
  
+       * Version 1.65.1 released.
+ 2014-03-08  Carl Hetherington  <cth@carlh.net>
+       * Fix incorrect audio analyses on multiple-stream content.
        * Support for unsigned 8-bit audio (hmm!).
  
  2014-03-06  Carl Hetherington  <cth@carlh.net>
diff --combined src/lib/audio_content.cc
index 01d1ecc382ad8b386be9f7c5d0fe45a9b0d51de6,b96300e15eb6c8c362f5964814a7ad1d4213eda8..6da5afa0c986a22bd45a52f90f434d9fab0df4bf
@@@ -40,7 -40,7 +40,7 @@@ int const AudioContentProperty::AUDIO_G
  int const AudioContentProperty::AUDIO_DELAY = 204;
  int const AudioContentProperty::AUDIO_MAPPING = 205;
  
 -AudioContent::AudioContent (shared_ptr<const Film> f, Time s)
 +AudioContent::AudioContent (shared_ptr<const Film> f, DCPTime s)
        : Content (f, s)
        , _audio_gain (0)
        , _audio_delay (Config::instance()->default_audio_delay ())
@@@ -141,17 -141,13 +141,19 @@@ AudioContent::audio_analysis_path () co
                return boost::filesystem::path ();
        }
  
-       return film->audio_analysis_path (dynamic_pointer_cast<const AudioContent> (shared_from_this ()));
+       boost::filesystem::path p = film->audio_analysis_dir ();
+       p /= digest ();
+       return p;
  }
  
  string
  AudioContent::technical_summary () const
  {
 -      return String::compose ("audio: channels %1, length %2, raw rate %3, out rate %4", audio_channels(), audio_length(), content_audio_frame_rate(), output_audio_frame_rate());
 +      return String::compose (
 +              "audio: channels %1, length %2, raw rate %3, out rate %4",
 +              audio_channels(),
 +              audio_length().seconds(),
 +              content_audio_frame_rate(),
 +              output_audio_frame_rate()
 +              );
  }
diff --combined src/lib/audio_content.h
index 18d88bccb98542d2a54adae4a75ac5aaf4702bed,d30db02d775709d270eb75eed7a421aa1c877907..cecc8f13d65c1a0875a0dca0f0b2e383a37eb20c
@@@ -43,7 -43,7 +43,7 @@@ class AudioContent : public virtual Con
  public:
        typedef int64_t Frame;
        
 -      AudioContent (boost::shared_ptr<const Film>, Time);
 +      AudioContent (boost::shared_ptr<const Film>, DCPTime);
        AudioContent (boost::shared_ptr<const Film>, boost::filesystem::path);
        AudioContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>);
        AudioContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
        std::string technical_summary () const;
  
        virtual int audio_channels () const = 0;
 -      virtual AudioContent::Frame audio_length () const = 0;
 +      virtual ContentTime audio_length () const = 0;
        virtual int content_audio_frame_rate () const = 0;
        virtual int output_audio_frame_rate () const = 0;
        virtual AudioMapping audio_mapping () const = 0;
        virtual void set_audio_mapping (AudioMapping) = 0;
+       virtual boost::filesystem::path audio_analysis_path () const;
  
        boost::signals2::connection analyse_audio (boost::function<void()>);
-       boost::filesystem::path audio_analysis_path () const;
  
        void set_audio_gain (float);
        void set_audio_delay (int);
diff --combined src/lib/config.h
index 14cd640eeeefb2a042086dd600b447626103f044,b9e8d6b021b3939e454a7e8d319d8e22c0716bf4..68aae7414a009206f5984a46630f7a5b3c56895c
@@@ -28,7 -28,7 +28,7 @@@
  #include <boost/shared_ptr.hpp>
  #include <boost/signals2.hpp>
  #include <boost/filesystem.hpp>
 -#include <libdcp/metadata.h>
 +#include <dcp/metadata.h>
  #include "dci_metadata.h"
  #include "colour_conversion.h"
  #include "server.h"
@@@ -66,6 -66,7 +66,7 @@@ public
  
        void set_use_any_servers (bool u) {
                _use_any_servers = u;
+               write ();
        }
  
        bool use_any_servers () const {
@@@ -75,6 -76,7 +76,7 @@@
        /** @param s New list of servers */
        void set_servers (std::vector<std::string> s) {
                _servers = s;
+               write ();
        }
  
        /** @return Host names / IP addresses of J2K encoding servers that should definitely be used */
                return _default_dcp_content_type;
        }
  
 -      libdcp::XMLMetadata dcp_metadata () const {
 +      dcp::XMLMetadata dcp_metadata () const {
                return _dcp_metadata;
        }
  
        /** @param n New number of local encoding threads */
        void set_num_local_encoding_threads (int n) {
                _num_local_encoding_threads = n;
+               write ();
        }
  
        void set_default_directory (boost::filesystem::path d) {
                _default_directory = d;
+               write ();
        }
  
        /** @param p New server port */
        void set_server_port_base (int p) {
                _server_port_base = p;
+               write ();
        }
  
        /** @param i IP address of a TMS that we can copy DCPs to */
        void set_tms_ip (std::string i) {
                _tms_ip = i;
+               write ();
        }
  
        /** @param p Path on a TMS that we should write DCPs to */
        void set_tms_path (std::string p) {
                _tms_path = p;
+               write ();
        }
  
        /** @param u User name to log into the TMS with */
        void set_tms_user (std::string u) {
                _tms_user = u;
+               write ();
        }
  
        /** @param p Password to log into the TMS with */
        void set_tms_password (std::string p) {
                _tms_password = p;
+               write ();
        }
  
        void add_cinema (boost::shared_ptr<Cinema> c) {
  
        void set_allowed_dcp_frame_rates (std::list<int> const & r) {
                _allowed_dcp_frame_rates = r;
+               write ();
        }
  
        void set_default_dci_metadata (DCIMetadata d) {
                _default_dci_metadata = d;
+               write ();
        }
  
        void set_language (std::string l) {
                _language = l;
+               write ();
        }
  
        void unset_language () {
                _language = boost::none;
+               write ();
        }
  
        void set_default_still_length (int s) {
                _default_still_length = s;
+               write ();
        }
  
        void set_default_container (Ratio const * c) {
                _default_container = c;
+               write ();
        }
  
        void set_default_dcp_content_type (DCPContentType const * t) {
                _default_dcp_content_type = t;
+               write ();
        }
  
 -      void set_dcp_metadata (libdcp::XMLMetadata m) {
 +      void set_dcp_metadata (dcp::XMLMetadata m) {
                _dcp_metadata = m;
+               write ();
        }
  
        void set_default_j2k_bandwidth (int b) {
                _default_j2k_bandwidth = b;
+               write ();
        }
  
        void set_default_audio_delay (int d) {
                _default_audio_delay = d;
+               write ();
        }
  
        void set_colour_conversions (std::vector<PresetColourConversion> const & c) {
                _colour_conversions = c;
+               write ();
        }
  
        void set_mail_server (std::string s) {
                _mail_server = s;
+               write ();
        }
  
        void set_mail_user (std::string u) {
                _mail_user = u;
+               write ();
        }
  
        void set_mail_password (std::string p) {
                _mail_password = p;
+               write ();
        }
  
        void set_kdm_from (std::string f) {
                _kdm_from = f;
+               write ();
        }
  
        void set_kdm_email (std::string e) {
                _kdm_email = e;
+               write ();
        }
  
        void set_check_for_updates (bool c) {
                _check_for_updates = c;
+               write ();
        }
  
        void set_check_for_test_updates (bool c) {
                _check_for_test_updates = c;
+               write ();
        }
        
        void write () const;
@@@ -335,7 -362,7 +362,7 @@@ private
        int _default_still_length;
        Ratio const * _default_container;
        DCPContentType const * _default_dcp_content_type;
 -      libdcp::XMLMetadata _dcp_metadata;
 +      dcp::XMLMetadata _dcp_metadata;
        int _default_j2k_bandwidth;
        int _default_audio_delay;
        std::vector<PresetColourConversion> _colour_conversions;
diff --combined src/lib/content.cc
index 814d9c1a5c00afc2ade07ed4d70f9b92cc536293,8294682475a65b30ceb6717f5eea37b1dff33fba..1fb4681a251b2810c662426912114f6a2af181e7
@@@ -54,7 -54,7 +54,7 @@@ Content::Content (shared_ptr<const Film
  
  }
  
 -Content::Content (shared_ptr<const Film> f, Time p)
 +Content::Content (shared_ptr<const Film> f, DCPTime p)
        : _film (f)
        , _position (p)
        , _trim_start (0)
@@@ -83,9 -83,9 +83,9 @@@ Content::Content (shared_ptr<const Film
                _paths.push_back ((*i)->content ());
        }
        _digest = node->string_child ("Digest");
 -      _position = node->number_child<Time> ("Position");
 -      _trim_start = node->number_child<Time> ("TrimStart");
 -      _trim_end = node->number_child<Time> ("TrimEnd");
 +      _position = DCPTime (node->number_child<double> ("Position"));
 +      _trim_start = DCPTime (node->number_child<double> ("TrimStart"));
 +      _trim_end = DCPTime (node->number_child<double> ("TrimEnd"));
  }
  
  Content::Content (shared_ptr<const Film> f, vector<shared_ptr<Content> > c)
        , _change_signals_frequent (false)
  {
        for (size_t i = 0; i < c.size(); ++i) {
 -              if (i > 0 && c[i]->trim_start ()) {
 +              if (i > 0 && c[i]->trim_start() > DCPTime()) {
                        throw JoinError (_("Only the first piece of content to be joined can have a start trim."));
                }
  
 -              if (i < (c.size() - 1) && c[i]->trim_end ()) {
 +              if (i < (c.size() - 1) && c[i]->trim_end () > DCPTime()) {
                        throw JoinError (_("Only the last piece of content to be joined can have an end trim."));
                }
  
@@@ -119,9 -119,9 +119,9 @@@ Content::as_xml (xmlpp::Node* node) con
                node->add_child("Path")->add_child_text (i->string ());
        }
        node->add_child("Digest")->add_child_text (_digest);
 -      node->add_child("Position")->add_child_text (lexical_cast<string> (_position));
 -      node->add_child("TrimStart")->add_child_text (lexical_cast<string> (_trim_start));
 -      node->add_child("TrimEnd")->add_child_text (lexical_cast<string> (_trim_end));
 +      node->add_child("Position")->add_child_text (lexical_cast<string> (_position.get ()));
 +      node->add_child("TrimStart")->add_child_text (lexical_cast<string> (_trim_start.get ()));
 +      node->add_child("TrimEnd")->add_child_text (lexical_cast<string> (_trim_end.get ()));
  }
  
  void
@@@ -146,7 -146,7 +146,7 @@@ Content::signal_changed (int p
  }
  
  void
 -Content::set_position (Time p)
 +Content::set_position (DCPTime p)
  {
        {
                boost::mutex::scoped_lock lm (_mutex);
  }
  
  void
 -Content::set_trim_start (Time t)
 +Content::set_trim_start (DCPTime t)
  {
        {
                boost::mutex::scoped_lock lm (_mutex);
  }
  
  void
 -Content::set_trim_end (Time t)
 +Content::set_trim_end (DCPTime t)
  {
        {
                boost::mutex::scoped_lock lm (_mutex);
@@@ -195,21 -195,33 +195,24 @@@ Content::clone () cons
        xmlpp::Document doc;
        xmlpp::Node* node = doc.create_root_node ("Content");
        as_xml (node);
-       return content_factory (film, cxml::NodePtr (new cxml::Node (node)), Film::current_state_version);
+       /* notes is unused here (we assume) */
+       list<string> notes;
+       return content_factory (film, cxml::NodePtr (new cxml::Node (node)), Film::current_state_version, notes);
  }
  
  string
  Content::technical_summary () const
  {
 -      return String::compose ("%1 %2 %3", path_summary(), digest(), position());
 +      return String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
  }
  
 -Time
 +DCPTime
  Content::length_after_trim () const
  {
        return full_length() - trim_start() - trim_end();
  }
  
 -/** @param t A time relative to the start of this content (not the position).
 - *  @return true if this time is trimmed by our trim settings.
 - */
 -bool
 -Content::trimmed (Time t) const
 -{
 -      return (t < trim_start() || t > (full_length() - trim_end ()));
 -}
 -
  /** @return string which includes everything about how this content affects
   *  its playlist.
   */
@@@ -219,9 -231,9 +222,9 @@@ Content::identifier () cons
        stringstream s;
        
        s << Content::digest()
 -        << "_" << position()
 -        << "_" << trim_start()
 -        << "_" << trim_end();
 +        << "_" << position().get()
 +        << "_" << trim_start().get()
 +        << "_" << trim_end().get();
  
        return s.str ();
  }
index 825c8049854ba3032519b7d27430560a18d9f8b8,98b1dd859536643c83c524e3a22a4df45b039c8a..092efd7903d7dba9219c483c57a03307d6733945
  #include "ffmpeg_content.h"
  #include "image_content.h"
  #include "sndfile_content.h"
 +#include "subrip_content.h"
  #include "util.h"
  
  using std::string;
+ using std::list;
  using boost::shared_ptr;
  
  shared_ptr<Content>
- content_factory (shared_ptr<const Film> film, cxml::NodePtr node, int version)
+ content_factory (shared_ptr<const Film> film, cxml::NodePtr node, int version, list<string>& notes)
  {
        string const type = node->string_child ("Type");
  
        boost::shared_ptr<Content> content;
        
        if (type == "FFmpeg") {
-               content.reset (new FFmpegContent (film, node, version));
+               content.reset (new FFmpegContent (film, node, version, notes));
        } else if (type == "Image") {
                content.reset (new ImageContent (film, node, version));
        } else if (type == "Sndfile") {
                content.reset (new SndfileContent (film, node, version));
 +      } else if (type == "SubRip") {
 +              content.reset (new SubRipContent (film, node, version));
        }
  
        return content;
@@@ -51,16 -49,11 +52,16 @@@ shared_ptr<Content
  content_factory (shared_ptr<const Film> film, boost::filesystem::path path)
  {
        shared_ptr<Content> content;
 +
 +      string ext = path.extension().string ();
 +      transform (ext.begin(), ext.end(), ext.begin(), ::tolower);
                
        if (valid_image_file (path)) {
                content.reset (new ImageContent (film, path));
        } else if (SndfileContent::valid_file (path)) {
                content.reset (new SndfileContent (film, path));
 +      } else if (ext == ".srt") {
 +              content.reset (new SubRipContent (film, path));
        } else {
                content.reset (new FFmpegContent (film, path));
        }
index 2b535a2ab75182eb2cb698489987ab8d46e6aec7,fadeec9cdc00a3126f2d9d066df8d04518229015..90c00283d940fc79c43f1e0cc3131ae92507e01b
@@@ -58,7 -58,7 +58,7 @@@ FFmpegContent::FFmpegContent (shared_pt
  
  }
  
- FFmpegContent::FFmpegContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version)
+ FFmpegContent::FFmpegContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version, list<string>& notes)
        : Content (f, node)
        , VideoContent (f, node, version)
        , AudioContent (f, node)
  
        c = node->node_children ("Filter");
        for (list<cxml::NodePtr>::iterator i = c.begin(); i != c.end(); ++i) {
-               _filters.push_back (Filter::from_id ((*i)->content ()));
+               Filter const * f = Filter::from_id ((*i)->content ());
+               if (f) {
+                       _filters.push_back (f);
+               } else {
+                       notes.push_back (String::compose (_("DCP-o-matic no longer supports the `%1' filter, so it has been turned off."), (*i)->content()));
+               }
        }
  
        _first_video = node->optional_number_child<double> ("FirstVideo");
@@@ -147,7 -152,7 +152,7 @@@ FFmpegContent::as_xml (xmlpp::Node* nod
        }
  
        if (_first_video) {
 -              node->add_child("FirstVideo")->add_child_text (lexical_cast<string> (_first_video.get ()));
 +              node->add_child("FirstVideo")->add_child_text (lexical_cast<string> (_first_video.get().get()));
        }
  }
  
@@@ -158,14 -163,14 +163,14 @@@ FFmpegContent::examine (shared_ptr<Job
  
        Content::examine (job);
  
 -      shared_ptr<const Film> film = _film.lock ();
 -      assert (film);
 -
        shared_ptr<FFmpegExaminer> examiner (new FFmpegExaminer (shared_from_this ()));
 +      take_from_video_examiner (examiner);
 +
 +      ContentTime video_length = examiner->video_length ();
  
 -      VideoContent::Frame video_length = 0;
 -      video_length = examiner->video_length ();
 -      film->log()->log (String::compose ("Video length obtained from header as %1 frames", video_length));
 +      shared_ptr<const Film> film = _film.lock ();
 +      assert (film);
 +      film->log()->log (String::compose ("Video length obtained from header as %1 frames", video_length.frames (video_frame_rate ())));
  
        {
                boost::mutex::scoped_lock lm (_mutex);
                _first_video = examiner->first_video ();
        }
  
 -      take_from_video_examiner (examiner);
 -
        signal_changed (ContentProperty::LENGTH);
        signal_changed (FFmpegContentProperty::SUBTITLE_STREAMS);
        signal_changed (FFmpegContentProperty::SUBTITLE_STREAM);
@@@ -213,26 -220,26 +218,26 @@@ FFmpegContent::technical_summary () con
                ss = _subtitle_stream->technical_summary ();
        }
  
-       pair<string, string> filt = Filter::ffmpeg_strings (_filters);
+       string filt = Filter::ffmpeg_string (_filters);
        
        return Content::technical_summary() + " - "
                + VideoContent::technical_summary() + " - "
                + AudioContent::technical_summary() + " - "
                + String::compose (
-                       "ffmpeg: audio %1, subtitle %2, filters %3 %4", as, ss, filt.first, filt.second
+                       "ffmpeg: audio %1, subtitle %2, filters %3", as, ss, filt
                        );
  }
  
  string
  FFmpegContent::information () const
  {
 -      if (video_length() == 0 || video_frame_rate() == 0) {
 +      if (video_length() == ContentTime (0) || video_frame_rate() == 0) {
                return "";
        }
        
        stringstream s;
        
 -      s << String::compose (_("%1 frames; %2 frames per second"), video_length(), video_frame_rate()) << "\n";
 +      s << String::compose (_("%1 frames; %2 frames per second"), video_length().frames (video_frame_rate()), video_frame_rate()) << "\n";
        s << VideoContent::information ();
  
        return s.str ();
@@@ -260,17 -267,19 +265,17 @@@ FFmpegContent::set_audio_stream (shared
        signal_changed (FFmpegContentProperty::AUDIO_STREAM);
  }
  
 -AudioContent::Frame
 +ContentTime
  FFmpegContent::audio_length () const
  {
 -      int const cafr = content_audio_frame_rate ();
 -      int const vfr  = video_frame_rate ();
 -      VideoContent::Frame const vl = video_length ();
 -
 -      boost::mutex::scoped_lock lm (_mutex);
 -      if (!_audio_stream) {
 -              return 0;
 +      {
 +              boost::mutex::scoped_lock lm (_mutex);
 +              if (!_audio_stream) {
 +                      return ContentTime ();
 +              }
        }
 -      
 -      return video_frames_to_audio_frames (vl, cafr, vfr);
 +
 +      return video_length();
  }
  
  int
@@@ -306,15 -315,16 +311,15 @@@ FFmpegContent::output_audio_frame_rate 
        /* Resample to a DCI-approved sample rate */
        double t = dcp_audio_frame_rate (content_audio_frame_rate ());
  
 -      FrameRateConversion frc (video_frame_rate(), film->video_frame_rate());
 +      FrameRateChange frc (video_frame_rate(), film->video_frame_rate());
  
        /* Compensate if the DCP is being run at a different frame rate
           to the source; that is, if the video is run such that it will
           look different in the DCP compared to the source (slower or faster).
 -         skip/repeat doesn't come into effect here.
        */
  
        if (frc.change_speed) {
 -              t *= video_frame_rate() * frc.factor() / film->video_frame_rate();
 +              t /= frc.speed_up;
        }
  
        return rint (t);
@@@ -362,7 -372,7 +367,7 @@@ FFmpegAudioStream::as_xml (xmlpp::Node
        root->add_child("FrameRate")->add_child_text (lexical_cast<string> (frame_rate));
        root->add_child("Channels")->add_child_text (lexical_cast<string> (channels));
        if (first_audio) {
 -              root->add_child("FirstAudio")->add_child_text (lexical_cast<string> (first_audio.get ()));
 +              root->add_child("FirstAudio")->add_child_text (lexical_cast<string> (first_audio.get().get()));
        }
        mapping.as_xml (root->add_child("Mapping"));
  }
@@@ -412,12 -422,14 +417,12 @@@ FFmpegSubtitleStream::as_xml (xmlpp::No
        FFmpegStream::as_xml (root);
  }
  
 -Time
 +DCPTime
  FFmpegContent::full_length () const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
 -      
 -      FrameRateConversion frc (video_frame_rate (), film->video_frame_rate ());
 -      return video_length() * frc.factor() * TIME_HZ / film->video_frame_rate ();
 +      return DCPTime (video_length(), FrameRateChange (video_frame_rate (), film->video_frame_rate ()));
  }
  
  AudioMapping
@@@ -470,3 -482,23 +475,23 @@@ FFmpegContent::identifier () cons
        return s.str ();
  }
  
+ boost::filesystem::path
+ FFmpegContent::audio_analysis_path () const
+ {
+       shared_ptr<const Film> film = _film.lock ();
+       if (!film) {
+               return boost::filesystem::path ();
+       }
+       /* We need to include the stream ID in this path so that we get different
+          analyses for each stream.
+       */
+       boost::filesystem::path p = film->audio_analysis_dir ();
+       string name = digest ();
+       if (audio_stream ()) {
+               name += "_" + audio_stream()->identifier ();
+       }
+       p /= name;
+       return p;
+ }
diff --combined src/lib/ffmpeg_content.h
index 935d9560de8feaa3bcc6b80cb94d9c2c6fe28cfb,6ab95d2fe9d2bf53a08acd050845721ed40fc1a1..e4c4a8a52de35ef3cd2caf47f4a1c8a5c75829b3
@@@ -87,7 -87,7 +87,7 @@@ public
        int frame_rate;
        int channels;
        AudioMapping mapping;
 -      boost::optional<double> first_audio;
 +      boost::optional<ContentTime> first_audio;
  
  private:
        friend class ffmpeg_pts_offset_test;
@@@ -127,7 -127,7 +127,7 @@@ class FFmpegContent : public VideoConte
  {
  public:
        FFmpegContent (boost::shared_ptr<const Film>, boost::filesystem::path);
-       FFmpegContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int version);
+       FFmpegContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int version, std::list<std::string> &);
        FFmpegContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
  
        boost::shared_ptr<FFmpegContent> shared_from_this () {
        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;
 +      ContentTime audio_length () const;
        int content_audio_frame_rate () const;
        int output_audio_frame_rate () const;
        AudioMapping audio_mapping () const;
        void set_audio_mapping (AudioMapping);
+       boost::filesystem::path audio_analysis_path () const;
  
        void set_filters (std::vector<Filter const *> const &);
        
        void set_subtitle_stream (boost::shared_ptr<FFmpegSubtitleStream>);
        void set_audio_stream (boost::shared_ptr<FFmpegAudioStream>);
  
 -      boost::optional<double> first_video () const {
 +      boost::optional<ContentTime> first_video () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _first_video;
        }
@@@ -193,7 -194,7 +194,7 @@@ private
        boost::shared_ptr<FFmpegSubtitleStream> _subtitle_stream;
        std::vector<boost::shared_ptr<FFmpegAudioStream> > _audio_streams;
        boost::shared_ptr<FFmpegAudioStream> _audio_stream;
 -      boost::optional<double> _first_video;
 +      boost::optional<ContentTime> _first_video;
        /** Video filters that should be used when generating DCPs */
        std::vector<Filter const *> _filters;
  };
index d37399eb3e00d9b5d09dff228d002c30b89e2038,851c64606b705af1974c1c953fa1d14863c6af89..32b00a1d640e321a1d464cd151ddacc7ce6ed636
@@@ -33,6 -33,7 +33,6 @@@ extern "C" 
  #include <libavcodec/avcodec.h>
  #include <libavformat/avformat.h>
  }
 -#include "film.h"
  #include "filter.h"
  #include "exceptions.h"
  #include "image.h"
@@@ -55,19 -56,20 +55,19 @@@ using std::pair
  using boost::shared_ptr;
  using boost::optional;
  using boost::dynamic_pointer_cast;
 -using libdcp::Size;
 +using dcp::Size;
  
 -FFmpegDecoder::FFmpegDecoder (shared_ptr<const Film> f, shared_ptr<const FFmpegContent> c, bool video, bool audio)
 -      : Decoder (f)
 -      , VideoDecoder (f, c)
 -      , AudioDecoder (f, c)
 -      , SubtitleDecoder (f)
 +FFmpegDecoder::FFmpegDecoder (shared_ptr<const FFmpegContent> c, shared_ptr<Log> log, bool video, bool audio, bool subtitles)
 +      : VideoDecoder (c)
 +      , AudioDecoder (c)
        , FFmpeg (c)
 +      , _log (log)
        , _subtitle_codec_context (0)
        , _subtitle_codec (0)
        , _decode_video (video)
        , _decode_audio (audio)
 +      , _decode_subtitles (subtitles)
        , _pts_offset (0)
 -      , _just_sought (false)
  {
        setup_subtitle ();
  
           Then we remove big initial gaps in PTS and we allow our
           insertion of black frames to work.
  
 -         We will do:
 -           audio_pts_to_use = audio_pts_from_ffmpeg + pts_offset;
 -           video_pts_to_use = video_pts_from_ffmpeg + pts_offset;
 +         We will do pts_to_use = pts_from_ffmpeg + pts_offset;
        */
  
        bool const have_video = video && c->first_video();
 -      bool const have_audio = audio && c->audio_stream() && c->audio_stream()->first_audio;
 +      bool const have_audio = _decode_audio && c->audio_stream () && c->audio_stream()->first_audio;
  
        /* First, make one of them start at 0 */
  
  
        /* Now adjust both so that the video pts starts on a frame */
        if (have_video && have_audio) {
 -              double first_video = c->first_video().get() + _pts_offset;
 -              double const old_first_video = first_video;
 -              
 -              /* Round the first video up to a frame boundary */
 -              if (fabs (rint (first_video * c->video_frame_rate()) - first_video * c->video_frame_rate()) > 1e-6) {
 -                      first_video = ceil (first_video * c->video_frame_rate()) / c->video_frame_rate ();
 -              }
 -
 -              _pts_offset += first_video - old_first_video;
 +              ContentTime first_video = c->first_video().get() + _pts_offset;
 +              ContentTime const old_first_video = first_video;
 +              _pts_offset += first_video.round_up (c->video_frame_rate ()) - old_first_video;
        }
  }
  
@@@ -129,11 -139,14 +129,11 @@@ FFmpegDecoder::flush (
        
        if (_ffmpeg_content->audio_stream() && _decode_audio) {
                decode_audio_packet ();
 +              AudioDecoder::flush ();
        }
 -
 -      /* Stop us being asked for any more data */
 -      _video_position = _ffmpeg_content->video_length ();
 -      _audio_position = _ffmpeg_content->audio_length ();
  }
  
 -void
 +bool
  FFmpegDecoder::pass ()
  {
        int r = av_read_frame (_format_context, &_packet);
                        /* Maybe we should fail here, but for now we'll just finish off instead */
                        char buf[256];
                        av_strerror (r, buf, sizeof(buf));
 -                      shared_ptr<const Film> film = _film.lock ();
 -                      assert (film);
 -                      film->log()->log (String::compose (N_("error on av_read_frame (%1) (%2)"), buf, r));
 +                      _log->log (String::compose (N_("error on av_read_frame (%1) (%2)"), buf, r));
                }
  
                flush ();
 -              return;
 +              return true;
        }
  
 -      shared_ptr<const Film> film = _film.lock ();
 -      assert (film);
 -
        int const si = _packet.stream_index;
        
        if (si == _video_stream && _decode_video) {
                decode_video_packet ();
        } else if (_ffmpeg_content->audio_stream() && _ffmpeg_content->audio_stream()->uses_index (_format_context, si) && _decode_audio) {
                decode_audio_packet ();
 -      } else if (_ffmpeg_content->subtitle_stream() && _ffmpeg_content->subtitle_stream()->uses_index (_format_context, si) && film->with_subtitles ()) {
 +      } else if (_ffmpeg_content->subtitle_stream() && _ffmpeg_content->subtitle_stream()->uses_index (_format_context, si) && _decode_subtitles) {
                decode_subtitle_packet ();
        }
  
        av_free_packet (&_packet);
 +      return false;
  }
  
  /** @param data pointer to array of pointers to buffers.
@@@ -293,133 -310,77 +293,133 @@@ FFmpegDecoder::bytes_per_audio_sample (
        return av_get_bytes_per_sample (audio_sample_format ());
  }
  
 -void
 -FFmpegDecoder::seek (VideoContent::Frame frame, bool accurate)
 +int
 +FFmpegDecoder::minimal_run (boost::function<bool (optional<ContentTime>, optional<ContentTime>, int)> finished)
  {
 -      double const time_base = av_q2d (_format_context->streams[_video_stream]->time_base);
 +      int frames_read = 0;
 +      optional<ContentTime> last_video;
 +      optional<ContentTime> last_audio;
  
 -      /* If we are doing an accurate seek, our initial shot will be 5 frames (5 being
 -         a number plucked from the air) earlier than we want to end up.  The loop below
 -         will hopefully then step through to where we want to be.
 -      */
 -      int initial = frame;
 +      while (!finished (last_video, last_audio, frames_read)) {
 +              int r = av_read_frame (_format_context, &_packet);
 +              if (r < 0) {
 +                      /* We should flush our decoders here, possibly yielding a few more frames,
 +                         but the consequence of having to do that is too hideous to contemplate.
 +                         Instead we give up and say that you can't seek too close to the end
 +                         of a file.
 +                      */
 +                      return frames_read;
 +              }
 +
 +              ++frames_read;
 +
 +              double const time_base = av_q2d (_format_context->streams[_packet.stream_index]->time_base);
  
 -      if (accurate) {
 -              initial -= 5;
 +              if (_packet.stream_index == _video_stream) {
 +
 +                      avcodec_get_frame_defaults (_frame);
 +                      
 +                      int finished = 0;
 +                      r = avcodec_decode_video2 (video_codec_context(), _frame, &finished, &_packet);
 +                      if (r >= 0 && finished) {
 +                              last_video = ContentTime::from_seconds (av_frame_get_best_effort_timestamp (_frame) * time_base) + _pts_offset;
 +                      }
 +
 +              } else if (_ffmpeg_content->audio_stream() && _ffmpeg_content->audio_stream()->uses_index (_format_context, _packet.stream_index)) {
 +                      AVPacket copy_packet = _packet;
 +                      while (copy_packet.size > 0) {
 +
 +                              int finished;
 +                              r = avcodec_decode_audio4 (audio_codec_context(), _frame, &finished, &_packet);
 +                              if (r >= 0 && finished) {
 +                                      last_audio = ContentTime::from_seconds (av_frame_get_best_effort_timestamp (_frame) * time_base) + _pts_offset;
 +                              }
 +                                      
 +                              copy_packet.data += r;
 +                              copy_packet.size -= r;
 +                      }
 +              }
 +              
 +              av_free_packet (&_packet);
        }
  
 -      if (initial < 0) {
 -              initial = 0;
 +      return frames_read;
 +}
 +
 +bool
 +FFmpegDecoder::seek_overrun_finished (ContentTime seek, optional<ContentTime> last_video, optional<ContentTime> last_audio) const
 +{
 +      return (last_video && last_video.get() >= seek) || (last_audio && last_audio.get() >= seek);
 +}
 +
 +bool
 +FFmpegDecoder::seek_final_finished (int n, int done) const
 +{
 +      return n == done;
 +}
 +
 +void
 +FFmpegDecoder::seek_and_flush (ContentTime t)
 +{
 +      ContentTime const u = t - _pts_offset;
 +      int64_t s = u.seconds() / av_q2d (_format_context->streams[_video_stream]->time_base);
 +
 +      if (_ffmpeg_content->audio_stream ()) {
 +              s = min (
 +                      s, int64_t (u.seconds() / av_q2d (_ffmpeg_content->audio_stream()->stream(_format_context)->time_base))
 +                      );
        }
  
 -      /* Initial seek time in the stream's timebase */
 -      int64_t const initial_vt = ((initial / _ffmpeg_content->video_frame_rate()) - _pts_offset) / time_base;
 +      /* Ridiculous empirical hack */
 +      s--;
 +      if (s < 0) {
 +              s = 0;
 +      }
  
 -      av_seek_frame (_format_context, _video_stream, initial_vt, AVSEEK_FLAG_BACKWARD);
 +      av_seek_frame (_format_context, _video_stream, s, 0);
  
        avcodec_flush_buffers (video_codec_context());
 +      if (audio_codec_context ()) {
 +              avcodec_flush_buffers (audio_codec_context ());
 +      }
        if (_subtitle_codec_context) {
                avcodec_flush_buffers (_subtitle_codec_context);
        }
 +}
  
 -      /* This !accurate is piling hack upon hack; setting _just_sought to true
 -         even with accurate == true defeats our attempt to align the start
 -         of the video and audio.  Here we disable that defeat when accurate == true
 -         i.e. when we are making a DCP rather than just previewing one.
 -         Ewww.  This should be gone in 2.0.
 -      */
 -      if (!accurate) {
 -              _just_sought = true;
 +void
 +FFmpegDecoder::seek (ContentTime time, bool accurate)
 +{
 +      Decoder::seek (time, accurate);
 +      if (_decode_audio) {
 +              AudioDecoder::seek (time, accurate);
        }
        
 -      _video_position = frame;
 -      
 -      if (frame == 0 || !accurate) {
 -              /* We're already there, or we're as close as we need to be */
 -              return;
 +      /* If we are doing an accurate seek, our initial shot will be 200ms (200 being
 +         a number plucked from the air) earlier than we want to end up.  The loop below
 +         will hopefully then step through to where we want to be.
 +      */
 +
 +      ContentTime pre_roll = accurate ? ContentTime::from_seconds (0.2) : ContentTime (0);
 +      ContentTime initial_seek = time - pre_roll;
 +      if (initial_seek < ContentTime (0)) {
 +              initial_seek = ContentTime (0);
        }
  
 -      while (1) {
 -              int r = av_read_frame (_format_context, &_packet);
 -              if (r < 0) {
 -                      return;
 -              }
 +      /* Initial seek time in the video stream's timebase */
  
 -              if (_packet.stream_index != _video_stream) {
 -                      av_free_packet (&_packet);
 -                      continue;
 -              }
 -              
 -              int finished = 0;
 -              r = avcodec_decode_video2 (video_codec_context(), _frame, &finished, &_packet);
 -              if (r >= 0 && finished) {
 -                      _video_position = rint (
 -                              (av_frame_get_best_effort_timestamp (_frame) * time_base + _pts_offset) * _ffmpeg_content->video_frame_rate()
 -                              );
 +      seek_and_flush (initial_seek);
  
 -                      if (_video_position >= (frame - 1)) {
 -                              av_free_packet (&_packet);
 -                              break;
 -                      }
 -              }
 -              
 -              av_free_packet (&_packet);
 +      if (!accurate) {
 +              /* That'll do */
 +              return;
 +      }
 +
 +      int const N = minimal_run (boost::bind (&FFmpegDecoder::seek_overrun_finished, this, time, _1, _2));
 +
 +      seek_and_flush (initial_seek);
 +      if (N > 0) {
 +              minimal_run (boost::bind (&FFmpegDecoder::seek_final_finished, this, N - 1, _3));
        }
  }
  
@@@ -436,23 -397,39 +436,23 @@@ FFmpegDecoder::decode_audio_packet (
  
                int frame_finished;
                int const decode_result = avcodec_decode_audio4 (audio_codec_context(), _frame, &frame_finished, &copy_packet);
 +
                if (decode_result < 0) {
 -                      shared_ptr<const Film> film = _film.lock ();
 -                      assert (film);
 -                      film->log()->log (String::compose ("avcodec_decode_audio4 failed (%1)", decode_result));
 +                      _log->log (String::compose ("avcodec_decode_audio4 failed (%1)", decode_result));
                        return;
                }
  
                if (frame_finished) {
 -                      
 -                      if (_audio_position == 0) {
 -                              /* Where we are in the source, in seconds */
 -                              double const pts = av_q2d (_format_context->streams[copy_packet.stream_index]->time_base)
 -                                      * av_frame_get_best_effort_timestamp(_frame) + _pts_offset;
 -
 -                              if (pts > 0) {
 -                                      /* Emit some silence */
 -                                      shared_ptr<AudioBuffers> silence (
 -                                              new AudioBuffers (
 -                                                      _ffmpeg_content->audio_channels(),
 -                                                      pts * _ffmpeg_content->content_audio_frame_rate()
 -                                                      )
 -                                              );
 -                                      
 -                                      silence->make_silent ();
 -                                      audio (silence, _audio_position);
 -                              }
 -                      }
 +                      ContentTime const ct = ContentTime::from_seconds (
 +                              av_frame_get_best_effort_timestamp (_frame) *
 +                              av_q2d (_ffmpeg_content->audio_stream()->stream (_format_context)->time_base))
 +                              + _pts_offset;
                        
                        int const data_size = av_samples_get_buffer_size (
                                0, audio_codec_context()->channels, _frame->nb_samples, audio_sample_format (), 1
                                );
 -                      
 -                      audio (deinterleave_audio (_frame->data, data_size), _audio_position);
 +
 +                      audio (deinterleave_audio (_frame->data, data_size), ct);
                }
                        
                copy_packet.data += decode_result;
@@@ -473,33 -450,72 +473,28 @@@ FFmpegDecoder::decode_video_packet (
        shared_ptr<FilterGraph> graph;
        
        list<shared_ptr<FilterGraph> >::iterator i = _filter_graphs.begin();
 -      while (i != _filter_graphs.end() && !(*i)->can_process (libdcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format)) {
 +      while (i != _filter_graphs.end() && !(*i)->can_process (dcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format)) {
                ++i;
        }
  
        if (i == _filter_graphs.end ()) {
 -              shared_ptr<const Film> film = _film.lock ();
 -              assert (film);
 -
 -              graph.reset (new FilterGraph (_ffmpeg_content, libdcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format));
 +              graph.reset (new FilterGraph (_ffmpeg_content, dcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format));
                _filter_graphs.push_back (graph);
 -
 -              film->log()->log (String::compose (N_("New graph for %1x%2, pixel format %3"), _frame->width, _frame->height, _frame->format));
 +              _log->log (String::compose (N_("New graph for %1x%2, pixel format %3"), _frame->width, _frame->height, _frame->format));
        } else {
                graph = *i;
        }
  
        list<pair<shared_ptr<Image>, int64_t> > images = graph->process (_frame);
  
-       string post_process = Filter::ffmpeg_strings (_ffmpeg_content->filters()).second;
-       
        for (list<pair<shared_ptr<Image>, int64_t> >::iterator i = images.begin(); i != images.end(); ++i) {
  
                shared_ptr<Image> image = i->first;
-               if (!post_process.empty ()) {
-                       image = image->post_process (post_process, true);
-               }
                
                if (i->second != AV_NOPTS_VALUE) {
 -
 -                      double const pts = i->second * av_q2d (_format_context->streams[_video_stream]->time_base) + _pts_offset;
 -
 -                      if (_just_sought) {
 -                              /* We just did a seek, so disable any attempts to correct for where we
 -                                 are / should be.
 -                              */
 -                              _video_position = rint (pts * _ffmpeg_content->video_frame_rate ());
 -                              _just_sought = false;
 -                      }
 -
 -                      double const next = _video_position / _ffmpeg_content->video_frame_rate();
 -                      double const one_frame = 1 / _ffmpeg_content->video_frame_rate ();
 -                      double delta = pts - next;
 -
 -                      while (delta > one_frame) {
 -                              /* This PTS is more than one frame forward in time of where we think we should be; emit
 -                                 a black frame.
 -                              */
 -
 -                              /* XXX: I think this should be a copy of the last frame... */
 -                              boost::shared_ptr<Image> black (
 -                                      new Image (
 -                                              static_cast<AVPixelFormat> (_frame->format),
 -                                              libdcp::Size (video_codec_context()->width, video_codec_context()->height),
 -                                              true
 -                                              )
 -                                      );
 -                              
 -                              black->make_black ();
 -                              video (image, false, _video_position);
 -                              delta -= one_frame;
 -                      }
 -
 -                      if (delta > -one_frame) {
 -                              /* This PTS is within a frame of being right; emit this (otherwise it will be dropped) */
 -                              video (image, false, _video_position);
 -                      }
 -                              
 +                      video (image, false, ContentTime::from_seconds (i->second * av_q2d (_format_context->streams[_video_stream]->time_base)) + _pts_offset);
                } else {
 -                      shared_ptr<const Film> film = _film.lock ();
 -                      assert (film);
 -                      film->log()->log ("Dropping frame without PTS");
 +                      _log->log ("Dropping frame without PTS");
                }
        }
  
@@@ -532,6 -548,14 +527,6 @@@ FFmpegDecoder::setup_subtitle (
        }
  }
  
 -bool
 -FFmpegDecoder::done () const
 -{
 -      bool const vd = !_decode_video || (_video_position >= _ffmpeg_content->video_length());
 -      bool const ad = !_decode_audio || !_ffmpeg_content->audio_stream() || (_audio_position >= _ffmpeg_content->audio_length());
 -      return vd && ad;
 -}
 -      
  void
  FFmpegDecoder::decode_subtitle_packet ()
  {
           indicate that the previous subtitle should stop.
        */
        if (sub.num_rects <= 0) {
 -              subtitle (shared_ptr<Image> (), dcpomatic::Rect<double> (), 0, 0);
 +              image_subtitle (shared_ptr<Image> (), dcpomatic::Rect<double> (), ContentTime (), ContentTime ());
                return;
        } else if (sub.num_rects > 1) {
                throw DecodeError (_("multi-part subtitles not yet supported"));
        }
                
 -      /* Subtitle PTS in seconds (within the source, not taking into account any of the
 +      /* Subtitle PTS (within the source, not taking into account any of the
           source that we may have chopped off for the DCP)
        */
 -      double const packet_time = (static_cast<double> (sub.pts ) / AV_TIME_BASE) + _pts_offset;
 -
 +      ContentTime packet_time = ContentTime::from_seconds (static_cast<double> (sub.pts) / AV_TIME_BASE) + _pts_offset;
 +      
        /* hence start time for this sub */
 -      Time const from = (packet_time + (double (sub.start_display_time) / 1e3)) * TIME_HZ;
 -      Time const to = (packet_time + (double (sub.end_display_time) / 1e3)) * TIME_HZ;
 +      ContentTime const from = packet_time + ContentTime::from_seconds (sub.start_display_time / 1e3);
 +      ContentTime const to = packet_time + ContentTime::from_seconds (sub.end_display_time / 1e3);
  
        AVSubtitleRect const * rect = sub.rects[0];
  
        /* Note RGBA is expressed little-endian, so the first byte in the word is R, second
           G, third B, fourth A.
        */
 -      shared_ptr<Image> image (new Image (PIX_FMT_RGBA, libdcp::Size (rect->w, rect->h), true));
 +      shared_ptr<Image> image (new Image (PIX_FMT_RGBA, dcp::Size (rect->w, rect->h), true));
  
        /* Start of the first line in the subtitle */
        uint8_t* sub_p = rect->pict.data[0];
                out_p += image->stride()[0] / sizeof (uint32_t);
        }
  
 -      libdcp::Size const vs = _ffmpeg_content->video_size ();
 +      dcp::Size const vs = _ffmpeg_content->video_size ();
  
 -      subtitle (
 +      image_subtitle (
                image,
                dcpomatic::Rect<double> (
                        static_cast<double> (rect->x) / vs.width,
diff --combined src/lib/film.cc
index 21e7383bf94fcfbfd0635c7d1c0efe3c90ee2741,04692fc1ee5a7e2e2f7280d6fe9bd372a043e65a..cc80f5bc2d8be73bf40f60b99a136a963f0f582e
  #include <boost/date_time.hpp>
  #include <libxml++/libxml++.h>
  #include <libcxml/cxml.h>
 -#include <libdcp/signer_chain.h>
 -#include <libdcp/cpl.h>
 -#include <libdcp/signer.h>
 -#include <libdcp/util.h>
 -#include <libdcp/kdm.h>
 +#include <dcp/signer_chain.h>
 +#include <dcp/cpl.h>
 +#include <dcp/signer.h>
 +#include <dcp/util.h>
 +#include <dcp/kdm.h>
  #include "film.h"
  #include "job.h"
  #include "util.h"
@@@ -78,8 -78,8 +78,8 @@@ using boost::to_upper_copy
  using boost::ends_with;
  using boost::starts_with;
  using boost::optional;
 -using libdcp::Size;
 -using libdcp::Signer;
 +using dcp::Size;
 +using dcp::Signer;
  
  /* 5 -> 6
   * AudioMapping XML changed.
@@@ -232,11 -232,9 +232,9 @@@ Film::filename_safe_name () cons
  }
  
  boost::filesystem::path
- Film::audio_analysis_path (shared_ptr<const AudioContent> c) const
+ Film::audio_analysis_dir () const
  {
-       boost::filesystem::path p = dir ("analysis");
-       p /= c->digest();
-       return p;
+       return dir ("analysis");
  }
  
  /** Add suitable Jobs to the JobManager to create a DCP for this Film */
@@@ -383,8 -381,10 +381,10 @@@ Film::write_metadata () cons
        _dirty = false;
  }
  
- /** Read state from our metadata file */
- void
+ /** Read state from our metadata file.
+  *  @return Notes about things that the user should know about, or empty.
+  */
+ list<string>
  Film::read_metadata ()
  {
        LocaleGuard lg;
        _sequence_video = f.bool_child ("SequenceVideo");
        _three_d = f.bool_child ("ThreeD");
        _interop = f.bool_child ("Interop");
 -      _key = libdcp::Key (f.string_child ("Key"));
 +      _key = dcp::Key (f.string_child ("Key"));
-       _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"), _state_version);
+       list<string> notes;
+       /* This method is the only one that can return notes (so far) */
+       _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"), _state_version, notes);
  
        _dirty = false;
+       return notes;
  }
  
  /** Given a directory name, return its full path within the Film's directory.
@@@ -757,7 -761,7 +761,7 @@@ Film::j2c_path (int f, Eyes e, bool t) 
        return file (p);
  }
  
 -/** @return List of subdirectories (not full paths) containing DCPs that can be successfully libdcp::DCP::read() */
 +/** @return List of subdirectories (not full paths) containing DCPs that can be successfully dcp::DCP::read() */
  list<boost::filesystem::path>
  Film::dcps () const
  {
                        ) {
  
                        try {
 -                              libdcp::DCP dcp (*i);
 +                              dcp::DCP dcp (*i);
                                dcp.read ();
                                out.push_back (i->path().leaf ());
                        } catch (...) {
@@@ -866,7 -870,7 +870,7 @@@ Film::move_content_later (shared_ptr<Co
        _playlist->move_later (c);
  }
  
 -Time
 +DCPTime
  Film::length () const
  {
        return _playlist->length ();
@@@ -878,18 -882,12 +882,18 @@@ Film::has_subtitles () cons
        return _playlist->has_subtitles ();
  }
  
 -OutputVideoFrame
 +int
  Film::best_video_frame_rate () const
  {
        return _playlist->best_dcp_frame_rate ();
  }
  
 +FrameRateChange
 +Film::active_frame_rate_change (DCPTime t) const
 +{
 +      return _playlist->active_frame_rate_change (t, video_frame_rate ());
 +}
 +
  void
  Film::playlist_content_changed (boost::weak_ptr<Content> c, int p)
  {
@@@ -908,7 -906,31 +912,7 @@@ Film::playlist_changed (
        signal_changed (CONTENT);
  }     
  
 -OutputAudioFrame
 -Film::time_to_audio_frames (Time t) const
 -{
 -      return divide_with_round (t * audio_frame_rate (), TIME_HZ);
 -}
 -
 -OutputVideoFrame
 -Film::time_to_video_frames (Time t) const
 -{
 -      return divide_with_round (t * video_frame_rate (), TIME_HZ);
 -}
 -
 -Time
 -Film::audio_frames_to_time (OutputAudioFrame f) const
 -{
 -      return divide_with_round (f * TIME_HZ, audio_frame_rate ());
 -}
 -
 -Time
 -Film::video_frames_to_time (OutputVideoFrame f) const
 -{
 -      return divide_with_round (f * TIME_HZ, video_frame_rate ());
 -}
 -
 -OutputAudioFrame
 +int
  Film::audio_frame_rate () const
  {
        /* XXX */
@@@ -923,23 -945,31 +927,31 @@@ Film::set_sequence_video (bool s
        signal_changed (SEQUENCE_VIDEO);
  }
  
 -libdcp::Size
+ /** @return Size of the largest possible image in whatever resolution we are using */
 +dcp::Size
  Film::full_frame () const
  {
        switch (_resolution) {
        case RESOLUTION_2K:
 -              return libdcp::Size (2048, 1080);
 +              return dcp::Size (2048, 1080);
        case RESOLUTION_4K:
 -              return libdcp::Size (4096, 2160);
 +              return dcp::Size (4096, 2160);
        }
  
        assert (false);
 -      return libdcp::Size ();
 +      return dcp::Size ();
  }
  
 -libdcp::Size
+ /** @return Size of the frame */
 -libdcp::KDM
++dcp::Size
+ Film::frame_size () const
+ {
+       return fit_ratio_within (container()->ratio(), full_frame ());
+ }
 +dcp::KDM
  Film::make_kdm (
 -      shared_ptr<libdcp::Certificate> target,
 +      shared_ptr<dcp::Certificate> target,
        boost::filesystem::path dcp_dir,
        boost::posix_time::ptime from,
        boost::posix_time::ptime until
  {
        shared_ptr<const Signer> signer = make_signer ();
  
 -      libdcp::DCP dcp (dir (dcp_dir.string ()));
 +      dcp::DCP dcp (dir (dcp_dir.string ()));
        
        try {
                dcp.read ();
        
        time_t now = time (0);
        struct tm* tm = localtime (&now);
 -      string const issue_date = libdcp::tm_to_string (tm);
 +      string const issue_date = dcp::tm_to_string (tm);
        
        dcp.cpls().front()->set_mxf_keys (key ());
        
 -      return libdcp::KDM (dcp.cpls().front(), signer, target, from, until, "DCP-o-matic", issue_date);
 +      return dcp::KDM (dcp.cpls().front(), signer, target, from, until, "DCP-o-matic", issue_date);
  }
  
 -list<libdcp::KDM>
 +list<dcp::KDM>
  Film::make_kdms (
        list<shared_ptr<Screen> > screens,
        boost::filesystem::path dcp,
        boost::posix_time::ptime until
        ) const
  {
 -      list<libdcp::KDM> kdms;
 +      list<dcp::KDM> kdms;
  
        for (list<shared_ptr<Screen> >::iterator i = screens.begin(); i != screens.end(); ++i) {
                kdms.push_back (make_kdm ((*i)->certificate, dcp, from, until));
  uint64_t
  Film::required_disk_space () const
  {
 -      return uint64_t (j2k_bandwidth() / 8) * length() / TIME_HZ;
 +      return uint64_t (j2k_bandwidth() / 8) * length().seconds();
  }
  
  /** This method checks the disk that the Film is on and tries to decide whether or not
diff --combined src/lib/film.h
index f776a3f72da22a440b8360128405f6384f523cab,162b67b351bd6b9d0e2a97a6dc60e5b1e49387e1..9d1445d927b81ab7333f3197d2891ef1306de809
@@@ -31,8 -31,8 +31,8 @@@
  #include <boost/signals2.hpp>
  #include <boost/enable_shared_from_this.hpp>
  #include <boost/filesystem.hpp>
 -#include <libdcp/key.h>
 -#include <libdcp/kdm.h>
 +#include <dcp/key.h>
 +#include <dcp/kdm.h>
  #include "util.h"
  #include "types.h"
  #include "dci_metadata.h"
@@@ -63,7 -63,7 +63,7 @@@ public
        boost::filesystem::path info_path (int, Eyes) const;
        boost::filesystem::path internal_video_mxf_dir () const;
        boost::filesystem::path internal_video_mxf_filename () const;
-       boost::filesystem::path audio_analysis_path (boost::shared_ptr<const AudioContent>) const;
+       boost::filesystem::path audio_analysis_dir () const;
  
        boost::filesystem::path video_mxf_filename () const;
        boost::filesystem::path audio_mxf_filename () const;
@@@ -83,7 -83,7 +83,7 @@@
        boost::filesystem::path file (boost::filesystem::path f) const;
        boost::filesystem::path dir (boost::filesystem::path d) const;
  
-       void read_metadata ();
+       std::list<std::string> read_metadata ();
        void write_metadata () const;
        boost::shared_ptr<xmlpp::Document> metadata () const;
  
                return _dirty;
        }
  
 -      libdcp::Size full_frame () const;
 -      libdcp::Size frame_size () const;
 +      dcp::Size full_frame () const;
++      dcp::Size frame_size () const;
  
        std::list<boost::filesystem::path> dcps () const;
  
        boost::shared_ptr<Player> make_player () const;
        boost::shared_ptr<Playlist> playlist () const;
  
 -      OutputAudioFrame audio_frame_rate () const;
 -
 -      OutputAudioFrame time_to_audio_frames (Time) const;
 -      OutputVideoFrame time_to_video_frames (Time) const;
 -      Time video_frames_to_time (OutputVideoFrame) const;
 -      Time audio_frames_to_time (OutputAudioFrame) const;
 +      int audio_frame_rate () const;
  
        uint64_t required_disk_space () const;
        bool should_be_enough_disk_space (double &, double &) const;
        /* Proxies for some Playlist methods */
  
        ContentList content () const;
 -      Time length () const;
 +      DCPTime length () const;
        bool has_subtitles () const;
 -      OutputVideoFrame best_video_frame_rate () const;
 +      int best_video_frame_rate () const;
 +      FrameRateChange active_frame_rate_change (DCPTime) const;
  
 -      libdcp::KDM
 +      dcp::KDM
        make_kdm (
 -              boost::shared_ptr<libdcp::Certificate> target,
 +              boost::shared_ptr<dcp::Certificate> target,
                boost::filesystem::path dcp,
                boost::posix_time::ptime from,
                boost::posix_time::ptime until
                ) const;
        
 -      std::list<libdcp::KDM> make_kdms (
 +      std::list<dcp::KDM> make_kdms (
                std::list<boost::shared_ptr<Screen> >,
                boost::filesystem::path dcp,
                boost::posix_time::ptime from,
                boost::posix_time::ptime until
                ) const;
  
 -      libdcp::Key key () const {
 +      dcp::Key key () const {
                return _key;
        }
  
@@@ -323,7 -328,7 +324,7 @@@ private
        bool _three_d;
        bool _sequence_video;
        bool _interop;
 -      libdcp::Key _key;
 +      dcp::Key _key;
  
        int _state_version;
  
diff --combined src/lib/filter_graph.cc
index 48d94e17570c2c6306ba975574c42a1e66f64e7f,a36a41f43c651abe8073a20f278411e95e848073..5add16d19bcedac612b55eecf5bc85a7feedf99a
@@@ -45,14 -45,14 +45,14 @@@ using std::make_pair
  using std::cout;
  using boost::shared_ptr;
  using boost::weak_ptr;
 -using libdcp::Size;
 +using dcp::Size;
  
  /** Construct a FilterGraph for the settings in a piece of content.
   *  @param content Content.
   *  @param s Size of the images to process.
   *  @param p Pixel format of the images to process.
   */
 -FilterGraph::FilterGraph (shared_ptr<const FFmpegContent> content, libdcp::Size s, AVPixelFormat p)
 +FilterGraph::FilterGraph (shared_ptr<const FFmpegContent> content, dcp::Size s, AVPixelFormat p)
        : _buffer_src_context (0)
        , _buffer_sink_context (0)
        , _size (s)
@@@ -60,7 -60,7 +60,7 @@@
  {
        _frame = av_frame_alloc ();
        
-       string filters = Filter::ffmpeg_strings (content->filters()).first;
+       string filters = Filter::ffmpeg_string (content->filters());
        if (filters.empty ()) {
                filters = "copy";
        }
                throw DecodeError (N_("could not configure filter graph."));
        }
  
 -      /* XXX: leaking `inputs' / `outputs' ? */
 +      avfilter_inout_free (&inputs);
 +      avfilter_inout_free (&outputs);
  }
  
  FilterGraph::~FilterGraph ()
@@@ -160,7 -159,7 +160,7 @@@ FilterGraph::process (AVFrame* frame
   *  @return true if this chain can process images with `s' and `p', otherwise false.
   */
  bool
 -FilterGraph::can_process (libdcp::Size s, AVPixelFormat p) const
 +FilterGraph::can_process (dcp::Size s, AVPixelFormat p) const
  {
        return (_size == s && _pixel_format == p);
  }
diff --combined src/lib/image.cc
index 98645c2996a4219f02681d276090b737df66df4e,25d1ef2760b0e9faa82ace72b81d006ae1926d6b..c3b1ca77afded0cc70270fb097d40a6b989a2bfa
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
 +    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
@@@ -31,7 -31,6 +31,7 @@@ extern "C" 
  #include "image.h"
  #include "exceptions.h"
  #include "scaler.h"
 +#include "timer.h"
  
  #include "i18n.h"
  
@@@ -40,7 -39,7 +40,7 @@@ using std::min
  using std::cout;
  using std::cerr;
  using boost::shared_ptr;
 -using libdcp::Size;
 +using dcp::Size;
  
  int
  Image::line_factor (int n) const
@@@ -84,7 -83,7 +84,7 @@@ Image::components () cons
  
  /** Crop this image, scale it to `inter_size' and then place it in a black frame of `out_size' */
  shared_ptr<Image>
 -Image::crop_scale_window (Crop crop, libdcp::Size inter_size, libdcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const
 +Image::crop_scale_window (Crop crop, dcp::Size inter_size, dcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const
  {
        assert (scaler);
        /* Empirical testing suggests that sws_scale() will crash if
        out->make_black ();
  
        /* Size of the image after any crop */
 -      libdcp::Size const cropped_size = crop.apply (size ());
 +      dcp::Size const cropped_size = crop.apply (size ());
  
        /* Scale context for a scale from cropped_size to inter_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
                );
  
        if (!scale_context) {
  }
  
  shared_ptr<Image>
 -Image::scale (libdcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const
 +Image::scale (dcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const
  {
        assert (scaler);
        /* Empirical testing suggests that sws_scale() will crash if
        return scaled;
  }
  
- /** Run a FFmpeg post-process on this image and return the processed version.
-  *  @param pp Flags for the required set of post processes.
-  *  @return Post-processed image.
-  */
- shared_ptr<Image>
- Image::post_process (string pp, bool aligned) const
- {
-       shared_ptr<Image> out (new Image (pixel_format(), size (), aligned));
-       int pp_format = 0;
-       switch (pixel_format()) {
-       case PIX_FMT_YUV420P:
-               pp_format = PP_FORMAT_420;
-               break;
-       case PIX_FMT_YUV422P10LE:
-       case PIX_FMT_YUV422P:
-       case PIX_FMT_UYVY422:
-               pp_format = PP_FORMAT_422;
-               break;
-       case PIX_FMT_YUV444P:
-       case PIX_FMT_YUV444P9BE:
-       case PIX_FMT_YUV444P9LE:
-       case PIX_FMT_YUV444P10BE:
-       case PIX_FMT_YUV444P10LE:
-               pp_format = PP_FORMAT_444;
-       default:
-               throw PixelFormatError ("post_process", pixel_format());
-       }
-               
-       pp_mode* mode = pp_get_mode_by_name_and_quality (pp.c_str (), PP_QUALITY_MAX);
-       pp_context* context = pp_get_context (size().width, size().height, pp_format | PP_CPU_CAPS_MMX2);
-       pp_postprocess (
-               (const uint8_t **) data(), stride(),
-               out->data(), out->stride(),
-               size().width, size().height,
-               0, 0, mode, context, 0
-               );
-               
-       pp_free_mode (mode);
-       pp_free_context (context);
-       return out;
- }
  shared_ptr<Image>
  Image::crop (Crop crop, bool aligned) const
  {
 -      libdcp::Size cropped_size = crop.apply (size ());
 +      dcp::Size cropped_size = crop.apply (size ());
        shared_ptr<Image> out (new Image (pixel_format(), cropped_size, aligned));
  
        for (int c = 0; c < components(); ++c) {
@@@ -391,18 -345,8 +346,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;
                }
        }
  }
@@@ -513,8 -457,8 +468,8 @@@ Image::bytes_per_pixel (int c) cons
   *  @param p Pixel format.
   *  @param s Size in pixels.
   */
 -Image::Image (AVPixelFormat p, libdcp::Size s, bool aligned)
 -      : libdcp::Image (s)
 +Image::Image (AVPixelFormat p, dcp::Size s, bool aligned)
 +      : dcp::Image (s)
        , _pixel_format (p)
        , _aligned (aligned)
  {
@@@ -551,7 -495,7 +506,7 @@@ Image::allocate (
  }
  
  Image::Image (Image const & other)
 -      : libdcp::Image (other)
 +      : dcp::Image (other)
        ,  _pixel_format (other._pixel_format)
        , _aligned (other._aligned)
  {
  }
  
  Image::Image (AVFrame* frame)
 -      : libdcp::Image (libdcp::Size (frame->width, frame->height))
 +      : dcp::Image (dcp::Size (frame->width, frame->height))
        , _pixel_format (static_cast<AVPixelFormat> (frame->format))
        , _aligned (true)
  {
  }
  
  Image::Image (shared_ptr<const Image> other, bool aligned)
 -      : libdcp::Image (other)
 +      : dcp::Image (other)
        , _pixel_format (other->_pixel_format)
        , _aligned (aligned)
  {
@@@ -621,7 -565,7 +576,7 @@@ Image::operator= (Image const & other
  void
  Image::swap (Image & other)
  {
 -      libdcp::Image::swap (other);
 +      dcp::Image::swap (other);
        
        std::swap (_pixel_format, other._pixel_format);
  
@@@ -664,7 -608,7 +619,7 @@@ Image::stride () cons
        return _stride;
  }
  
 -libdcp::Size
 +dcp::Size
  Image::size () const
  {
        return _size;
diff --combined src/lib/image.h
index 3220a23b4adba6b42632edc4df3355898516d180,2d9f322310c2c3e66d5a9d4e05f9c88deffb3b51..5eba11041713f42ecc252648a7789505dbba467b
@@@ -31,16 -31,16 +31,16 @@@ extern "C" 
  #include <libavcodec/avcodec.h>
  #include <libavfilter/avfilter.h>
  }
 -#include <libdcp/image.h>
 +#include <dcp/image.h>
  #include "util.h"
  #include "position.h"
  
  class Scaler;
  
 -class Image : public libdcp::Image
 +class Image : public dcp::Image
  {
  public:
 -      Image (AVPixelFormat, libdcp::Size, bool);
 +      Image (AVPixelFormat, dcp::Size, bool);
        Image (AVFrame *);
        Image (Image const &);
        Image (boost::shared_ptr<const Image>, bool);
        uint8_t ** data () const;
        int * line_size () const;
        int * stride () const;
 -      libdcp::Size size () const;
 +      dcp::Size size () const;
        bool aligned () const;
  
        int components () const;
        int line_factor (int) const;
        int lines (int) const;
  
 -      boost::shared_ptr<Image> scale (libdcp::Size, Scaler const *, AVPixelFormat, bool aligned) const;
 +      boost::shared_ptr<Image> scale (dcp::Size, Scaler const *, AVPixelFormat, bool aligned) const;
-       boost::shared_ptr<Image> post_process (std::string, bool aligned) const;
        boost::shared_ptr<Image> crop (Crop c, bool aligned) const;
  
 -      boost::shared_ptr<Image> crop_scale_window (Crop c, libdcp::Size, libdcp::Size, Scaler const *, AVPixelFormat, bool aligned) const;
 +      boost::shared_ptr<Image> crop_scale_window (Crop c, dcp::Size, dcp::Size, Scaler const *, AVPixelFormat, bool aligned) const;
        
        void make_black ();
        void alpha_blend (boost::shared_ptr<const Image> image, Position<int> pos);
diff --combined src/lib/player.cc
index e89e84aa6488179a1caf20f600a0d7b8e640f061,d05597dd556540f50839f35fd57510fee75e7243..f83e4ed26b7368228db0cad5e476d27c05a1ab7b
  */
  
  #include <stdint.h>
 +#include <algorithm>
  #include "player.h"
  #include "film.h"
  #include "ffmpeg_decoder.h"
 +#include "audio_buffers.h"
  #include "ffmpeg_content.h"
  #include "image_decoder.h"
  #include "image_content.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;
@@@ -49,20 -45,71 +49,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)
        , _have_valid_pieces (false)
        , _video_position (0)
        , _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))
 +      , _audio_merger (f->audio_channels(), f->audio_frame_rate ())
        , _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));
        _film_changed_connection = _film->Changed.connect (bind (&Player::film_changed, this, _1));
-       set_video_container_size (fit_ratio_within (_film->container()->ratio (), _film->full_frame ()));
+       set_video_container_size (_film->frame_size ());
  }
  
  void
@@@ -103,165 -148,111 +103,165 @@@ 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 = DCPTime::max ();
 +      DCPTime earliest_audio = DCPTime::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 ((*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);
 -                      } else {
 -                              earliest->decoder->pass ();
 -                      }
 -              }
 -              break;
 -
 -      case AUDIO:
 -              if (earliest_t > _audio_position) {
 -                      emit_silence (_film->time_to_audio_frames (earliest_t - _audio_position));
 -              } 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 ());
 -                                      }
 -                              }
 -                      }
 +      if (earliest_audio != DCPTime::max ()) {
 +              if (earliest_audio.get() < 0) {
 +                      earliest_audio = DCPTime ();
                }
 -              break;
 +              TimedAudioBuffers tb = _audio_merger.pull (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 += DCPTime::from_frames (tb.audio->frames(), _film->audio_frame_rate ());
        }
  
 -      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;
 +      /* 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;
                        }
  
 -                      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);
 +                      if (i == _pieces.end() || !_last_incoming_video.video || !_have_valid_pieces) {
 +                              /* We're outside all video content */
 +                              emit_black ();
 +                              _statistics.video.black++;
 +                      } else {
 +                              /* 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++;
                        }
 +
 +                      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 {
 +                      /* Too far behind: skip */
 +                      _statistics.video.skip++;
                }
  
 -              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 ());
 +              _just_did_inaccurate_seek = false;
 +
 +      } else if (da && _audio) {
 +
 +              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;
 +      dcp::Size image_size = content->scale().size (content, _video_container_size);
 +      if (_approximate_size) {
 +              image_size.width &= ~3;
 +              image_size.height &= ~3;
        }
  
 -      Time const time = content->position() + relative_time + extra - content->trim_start ();
 -      libdcp::Size const image_size = content->scale().size (content, _video_container_size, _film->frame_size ());
 -
        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 += DCPTime::from_frames (1, _film->video_frame_rate ());
        }
  }
  
  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;
 +              audio->data = gain;
        }
 -      
 -      Time const relative_time = _film->audio_frames_to_time (frame);
  
 -      if (content->trimmed (relative_time)) {
 -              return;
 -      }
 -
 -      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) {
 +                      if (map.get (i, static_cast<dcp::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))
 +                                      static_cast<dcp::Channel> (j),
 +                                      map.get (i, static_cast<dcp::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 += DCPTime::from_seconds (content->audio_delay() / 1000.0);
 +      if (audio->dcp_time < DCPTime (0)) {
 +              int const frames = - audio->dcp_time.frames (_film->audio_frame_rate());
 +              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 = DCPTime ();
        }
  
 -      _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 tb = _audio_merger.flush ();
        if (_audio && tb.audio) {
                Audio (tb.audio, tb.time);
 -              _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
 +              _audio_position += DCPTime::from_frames (tb.audio->frames (), _film->audio_frame_rate ());
        }
  
        while (_video && _video_position < _audio_position) {
        }
  
        while (_audio && _audio_position < _video_position) {
 -              emit_silence (_film->time_to_audio_frames (_video_position - _audio_position));
 +              emit_silence (_video_position - _audio_position);
        }
 -      
  }
  
  /** Seek so that the next pass() will yield (approximately) the requested frame.
   *  @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);
  
                /* 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 = t.round_up (_film->video_frame_rate());
 +      _audio_position = t.round_up (_film->audio_frame_rate());
  
 -      /* XXX: don't seek audio because we don't need to... */
 +      _audio_merger.clear (_audio_position);
 +
 +      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;
 +              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 (fc, _film->log(), _video, _audio, _film->with_subtitles ()));
 +                      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 (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 (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 (rc));
 +                      frc = best_overlap_frc;
                }
  
 -              _pieces.push_back (piece);
 +              ContentTime st ((*i)->trim_start(), frc.get ());
 +              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 = DCPTime ();
 +      _audio_position = DCPTime ();
  }
  
  void
@@@ -560,8 -532,7 +560,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 (
@@@ -586,7 -557,7 +586,7 @@@ Player::playlist_changed (
  }
  
  void
 -Player::set_video_container_size (libdcp::Size s)
 +Player::set_video_container_size (dcp::Size s)
  {
        _video_container_size = s;
  
                );
  }
  
 -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 ()
  {
  #endif
  
        Video (_black_frame, EYES_BOTH, ColourConversion(), _last_emit_was_black, _video_position);
 -      _video_position += _film->video_frames_to_time (1);
 +      _video_position += DCPTime::from_frames (1, _film->video_frame_rate ());
        _last_emit_was_black = true;
  }
  
  void
 -Player::emit_silence (OutputAudioFrame most)
 +Player::emit_silence (DCPTime most)
  {
 -      if (most == 0) {
 +      if (most == DCPTime ()) {
                return;
        }
        
 -      OutputAudioFrame N = min (most, _film->audio_frame_rate() / 2);
 -      shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), N));
 +      DCPTime t = min (most, DCPTime::from_seconds (0.5));
 +      shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), t.frames (_film->audio_frame_rate())));
        silence->make_silent ();
        Audio (silence, _audio_position);
 -      _audio_position += _film->audio_frames_to_time (N);
 +      
 +      _audio_position += t;
  }
  
  void
@@@ -645,14 -638,26 +645,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)
 +Player::update_subtitle_from_image ()
  {
 -      _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 ()
 -{
 -      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;
 -      libdcp::Size scaled_size;
 +      dcpomatic::Rect<double> in_rect = _image_subtitle.subtitle->rect;
 +      dcp::Size scaled_size;
  
        in_rect.x += sc->subtitle_x_offset ();
        in_rect.y += sc->subtitle_y_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,
 -      libdcp::Size inter_size,
 -      libdcp::Size out_size,
 +      dcp::Size inter_size,
 +      dcp::Size out_size,
        Scaler const * scaler
        )
        : _in (in)
@@@ -755,10 -756,10 +755,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.seconds()));
 +}
 +
 +PlayerStatistics const &
 +Player::statistics () const
 +{
 +      return _statistics;
 +}
diff --combined src/lib/playlist.cc
index b7b4c20f7fc56b6dc232be8a773360bab0bcee83,608323e3b5bde2aa32c6fc074541f73ac10afcdc..c46e65d8bfface99d07ef8d5dc00e2475c374f31
@@@ -81,14 -81,14 +81,14 @@@ Playlist::maybe_sequence_video (
        _sequencing_video = true;
        
        ContentList cl = _content;
 -      Time next = 0;
 +      DCPTime next;
        for (ContentList::iterator i = _content.begin(); i != _content.end(); ++i) {
                if (!dynamic_pointer_cast<VideoContent> (*i)) {
                        continue;
                }
                
                (*i)->set_position (next);
 -              next = (*i)->end() + 1;
 +              next = (*i)->end() + DCPTime::delta ();
        }
  
        /* This won't change order, so it does not need a sort */
@@@ -113,11 -113,11 +113,11 @@@ Playlist::video_identifier () cons
  
  /** @param node <Playlist> node */
  void
- Playlist::set_from_xml (shared_ptr<const Film> film, shared_ptr<const cxml::Node> node, int version)
+ Playlist::set_from_xml (shared_ptr<const Film> film, shared_ptr<const cxml::Node> node, int version, list<string>& notes)
  {
        list<cxml::NodePtr> c = node->node_children ("Content");
        for (list<cxml::NodePtr>::iterator i = c.begin(); i != c.end(); ++i) {
-               _content.push_back (content_factory (film, *i, version));
+               _content.push_back (content_factory (film, *i, version, notes));
        }
  
        sort (_content.begin(), _content.end(), ContentSorter ());
@@@ -254,12 -254,12 +254,12 @@@ Playlist::best_dcp_frame_rate () cons
        return best->dcp;
  }
  
 -Time
 +DCPTime
  Playlist::length () const
  {
 -      Time len = 0;
 +      DCPTime len;
        for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
 -              len = max (len, (*i)->end() + 1);
 +              len = max (len, (*i)->end() + DCPTime::delta ());
        }
  
        return len;
@@@ -279,10 -279,10 +279,10 @@@ Playlist::reconnect (
        }
  }
  
 -Time
 +DCPTime
  Playlist::video_end () const
  {
 -      Time end = 0;
 +      DCPTime end;
        for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
                if (dynamic_pointer_cast<const VideoContent> (*i)) {
                        end = max (end, (*i)->end ());
        return end;
  }
  
 +FrameRateChange
 +Playlist::active_frame_rate_change (DCPTime t, int dcp_video_frame_rate) const
 +{
 +      for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
 +              shared_ptr<const VideoContent> vc = dynamic_pointer_cast<const VideoContent> (*i);
 +              if (!vc) {
 +                      break;
 +              }
 +
 +              if (vc->position() >= t && t < vc->end()) {
 +                      return FrameRateChange (vc->video_frame_rate(), dcp_video_frame_rate);
 +              }
 +      }
 +
 +      return FrameRateChange (dcp_video_frame_rate, dcp_video_frame_rate);
 +}
 +
  void
  Playlist::set_sequence_video (bool s)
  {
@@@ -331,7 -314,7 +331,7 @@@ Playlist::content () cons
  void
  Playlist::repeat (ContentList c, int n)
  {
 -      pair<Time, Time> range (TIME_MAX, 0);
 +      pair<DCPTime, DCPTime> range (DCPTime::max (), DCPTime ());
        for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
                range.first = min (range.first, (*i)->position ());
                range.second = max (range.second, (*i)->position ());
                range.second = max (range.second, (*i)->end ());
        }
  
 -      Time pos = range.second;
 +      DCPTime pos = range.second;
        for (int i = 0; i < n; ++i) {
                for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
                        shared_ptr<Content> copy = (*i)->clone ();
@@@ -372,7 -355,7 +372,7 @@@ Playlist::move_earlier (shared_ptr<Cont
                return;
        }
        
 -      Time const p = (*previous)->position ();
 +      DCPTime const p = (*previous)->position ();
        (*previous)->set_position (p + c->length_after_trim ());
        c->set_position (p);
        sort (_content.begin(), _content.end(), ContentSorter ());
@@@ -399,7 -382,7 +399,7 @@@ Playlist::move_later (shared_ptr<Conten
                return;
        }
  
 -      Time const p = (*next)->position ();
 +      DCPTime const p = (*next)->position ();
        (*next)->set_position (c->position ());
        c->set_position (p + c->length_after_trim ());
        sort (_content.begin(), _content.end(), ContentSorter ());
diff --combined src/lib/playlist.h
index 35709f109d2bf4bb4cbcbd725f61bbce7f7bf3ae,394023f5c5c90e4315b1fc9d4f37d8ef394a1f5e..444eb9ae5ebb5ae1aac34932440025c92c3f8b43
@@@ -25,7 -25,6 +25,7 @@@
  #include <boost/enable_shared_from_this.hpp>
  #include "ffmpeg_content.h"
  #include "audio_mapping.h"
 +#include "util.h"
  
  class Content;
  class FFmpegContent;
@@@ -57,7 -56,7 +57,7 @@@ public
        ~Playlist ();
  
        void as_xml (xmlpp::Node *);
-       void set_from_xml (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int);
+       void set_from_xml (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int, std::list<std::string> &);
  
        void add (boost::shared_ptr<Content>);
        void remove (boost::shared_ptr<Content>);
  
        std::string video_identifier () const;
  
 -      Time length () const;
 +      DCPTime length () const;
        
        int best_dcp_frame_rate () const;
 -      Time video_end () const;
 +      DCPTime video_end () const;
 +      FrameRateChange active_frame_rate_change (DCPTime, int dcp_frame_rate) const;
  
        void set_sequence_video (bool);
        void maybe_sequence_video ();
diff --combined src/lib/video_content.cc
index 11310c5dad2954d0d7e5cd4412a605eb2a061286,966b8e8b34e4a32bd3e1d7e1c803f9624cd2e30f..c3aea2b0faab8032ffcf092a773471285cf55537
@@@ -19,7 -19,7 +19,7 @@@
  
  #include <iomanip>
  #include <libcxml/cxml.h>
 -#include <libdcp/colour_matrix.h>
 +#include <dcp/colour_matrix.h>
  #include "video_content.h"
  #include "video_examiner.h"
  #include "compose.hpp"
@@@ -61,7 -61,7 +61,7 @@@ VideoContent::VideoContent (shared_ptr<
        setup_default_colour_conversion ();
  }
  
 -VideoContent::VideoContent (shared_ptr<const Film> f, Time s, VideoContent::Frame len)
 +VideoContent::VideoContent (shared_ptr<const Film> f, DCPTime s, ContentTime len)
        : Content (f, s)
        , _video_length (len)
        , _video_frame_rate (0)
@@@ -84,7 -84,7 +84,7 @@@ VideoContent::VideoContent (shared_ptr<
  VideoContent::VideoContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version)
        : Content (f, node)
  {
 -      _video_length = node->number_child<VideoContent::Frame> ("VideoLength");
 +      _video_length = ContentTime (node->number_child<int64_t> ("VideoLength"));
        _video_size.width = node->number_child<int> ("VideoWidth");
        _video_size.height = node->number_child<int> ("VideoHeight");
        _video_frame_rate = node->number_child<float> ("VideoFrameRate");
@@@ -155,7 -155,7 +155,7 @@@ voi
  VideoContent::as_xml (xmlpp::Node* node) const
  {
        boost::mutex::scoped_lock lm (_mutex);
 -      node->add_child("VideoLength")->add_child_text (lexical_cast<string> (_video_length));
 +      node->add_child("VideoLength")->add_child_text (lexical_cast<string> (_video_length.get ()));
        node->add_child("VideoWidth")->add_child_text (lexical_cast<string> (_video_size.width));
        node->add_child("VideoHeight")->add_child_text (lexical_cast<string> (_video_size.height));
        node->add_child("VideoFrameRate")->add_child_text (lexical_cast<string> (_video_frame_rate));
  void
  VideoContent::setup_default_colour_conversion ()
  {
 -      _colour_conversion = PresetColourConversion (_("sRGB"), 2.4, true, libdcp::colour_matrix::srgb_to_xyz, 2.6).conversion;
 +      _colour_conversion = PresetColourConversion (_("sRGB"), 2.4, true, dcp::colour_matrix::srgb_to_xyz, 2.6).conversion;
  }
  
  void
  VideoContent::take_from_video_examiner (shared_ptr<VideoExaminer> d)
  {
        /* These examiner calls could call other content methods which take a lock on the mutex */
 -      libdcp::Size const vs = d->video_size ();
 +      dcp::Size const vs = d->video_size ();
        float const vfr = d->video_frame_rate ();
        
        {
@@@ -317,26 -317,20 +317,26 @@@ VideoContent::set_video_frame_type (Vid
  string
  VideoContent::technical_summary () const
  {
 -      return String::compose ("video: length %1, size %2x%3, rate %4", video_length(), video_size().width, video_size().height, video_frame_rate());
 +      return String::compose (
 +              "video: length %1, size %2x%3, rate %4",
 +              video_length().seconds(),
 +              video_size().width,
 +              video_size().height,
 +              video_frame_rate()
 +              );
  }
  
 -libdcp::Size
 +dcp::Size
  VideoContent::video_size_after_3d_split () const
  {
 -      libdcp::Size const s = video_size ();
 +      dcp::Size const s = video_size ();
        switch (video_frame_type ()) {
        case VIDEO_FRAME_TYPE_2D:
                return s;
        case VIDEO_FRAME_TYPE_3D_LEFT_RIGHT:
 -              return libdcp::Size (s.width / 2, s.height);
 +              return dcp::Size (s.width / 2, s.height);
        case VIDEO_FRAME_TYPE_3D_TOP_BOTTOM:
 -              return libdcp::Size (s.width, s.height / 2);
 +              return dcp::Size (s.width, s.height / 2);
        }
  
        assert (false);
@@@ -354,21 -348,28 +354,21 @@@ VideoContent::set_colour_conversion (Co
  }
  
  /** @return Video size after 3D split and crop */
 -libdcp::Size
 +dcp::Size
  VideoContent::video_size_after_crop () const
  {
        return crop().apply (video_size_after_3d_split ());
  }
  
  /** @param t A time offset from the start of this piece of content.
 - *  @return Corresponding frame index.
 + *  @return Corresponding time with respect to the content.
   */
 -VideoContent::Frame
 -VideoContent::time_to_content_video_frames (Time t) const
 +ContentTime
 +VideoContent::dcp_time_to_content_time (DCPTime t) const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
 -      
 -      FrameRateConversion frc (video_frame_rate(), film->video_frame_rate());
 -
 -      /* Here we are converting from time (in the DCP) to a frame number in the content.
 -         Hence we need to use the DCP's frame rate and the double/skip correction, not
 -         the source's rate.
 -      */
 -      return t * film->video_frame_rate() / (frc.factor() * TIME_HZ);
 +      return ContentTime (t, FrameRateChange (video_frame_rate(), film->video_frame_rate()));
  }
  
  VideoContentScale::VideoContentScale (Ratio const * r)
@@@ -442,19 -443,30 +442,30 @@@ VideoContentScale::name () cons
        return _("No scale");
  }
  
 -libdcp::Size
 -VideoContentScale::size (shared_ptr<const VideoContent> c, libdcp::Size display_container, libdcp::Size film_container) const
+ /** @param display_container Size of the container that we are displaying this content in.
+  *  @param film_container The size of the film's image.
+  */
- VideoContentScale::size (shared_ptr<const VideoContent> c, dcp::Size container) const
 +dcp::Size
++VideoContentScale::size (shared_ptr<const VideoContent> c, dcp::Size display_container, dcp::Size film_container) const
  {
        if (_ratio) {
-               return fit_ratio_within (_ratio->ratio (), container);
+               return fit_ratio_within (_ratio->ratio (), display_container);
        }
  
-       /* Force scale if the container is smaller than the content's image */
-       if (_scale || container.width < c->video_size().width || container.height < c->video_size().height) {
-               return fit_ratio_within (c->video_size().ratio (), container);
+       libdcp::Size const ac = c->video_size_after_crop ();
+       /* Force scale if the film_container is smaller than the content's image */
+       if (_scale || film_container.width < ac.width || film_container.height < ac.height) {
+               return fit_ratio_within (ac.ratio (), display_container);
        }
  
-       return c->video_size ();
+       /* Scale the image so that it will be in the right place in film_container, even if display_container is a
+          different size.
+       */
+       return libdcp::Size (
+               c->video_size().width  * float(display_container.width)  / film_container.width,
+               c->video_size().height * float(display_container.height) / film_container.height
+               );
  }
  
  void
diff --combined src/lib/video_content.h
index a82c6c22131a339229c55d078942677d57718465,ea4676cecbe1607fe4a86e47a71c278e763e86b2..d2b19480f056f1895ab722d1ee50a96ee1989d37
@@@ -45,7 -45,7 +45,7 @@@ public
        VideoContentScale (bool);
        VideoContentScale (boost::shared_ptr<cxml::Node>);
  
-       dcp::Size size (boost::shared_ptr<const VideoContent>, dcp::Size) const;
 -      libdcp::Size size (boost::shared_ptr<const VideoContent>, libdcp::Size, libdcp::Size) const;
++      dcp::Size size (boost::shared_ptr<const VideoContent>, dcp::Size, dcp::Size) const;
        std::string id () const;
        std::string name () const;
        void as_xml (xmlpp::Node *) const;
@@@ -64,7 -64,9 +64,9 @@@
        }
  
  private:
+       /** a ratio to stretch the content to, or 0 for no stretch */
        Ratio const * _ratio;
+       /** true if we want to scale the content */
        bool _scale;
  
        static std::vector<VideoContentScale> _scales;
@@@ -79,7 -81,7 +81,7 @@@ public
        typedef int Frame;
  
        VideoContent (boost::shared_ptr<const Film>);
 -      VideoContent (boost::shared_ptr<const Film>, Time, VideoContent::Frame);
 +      VideoContent (boost::shared_ptr<const Film>, DCPTime, ContentTime);
        VideoContent (boost::shared_ptr<const Film>, boost::filesystem::path);
        VideoContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int);
        VideoContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
        virtual std::string information () const;
        virtual std::string identifier () const;
  
 -      VideoContent::Frame video_length () const {
 +      ContentTime video_length () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _video_length;
        }
  
 -      libdcp::Size video_size () const {
 +      dcp::Size video_size () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _video_size;
        }
                return _colour_conversion;
        }
  
 -      libdcp::Size video_size_after_3d_split () const;
 -      libdcp::Size video_size_after_crop () const;
 +      dcp::Size video_size_after_3d_split () const;
 +      dcp::Size video_size_after_crop () const;
  
 -      VideoContent::Frame time_to_content_video_frames (Time) const;
 +      ContentTime dcp_time_to_content_time (DCPTime) const;
  
  protected:
        void take_from_video_examiner (boost::shared_ptr<VideoExaminer>);
  
 -      VideoContent::Frame _video_length;
 +      ContentTime _video_length;
        float _video_frame_rate;
  
  private:
  
        void setup_default_colour_conversion ();
        
 -      libdcp::Size _video_size;
 +      dcp::Size _video_size;
        VideoFrameType _video_frame_type;
        Crop _crop;
        VideoContentScale _scale;
diff --combined src/lib/writer.cc
index 33b7dd51ed00c04ed754ff7d7653cb5dead9639e,23f8bee97d487b5a018f4e0eed6f3922da3f407d..125efd644e857933a2bf59d1157061eec405bde8
  
  #include <fstream>
  #include <cerrno>
 -#include <libdcp/mono_picture_asset.h>
 -#include <libdcp/stereo_picture_asset.h>
 -#include <libdcp/sound_asset.h>
 -#include <libdcp/reel.h>
 -#include <libdcp/dcp.h>
 -#include <libdcp/cpl.h>
 +#include <dcp/mono_picture_mxf.h>
 +#include <dcp/stereo_picture_mxf.h>
 +#include <dcp/sound_mxf.h>
 +#include <dcp/sound_mxf_writer.h>
 +#include <dcp/reel.h>
 +#include <dcp/reel_mono_picture_asset.h>
 +#include <dcp/reel_stereo_picture_asset.h>
 +#include <dcp/reel_sound_asset.h>
 +#include <dcp/dcp.h>
 +#include <dcp/cpl.h>
  #include "writer.h"
  #include "compose.hpp"
  #include "film.h"
  #include "config.h"
  #include "job.h"
  #include "cross.h"
 +#include "audio_buffers.h"
  
  #include "i18n.h"
  
+ /* OS X strikes again */
+ #undef set_key
  using std::make_pair;
  using std::pair;
  using std::string;
@@@ -53,7 -51,6 +56,7 @@@ using std::cout
  using std::stringstream;
  using boost::shared_ptr;
  using boost::weak_ptr;
 +using boost::dynamic_pointer_cast;
  
  int const Writer::_maximum_frames_in_memory = Config::instance()->num_local_encoding_threads() + 4;
  
@@@ -86,33 -83,35 +89,33 @@@ Writer::Writer (shared_ptr<const Film> 
        */
  
        if (_film->three_d ()) {
 -              _picture_asset.reset (new libdcp::StereoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ()));
 +              _picture_mxf.reset (new dcp::StereoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1)));
        } else {
 -              _picture_asset.reset (new libdcp::MonoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ()));
 +              _picture_mxf.reset (new dcp::MonoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1)));
        }
  
-       _picture_mxf->set_size (fit_ratio_within (_film->container()->ratio(), _film->full_frame ()));
 -      _picture_asset->set_edit_rate (_film->video_frame_rate ());
 -      _picture_asset->set_size (_film->frame_size ());
 -      _picture_asset->set_interop (_film->interop ());
++      _picture_mxf->set_size (_film->frame_size ());
  
        if (_film->encrypted ()) {
 -              _picture_asset->set_key (_film->key ());
 +              _picture_mxf->set_key (_film->key ());
        }
        
 -      _picture_asset_writer = _picture_asset->start_write (_first_nonexistant_frame > 0);
 +      _picture_mxf_writer = _picture_mxf->start_write (
 +              _film->internal_video_mxf_dir() / _film->internal_video_mxf_filename(),
 +              _film->interop() ? dcp::INTEROP : dcp::SMPTE,
 +              _first_nonexistant_frame > 0
 +              );
  
 -      _sound_asset.reset (new libdcp::SoundAsset (_film->directory (), _film->audio_mxf_filename ()));
 -      _sound_asset->set_edit_rate (_film->video_frame_rate ());
 -      _sound_asset->set_channels (_film->audio_channels ());
 -      _sound_asset->set_sampling_rate (_film->audio_frame_rate ());
 -      _sound_asset->set_interop (_film->interop ());
 +      _sound_mxf.reset (new dcp::SoundMXF (dcp::Fraction (_film->video_frame_rate(), 1), _film->audio_frame_rate (), _film->audio_channels ()));
  
        if (_film->encrypted ()) {
 -              _sound_asset->set_key (_film->key ());
 +              _sound_mxf->set_key (_film->key ());
        }
        
 -      /* Write the sound asset into the film directory so that we leave the creation
 +      /* Write the sound MXF into the film directory so that we leave the creation
           of the DCP directory until the last minute.
        */
 -      _sound_asset_writer = _sound_asset->start_write ();
 +      _sound_mxf_writer = _sound_mxf->start_write (_film->directory() / _film->audio_mxf_filename(), _film->interop() ? dcp::INTEROP : dcp::SMPTE);
  
        _thread = new boost::thread (boost::bind (&Writer::thread, this));
  
@@@ -165,7 -164,7 +168,7 @@@ Writer::fake_write (int frame, Eyes eye
        }
        
        FILE* ifi = fopen_boost (_film->info_path (frame, eyes), "r");
 -      libdcp::FrameInfo info (ifi);
 +      dcp::FrameInfo info (ifi);
        fclose (ifi);
        
        QueueItem qi;
  void
  Writer::write (shared_ptr<const AudioBuffers> audio)
  {
 -      _sound_asset_writer->write (audio->data(), audio->frames());
 +      _sound_mxf_writer->write (audio->data(), audio->frames());
  }
  
  /** This must be called from Writer::thread() with an appropriate lock held */
@@@ -262,7 -261,7 +265,7 @@@ tr
                                        qi.encoded.reset (new EncodedData (_film->j2c_path (qi.frame, qi.eyes, false)));
                                }
  
 -                              libdcp::FrameInfo fin = _picture_asset_writer->write (qi.encoded->data(), qi.encoded->size());
 +                              dcp::FrameInfo fin = _picture_mxf_writer->write (qi.encoded->data(), qi.encoded->size());
                                qi.encoded->write_info (_film, qi.frame, qi.eyes, fin);
                                _last_written[qi.eyes] = qi.encoded;
                                ++_full_written;
                        }
                        case QueueItem::FAKE:
                                _film->log()->log (String::compose (N_("Writer FAKE-writes %1 to MXF"), qi.frame));
 -                              _picture_asset_writer->fake_write (qi.size);
 +                              _picture_mxf_writer->fake_write (qi.size);
                                _last_written[qi.eyes].reset ();
                                ++_fake_written;
                                break;
                        case QueueItem::REPEAT:
                        {
                                _film->log()->log (String::compose (N_("Writer REPEAT-writes %1 to MXF"), qi.frame));
 -                              libdcp::FrameInfo fin = _picture_asset_writer->write (
 +                              dcp::FrameInfo fin = _picture_mxf_writer->write (
                                        _last_written[qi.eyes]->data(),
                                        _last_written[qi.eyes]->size()
                                        );
                        _last_written_frame = qi.frame;
                        _last_written_eyes = qi.eyes;
                        
 -                      if (_film->length()) {
 -                              shared_ptr<Job> job = _job.lock ();
 -                              assert (job);
 -                              int total = _film->time_to_video_frames (_film->length ());
 -                              if (_film->three_d ()) {
 -                                      /* _full_written and so on are incremented for each eye, so we need to double the total
 -                                         frames to get the correct progress.
 -                                      */
 -                                      total *= 2;
 -                              }
 +                      shared_ptr<Job> job = _job.lock ();
 +                      assert (job);
 +                      int64_t total = _film->length().frames (_film->video_frame_rate ());
 +                      if (_film->three_d ()) {
 +                              /* _full_written and so on are incremented for each eye, so we need to double the total
 +                                 frames to get the correct progress.
 +                              */
 +                              total *= 2;
 +                      }
 +                      if (total) {
                                job->set_progress (float (_full_written + _fake_written + _repeat_written) / total);
                        }
                }
@@@ -377,9 -376,13 +380,9 @@@ Writer::finish (
        
        terminate_thread (true);
  
 -      _picture_asset_writer->finalize ();
 -      _sound_asset_writer->finalize ();
 +      _picture_mxf_writer->finalize ();
 +      _sound_mxf_writer->finalize ();
        
 -      int const frames = _last_written_frame + 1;
 -
 -      _picture_asset->set_duration (frames);
 -
        /* Hard-link the video MXF into the DCP */
        boost::filesystem::path video_from;
        video_from /= _film->internal_video_mxf_dir();
                _film->log()->log ("Hard-link failed; fell back to copying");
        }
  
 -      /* And update the asset */
 -
 -      _picture_asset->set_directory (_film->dir (_film->dcp_name ()));
 -      _picture_asset->set_file_name (_film->video_mxf_filename ());
 -
        /* Move the audio MXF into the DCP */
  
        boost::filesystem::path audio_to;
                        );
        }
  
 -      _sound_asset->set_directory (_film->dir (_film->dcp_name ()));
 -      _sound_asset->set_duration (frames);
 -      
 -      libdcp::DCP dcp (_film->dir (_film->dcp_name()));
 +      dcp::DCP dcp (_film->dir (_film->dcp_name()));
  
 -      shared_ptr<libdcp::CPL> cpl (
 -              new libdcp::CPL (
 -                      _film->dir (_film->dcp_name()),
 +      shared_ptr<dcp::CPL> cpl (
 +              new dcp::CPL (
                        _film->dcp_name(),
 -                      _film->dcp_content_type()->libdcp_kind (),
 -                      frames,
 -                      _film->video_frame_rate ()
 +                      _film->dcp_content_type()->libdcp_kind ()
                        )
                );
        
 -      dcp.add_cpl (cpl);
 +      dcp.add (cpl);
 +
 +      shared_ptr<dcp::Reel> reel (new dcp::Reel ());
 +
 +      shared_ptr<dcp::MonoPictureMXF> mono = dynamic_pointer_cast<dcp::MonoPictureMXF> (_picture_mxf);
 +      if (mono) {
 +              reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelMonoPictureAsset (mono, 0)));
 +      }
  
 -      cpl->add_reel (shared_ptr<libdcp::Reel> (new libdcp::Reel (
 -                                                       _picture_asset,
 -                                                       _sound_asset,
 -                                                       shared_ptr<libdcp::SubtitleAsset> ()
 -                                                       )
 -                             ));
 +      shared_ptr<dcp::StereoPictureMXF> stereo = dynamic_pointer_cast<dcp::StereoPictureMXF> (_picture_mxf);
 +      if (stereo) {
 +              reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelStereoPictureAsset (stereo, 0)));
 +      }
 +
 +      reel->add (shared_ptr<dcp::ReelSoundAsset> (new dcp::ReelSoundAsset (_sound_mxf, 0)));
 +      
 +      cpl->add (reel);
  
        shared_ptr<Job> job = _job.lock ();
        assert (job);
  
        job->sub (_("Computing image digest"));
 -      _picture_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false));
 +      _picture_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false));
  
        job->sub (_("Computing audio digest"));
 -      _sound_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false));
 +      _sound_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false));
  
 -      libdcp::XMLMetadata meta = Config::instance()->dcp_metadata ();
 +      dcp::XMLMetadata meta = Config::instance()->dcp_metadata ();
        meta.set_issue_date_now ();
 -      dcp.write_xml (_film->interop (), meta, _film->is_signed() ? make_signer () : shared_ptr<const libdcp::Signer> ());
 +      dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, _film->is_signed() ? make_signer () : shared_ptr<const dcp::Signer> ());
  
        _film->log()->log (
                String::compose (N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT; %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk)
@@@ -491,7 -496,7 +494,7 @@@ Writer::check_existing_picture_mxf_fram
                return false;
        }
        
 -      libdcp::FrameInfo info (ifi);
 +      dcp::FrameInfo info (ifi);
        fclose (ifi);
        if (info.size == 0) {
                _film->log()->log (String::compose ("Existing frame %1 has no info file", f));
diff --combined src/wx/config_dialog.cc
index 911cb4b3f3cf07efb19d28aa8deeb948d2107926,8938c84f949070a821791d88ec6a9baa26b752ca..44745006c3409297edf2f9a9296a14b8bb3581ea
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
  #include <boost/lexical_cast.hpp>
  #include <boost/filesystem.hpp>
  #include <wx/stdpaths.h>
- #include <wx/notebook.h>
+ #include <wx/preferences.h>
+ #include <wx/filepicker.h>
+ #include <wx/spinctrl.h>
 -#include <libdcp/colour_matrix.h>
 +#include <dcp/colour_matrix.h>
  #include "lib/config.h"
  #include "lib/ratio.h"
  #include "lib/scaler.h"
@@@ -35,6 -37,7 +37,7 @@@
  #include "lib/colour_conversion.h"
  #include "config_dialog.h"
  #include "wx_util.h"
+ #include "editable_list.h"
  #include "filter_dialog.h"
  #include "dir_picker_ctrl.h"
  #include "dci_metadata_dialog.h"
  using std::vector;
  using std::string;
  using std::list;
+ using std::cout;
  using boost::bind;
  using boost::shared_ptr;
  using boost::lexical_cast;
  
- ConfigDialog::ConfigDialog (wxWindow* parent)
-       : wxDialog (parent, wxID_ANY, _("DCP-o-matic Preferences"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
+ class Page
  {
-       wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-       _notebook = new wxNotebook (this, wxID_ANY);
-       s->Add (_notebook, 1);
-       make_misc_panel ();
-       _notebook->AddPage (_misc_panel, _("Miscellaneous"), true);
-       make_defaults_panel ();
-       _notebook->AddPage (_defaults_panel, _("Defaults"), false);
-       make_servers_panel ();
-       _notebook->AddPage (_servers_panel, _("Encoding servers"), false);
-       make_colour_conversions_panel ();
-       _notebook->AddPage (_colour_conversions_panel, _("Colour conversions"), false);
-       make_metadata_panel ();
-       _notebook->AddPage (_metadata_panel, _("Metadata"), false);
-       make_tms_panel ();
-       _notebook->AddPage (_tms_panel, _("TMS"), false);
-       make_kdm_email_panel ();
-       _notebook->AddPage (_kdm_email_panel, _("KDM email"), false);
-       wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
-       overall_sizer->Add (s, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
-       wxSizer* buttons = CreateSeparatedButtonSizer (wxOK);
-       if (buttons) {
-               overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
-       }
-       SetSizer (overall_sizer);
-       overall_sizer->Layout ();
-       overall_sizer->SetSizeHints (this);
- }
+ public:
+       Page (wxSize panel_size, int border)
+               : _panel_size (panel_size)
+               , _border (border)
+       {}
+ protected:
+       wxSize _panel_size;
+       int _border;
+ };
  
- void
- ConfigDialog::make_misc_panel ()
+ class GeneralPage : public wxStockPreferencesPage, public Page
  {
-       _misc_panel = new wxPanel (_notebook);
-       wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-       _misc_panel->SetSizer (s);
-       wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-       table->AddGrowableCol (1, 1);
-       s->Add (table, 1, wxALL | wxEXPAND, 8);
-       _set_language = new wxCheckBox (_misc_panel, wxID_ANY, _("Set language"));
-       table->Add (_set_language, 1);
-       _language = new wxChoice (_misc_panel, wxID_ANY);
-       _language->Append (wxT ("English"));
-       _language->Append (wxT ("Français"));
-       _language->Append (wxT ("Italiano"));
-       _language->Append (wxT ("Español"));
-       _language->Append (wxT ("Svenska"));
-       _language->Append (wxT ("Deutsch"));
-       table->Add (_language);
-       wxStaticText* restart = add_label_to_sizer (table, _misc_panel, _("(restart DCP-o-matic to see language changes)"), false);
-       wxFont font = restart->GetFont();
-       font.SetStyle (wxFONTSTYLE_ITALIC);
-       font.SetPointSize (font.GetPointSize() - 1);
-       restart->SetFont (font);
-       table->AddSpacer (0);
-       add_label_to_sizer (table, _misc_panel, _("Threads to use for encoding on this host"), true);
-       _num_local_encoding_threads = new wxSpinCtrl (_misc_panel);
-       table->Add (_num_local_encoding_threads, 1);
-       add_label_to_sizer (table, _misc_panel, _("Outgoing mail server"), true);
-       _mail_server = new wxTextCtrl (_misc_panel, wxID_ANY);
-       table->Add (_mail_server, 1, wxEXPAND | wxALL);
-       add_label_to_sizer (table, _misc_panel, _("Mail user name"), true);
-       _mail_user = new wxTextCtrl (_misc_panel, wxID_ANY);
-       table->Add (_mail_user, 1, wxEXPAND | wxALL);
-       add_label_to_sizer (table, _misc_panel, _("Mail password"), true);
-       _mail_password = new wxTextCtrl (_misc_panel, wxID_ANY);
-       table->Add (_mail_password, 1, wxEXPAND | wxALL);
-       wxStaticText* plain = add_label_to_sizer (table, _misc_panel, _("(password will be stored on disk in plaintext)"), false);
-       plain->SetFont (font);
-       table->AddSpacer (0);
-       
-       add_label_to_sizer (table, _misc_panel, _("From address for KDM emails"), true);
-       _kdm_from = new wxTextCtrl (_misc_panel, wxID_ANY);
-       table->Add (_kdm_from, 1, wxEXPAND | wxALL);
+ public:
+       GeneralPage (wxSize panel_size, int border)
+               : wxStockPreferencesPage (Kind_General)
+               , Page (panel_size, border)
+       {}
  
-       _check_for_updates = new wxCheckBox (_misc_panel, wxID_ANY, _("Check for updates on startup"));
-       table->Add (_check_for_updates, 1, wxEXPAND | wxALL);
-       table->AddSpacer (0);
+       wxWindow* CreateWindow (wxWindow* parent)
+       {
+               wxPanel* panel = new wxPanel (parent);
+               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
+               panel->SetSizer (s);
+               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+               table->AddGrowableCol (1, 1);
+               s->Add (table, 1, wxALL | wxEXPAND, _border);
+               
+               _set_language = new wxCheckBox (panel, wxID_ANY, _("Set language"));
+               table->Add (_set_language, 1);
+               _language = new wxChoice (panel, wxID_ANY);
+               _language->Append (wxT ("English"));
+               _language->Append (wxT ("Français"));
+               _language->Append (wxT ("Italiano"));
+               _language->Append (wxT ("Español"));
+               _language->Append (wxT ("Svenska"));
+               _language->Append (wxT ("Deutsch"));
+               table->Add (_language);
+               
+               wxStaticText* restart = add_label_to_sizer (table, panel, _("(restart DCP-o-matic to see language changes)"), false);
+               wxFont font = restart->GetFont();
+               font.SetStyle (wxFONTSTYLE_ITALIC);
+               font.SetPointSize (font.GetPointSize() - 1);
+               restart->SetFont (font);
+               table->AddSpacer (0);
+               
+               add_label_to_sizer (table, panel, _("Threads to use for encoding on this host"), true);
+               _num_local_encoding_threads = new wxSpinCtrl (panel);
+               table->Add (_num_local_encoding_threads, 1);
+               
+               add_label_to_sizer (table, panel, _("Outgoing mail server"), true);
+               _mail_server = new wxTextCtrl (panel, wxID_ANY);
+               table->Add (_mail_server, 1, wxEXPAND | wxALL);
+               
+               add_label_to_sizer (table, panel, _("Mail user name"), true);
+               _mail_user = new wxTextCtrl (panel, wxID_ANY);
+               table->Add (_mail_user, 1, wxEXPAND | wxALL);
+               
+               add_label_to_sizer (table, panel, _("Mail password"), true);
+               _mail_password = new wxTextCtrl (panel, wxID_ANY);
+               table->Add (_mail_password, 1, wxEXPAND | wxALL);
+               
+               wxStaticText* plain = add_label_to_sizer (table, panel, _("(password will be stored on disk in plaintext)"), false);
+               plain->SetFont (font);
+               table->AddSpacer (0);
+               
+               add_label_to_sizer (table, panel, _("From address for KDM emails"), true);
+               _kdm_from = new wxTextCtrl (panel, wxID_ANY);
+               table->Add (_kdm_from, 1, wxEXPAND | wxALL);
+               
+               _check_for_updates = new wxCheckBox (panel, wxID_ANY, _("Check for updates on startup"));
+               table->Add (_check_for_updates, 1, wxEXPAND | wxALL);
+               table->AddSpacer (0);
+               
+               _check_for_test_updates = new wxCheckBox (panel, wxID_ANY, _("Check for testing updates as well as stable ones"));
+               table->Add (_check_for_test_updates, 1, wxEXPAND | wxALL);
+               table->AddSpacer (0);
+               
+               Config* config = Config::instance ();
+               
+               _set_language->SetValue (config->language ());
+               
+               if (config->language().get_value_or ("") == "fr") {
+                       _language->SetSelection (1);
+               } else if (config->language().get_value_or ("") == "it") {
+               _language->SetSelection (2);
+               } else if (config->language().get_value_or ("") == "es") {
+                       _language->SetSelection (3);
+               } else if (config->language().get_value_or ("") == "sv") {
+                       _language->SetSelection (4);
+               } else if (config->language().get_value_or ("") == "de") {
+                       _language->SetSelection (5);
+               } else {
+                       _language->SetSelection (0);
+               }
+               
+               setup_language_sensitivity ();
+               
+               _set_language->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::set_language_changed, this));
+               _language->Bind     (wxEVT_COMMAND_CHOICE_SELECTED,  boost::bind (&GeneralPage::language_changed,     this));
+               
+               _num_local_encoding_threads->SetRange (1, 128);
+               _num_local_encoding_threads->SetValue (config->num_local_encoding_threads ());
+               _num_local_encoding_threads->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&GeneralPage::num_local_encoding_threads_changed, this));
+               
+               _mail_server->SetValue (std_to_wx (config->mail_server ()));
+               _mail_server->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&GeneralPage::mail_server_changed, this));
+               _mail_user->SetValue (std_to_wx (config->mail_user ()));
+               _mail_user->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&GeneralPage::mail_user_changed, this));
+               _mail_password->SetValue (std_to_wx (config->mail_password ()));
+               _mail_password->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&GeneralPage::mail_password_changed, this));
+               _kdm_from->SetValue (std_to_wx (config->kdm_from ()));
+               _kdm_from->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&GeneralPage::kdm_from_changed, this));
+               _check_for_updates->SetValue (config->check_for_updates ());
+               _check_for_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::check_for_updates_changed, this));
+               _check_for_test_updates->SetValue (config->check_for_test_updates ());
+               _check_for_test_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::check_for_test_updates_changed, this));
+               
+               return panel;
+       }
  
-       _check_for_test_updates = new wxCheckBox (_misc_panel, wxID_ANY, _("Check for testing updates as well as stable ones"));
-       table->Add (_check_for_test_updates, 1, wxEXPAND | wxALL);
-       table->AddSpacer (0);
-       
-       Config* config = Config::instance ();
+ private:      
+       void setup_language_sensitivity ()
+       {
+               _language->Enable (_set_language->GetValue ());
+       }
  
-       _set_language->SetValue (config->language ());
+       void set_language_changed ()
+       {
+               setup_language_sensitivity ();
+               if (_set_language->GetValue ()) {
+                       language_changed ();
+               } else {
+                       Config::instance()->unset_language ();
+               }
+       }
  
-       if (config->language().get_value_or ("") == "fr") {
-               _language->SetSelection (1);
-       } else if (config->language().get_value_or ("") == "it") {
-               _language->SetSelection (2);
-       } else if (config->language().get_value_or ("") == "es") {
-               _language->SetSelection (3);
-       } else if (config->language().get_value_or ("") == "sv") {
-               _language->SetSelection (4);
-       } else if (config->language().get_value_or ("") == "de") {
-               _language->SetSelection (5);
-       } else {
-               _language->SetSelection (0);
-       }
-       setup_language_sensitivity ();
-       _set_language->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&ConfigDialog::set_language_changed, this));
-       _language->Bind     (wxEVT_COMMAND_CHOICE_SELECTED,  boost::bind (&ConfigDialog::language_changed,     this));
-       _num_local_encoding_threads->SetRange (1, 128);
-       _num_local_encoding_threads->SetValue (config->num_local_encoding_threads ());
-       _num_local_encoding_threads->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&ConfigDialog::num_local_encoding_threads_changed, this));
-       _mail_server->SetValue (std_to_wx (config->mail_server ()));
-       _mail_server->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::mail_server_changed, this));
-       _mail_user->SetValue (std_to_wx (config->mail_user ()));
-       _mail_user->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::mail_user_changed, this));
-       _mail_password->SetValue (std_to_wx (config->mail_password ()));
-       _mail_password->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::mail_password_changed, this));
-       _kdm_from->SetValue (std_to_wx (config->kdm_from ()));
-       _kdm_from->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::kdm_from_changed, this));
-       _check_for_updates->SetValue (config->check_for_updates ());
-       _check_for_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&ConfigDialog::check_for_updates_changed, this));
-       _check_for_test_updates->SetValue (config->check_for_test_updates ());
-       _check_for_test_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&ConfigDialog::check_for_test_updates_changed, this));
- }
+       void language_changed ()
+       {
+               switch (_language->GetSelection ()) {
+               case 0:
+                       Config::instance()->set_language ("en");
+                       break;
+               case 1:
+                       Config::instance()->set_language ("fr");
+                       break;
+               case 2:
+                       Config::instance()->set_language ("it");
+                       break;
+               case 3:
+                       Config::instance()->set_language ("es");
+                       break;
+               case 4:
+                       Config::instance()->set_language ("sv");
+                       break;
+               case 5:
+                       Config::instance()->set_language ("de");
+                       break;
+               }
+       }
+       
+       void mail_server_changed ()
+       {
+               Config::instance()->set_mail_server (wx_to_std (_mail_server->GetValue ()));
+       }
+       
+       void mail_user_changed ()
+       {
+               Config::instance()->set_mail_user (wx_to_std (_mail_user->GetValue ()));
+       }
+       
+       void mail_password_changed ()
+       {
+               Config::instance()->set_mail_password (wx_to_std (_mail_password->GetValue ()));
+       }
+       
+       void kdm_from_changed ()
+       {
+               Config::instance()->set_kdm_from (wx_to_std (_kdm_from->GetValue ()));
+       }
  
- void
- ConfigDialog::make_defaults_panel ()
- {
-       _defaults_panel = new wxPanel (_notebook);
-       wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-       _defaults_panel->SetSizer (s);
+       void check_for_updates_changed ()
+       {
+               Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
+       }
+       
+       void check_for_test_updates_changed ()
+       {
+               Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
+       }
  
-       wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-       table->AddGrowableCol (1, 1);
-       s->Add (table, 1, wxALL | wxEXPAND, 8);
+       void num_local_encoding_threads_changed ()
+       {
+               Config::instance()->set_num_local_encoding_threads (_num_local_encoding_threads->GetValue ());
+       }
+       
+       wxCheckBox* _set_language;
+       wxChoice* _language;
+       wxSpinCtrl* _num_local_encoding_threads;
+       wxTextCtrl* _mail_server;
+       wxTextCtrl* _mail_user;
+       wxTextCtrl* _mail_password;
+       wxTextCtrl* _kdm_from;
+       wxCheckBox* _check_for_updates;
+       wxCheckBox* _check_for_test_updates;
+ };
+ class DefaultsPage : public wxPreferencesPage, public Page
+ {
+ public:
+       DefaultsPage (wxSize panel_size, int border)
+               : Page (panel_size, border)
+       {}
+       
+       wxString GetName () const
+       {
+               return _("Defaults");
+       }
  
+ #ifdef DCPOMATIC_OSX  
+       wxBitmap GetLargeIcon () const
        {
-               add_label_to_sizer (table, _defaults_panel, _("Default duration of still images"), true);
-               wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _default_still_length = new wxSpinCtrl (_defaults_panel);
-               s->Add (_default_still_length);
-               add_label_to_sizer (s, _defaults_panel, _("s"), false);
-               table->Add (s, 1);
+               return wxBitmap ("defaults", wxBITMAP_TYPE_PNG_RESOURCE);
        }
+ #endif        
  
-       add_label_to_sizer (table, _defaults_panel, _("Default directory for new films"), true);
+       wxWindow* CreateWindow (wxWindow* parent)
+       {
+               wxPanel* panel = new wxPanel (parent);
+               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
+               panel->SetSizer (s);
+               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+               table->AddGrowableCol (1, 1);
+               s->Add (table, 1, wxALL | wxEXPAND, _border);
+               
+               {
+                       add_label_to_sizer (table, panel, _("Default duration of still images"), true);
+                       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
+                       _still_length = new wxSpinCtrl (panel);
+                       s->Add (_still_length);
+                       add_label_to_sizer (s, panel, _("s"), false);
+                       table->Add (s, 1);
+               }
+               
+               add_label_to_sizer (table, panel, _("Default directory for new films"), true);
  #ifdef DCPOMATIC_USE_OWN_DIR_PICKER
-       _default_directory = new DirPickerCtrl (_defaults_panel);
+               _directory = new DirPickerCtrl (panel);
  #else 
-       _default_directory = new wxDirPickerCtrl (_defaults_panel, wxDD_DIR_MUST_EXIST);
+               _directory = new wxDirPickerCtrl (panel, wxDD_DIR_MUST_EXIST);
  #endif
-       table->Add (_default_directory, 1, wxEXPAND);
-       add_label_to_sizer (table, _defaults_panel, _("Default DCI name details"), true);
-       _default_dci_metadata_button = new wxButton (_defaults_panel, wxID_ANY, _("Edit..."));
-       table->Add (_default_dci_metadata_button);
+               table->Add (_directory, 1, wxEXPAND);
+               
+               add_label_to_sizer (table, panel, _("Default DCI name details"), true);
+               _dci_metadata_button = new wxButton (panel, wxID_ANY, _("Edit..."));
+               table->Add (_dci_metadata_button);
+               
+               add_label_to_sizer (table, panel, _("Default container"), true);
+               _container = new wxChoice (panel, wxID_ANY);
+               table->Add (_container);
+               
+               add_label_to_sizer (table, panel, _("Default content type"), true);
+               _dcp_content_type = new wxChoice (panel, wxID_ANY);
+               table->Add (_dcp_content_type);
+               
+               {
+                       add_label_to_sizer (table, panel, _("Default JPEG2000 bandwidth"), true);
+                       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
+                       _j2k_bandwidth = new wxSpinCtrl (panel);
+                       s->Add (_j2k_bandwidth);
+                       add_label_to_sizer (s, panel, _("Mbit/s"), false);
+                       table->Add (s, 1);
+               }
+               
+               {
+                       add_label_to_sizer (table, panel, _("Default audio delay"), true);
+                       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
+                       _audio_delay = new wxSpinCtrl (panel);
+                       s->Add (_audio_delay);
+                       add_label_to_sizer (s, panel, _("ms"), false);
+                       table->Add (s, 1);
+               }
  
-       add_label_to_sizer (table, _defaults_panel, _("Default container"), true);
-       _default_container = new wxChoice (_defaults_panel, wxID_ANY);
-       table->Add (_default_container);
+               add_label_to_sizer (table, panel, _("Default issuer"), true);
+               _issuer = new wxTextCtrl (panel, wxID_ANY);
+               table->Add (_issuer, 1, wxEXPAND);
+               add_label_to_sizer (table, panel, _("Default creator"), true);
+               _creator = new wxTextCtrl (panel, wxID_ANY);
+               table->Add (_creator, 1, wxEXPAND);
+               
+               Config* config = Config::instance ();
+               
+               _still_length->SetRange (1, 3600);
+               _still_length->SetValue (config->default_still_length ());
+               _still_length->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::still_length_changed, this));
+               
+               _directory->SetPath (std_to_wx (config->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())).string ()));
+               _directory->Bind (wxEVT_COMMAND_DIRPICKER_CHANGED, boost::bind (&DefaultsPage::directory_changed, this));
+               
+               _dci_metadata_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&DefaultsPage::edit_dci_metadata_clicked, this, parent));
+               
+               vector<Ratio const *> ratio = Ratio::all ();
+               int n = 0;
+               for (vector<Ratio const *>::iterator i = ratio.begin(); i != ratio.end(); ++i) {
+                       _container->Append (std_to_wx ((*i)->nickname ()));
+                       if (*i == config->default_container ()) {
+                               _container->SetSelection (n);
+                       }
+                       ++n;
+               }
+               
+               _container->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::container_changed, this));
+               
+               vector<DCPContentType const *> const ct = DCPContentType::all ();
+               n = 0;
+               for (vector<DCPContentType const *>::const_iterator i = ct.begin(); i != ct.end(); ++i) {
+                       _dcp_content_type->Append (std_to_wx ((*i)->pretty_name ()));
+                       if (*i == config->default_dcp_content_type ()) {
+                               _dcp_content_type->SetSelection (n);
+                       }
+                       ++n;
+               }
+               
+               _dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DefaultsPage::dcp_content_type_changed, this));
+               
+               _j2k_bandwidth->SetRange (50, 250);
+               _j2k_bandwidth->SetValue (config->default_j2k_bandwidth() / 1000000);
+               _j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::j2k_bandwidth_changed, this));
+               
+               _audio_delay->SetRange (-1000, 1000);
+               _audio_delay->SetValue (config->default_audio_delay ());
+               _audio_delay->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DefaultsPage::audio_delay_changed, this));
+               _issuer->SetValue (std_to_wx (config->dcp_metadata().issuer));
+               _issuer->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&DefaultsPage::issuer_changed, this));
+               _creator->SetValue (std_to_wx (config->dcp_metadata().creator));
+               _creator->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&DefaultsPage::creator_changed, this));
+               return panel;
+       }
  
-       add_label_to_sizer (table, _defaults_panel, _("Default content type"), true);
-       _default_dcp_content_type = new wxChoice (_defaults_panel, wxID_ANY);
-       table->Add (_default_dcp_content_type);
+ private:
+       void j2k_bandwidth_changed ()
+       {
+               Config::instance()->set_default_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1000000);
+       }
+       
+       void audio_delay_changed ()
+       {
+               Config::instance()->set_default_audio_delay (_audio_delay->GetValue());
+       }
  
+       void directory_changed ()
        {
-               add_label_to_sizer (table, _defaults_panel, _("Default JPEG2000 bandwidth"), true);
-               wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _default_j2k_bandwidth = new wxSpinCtrl (_defaults_panel);
-               s->Add (_default_j2k_bandwidth);
-               add_label_to_sizer (s, _defaults_panel, _("Mbit/s"), false);
-               table->Add (s, 1);
+               Config::instance()->set_default_directory (wx_to_std (_directory->GetPath ()));
        }
  
+       void edit_dci_metadata_clicked (wxWindow* parent)
        {
-               add_label_to_sizer (table, _defaults_panel, _("Default audio delay"), true);
-               wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _default_audio_delay = new wxSpinCtrl (_defaults_panel);
-               s->Add (_default_audio_delay);
-               add_label_to_sizer (s, _defaults_panel, _("ms"), false);
-               table->Add (s, 1);
+               DCIMetadataDialog* d = new DCIMetadataDialog (parent, Config::instance()->default_dci_metadata ());
+               d->ShowModal ();
+               Config::instance()->set_default_dci_metadata (d->dci_metadata ());
+               d->Destroy ();
        }
  
-       Config* config = Config::instance ();
+       void still_length_changed ()
+       {
+               Config::instance()->set_default_still_length (_still_length->GetValue ());
+       }
        
-       _default_still_length->SetRange (1, 3600);
-       _default_still_length->SetValue (config->default_still_length ());
-       _default_still_length->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&ConfigDialog::default_still_length_changed, this));
-       _default_directory->SetPath (std_to_wx (config->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())).string ()));
-       _default_directory->Bind (wxEVT_COMMAND_DIRPICKER_CHANGED, boost::bind (&ConfigDialog::default_directory_changed, this));
-       _default_dci_metadata_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ConfigDialog::edit_default_dci_metadata_clicked, this));
-       vector<Ratio const *> ratio = Ratio::all ();
-       int n = 0;
-       for (vector<Ratio const *>::iterator i = ratio.begin(); i != ratio.end(); ++i) {
-               _default_container->Append (std_to_wx ((*i)->nickname ()));
-               if (*i == config->default_container ()) {
-                       _default_container->SetSelection (n);
-               }
-               ++n;
+       void container_changed ()
+       {
+               vector<Ratio const *> ratio = Ratio::all ();
+               Config::instance()->set_default_container (ratio[_container->GetSelection()]);
        }
-       _default_container->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&ConfigDialog::default_container_changed, this));
        
-       vector<DCPContentType const *> const ct = DCPContentType::all ();
-       n = 0;
-       for (vector<DCPContentType const *>::const_iterator i = ct.begin(); i != ct.end(); ++i) {
-               _default_dcp_content_type->Append (std_to_wx ((*i)->pretty_name ()));
-               if (*i == config->default_dcp_content_type ()) {
-                       _default_dcp_content_type->SetSelection (n);
-               }
-               ++n;
+       void dcp_content_type_changed ()
+       {
+               vector<DCPContentType const *> ct = DCPContentType::all ();
+               Config::instance()->set_default_dcp_content_type (ct[_dcp_content_type->GetSelection()]);
        }
  
-       _default_dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&ConfigDialog::default_dcp_content_type_changed, this));
-       _default_j2k_bandwidth->SetRange (50, 250);
-       _default_j2k_bandwidth->SetValue (config->default_j2k_bandwidth() / 1000000);
-       _default_j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&ConfigDialog::default_j2k_bandwidth_changed, this));
-       _default_audio_delay->SetRange (-1000, 1000);
-       _default_audio_delay->SetValue (config->default_audio_delay ());
-       _default_audio_delay->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&ConfigDialog::default_audio_delay_changed, this));
- }
- void
- ConfigDialog::make_tms_panel ()
- {
-       _tms_panel = new wxPanel (_notebook);
-       wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-       _tms_panel->SetSizer (s);
-       wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-       table->AddGrowableCol (1, 1);
-       s->Add (table, 1, wxALL | wxEXPAND, 8);
-       add_label_to_sizer (table, _tms_panel, _("IP address"), true);
-       _tms_ip = new wxTextCtrl (_tms_panel, wxID_ANY);
-       table->Add (_tms_ip, 1, wxEXPAND);
-       add_label_to_sizer (table, _tms_panel, _("Target path"), true);
-       _tms_path = new wxTextCtrl (_tms_panel, wxID_ANY);
-       table->Add (_tms_path, 1, wxEXPAND);
-       add_label_to_sizer (table, _tms_panel, _("User name"), true);
-       _tms_user = new wxTextCtrl (_tms_panel, wxID_ANY);
-       table->Add (_tms_user, 1, wxEXPAND);
-       add_label_to_sizer (table, _tms_panel, _("Password"), true);
-       _tms_password = new wxTextCtrl (_tms_panel, wxID_ANY);
-       table->Add (_tms_password, 1, wxEXPAND);
-       Config* config = Config::instance ();
+       void issuer_changed ()
+       {
+               libdcp::XMLMetadata m = Config::instance()->dcp_metadata ();
+               m.issuer = wx_to_std (_issuer->GetValue ());
+               Config::instance()->set_dcp_metadata (m);
+       }
        
-       _tms_ip->SetValue (std_to_wx (config->tms_ip ()));
-       _tms_ip->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::tms_ip_changed, this));
-       _tms_path->SetValue (std_to_wx (config->tms_path ()));
-       _tms_path->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::tms_path_changed, this));
-       _tms_user->SetValue (std_to_wx (config->tms_user ()));
-       _tms_user->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::tms_user_changed, this));
-       _tms_password->SetValue (std_to_wx (config->tms_password ()));
-       _tms_password->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::tms_password_changed, this));
- }
- void
- ConfigDialog::make_metadata_panel ()
- {
-       _metadata_panel = new wxPanel (_notebook);
-       wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-       _metadata_panel->SetSizer (s);
-       wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
-       table->AddGrowableCol (1, 1);
-       s->Add (table, 1, wxALL | wxEXPAND, 8);
-       add_label_to_sizer (table, _metadata_panel, _("Issuer"), true);
-       _issuer = new wxTextCtrl (_metadata_panel, wxID_ANY);
-       table->Add (_issuer, 1, wxEXPAND);
-       add_label_to_sizer (table, _metadata_panel, _("Creator"), true);
-       _creator = new wxTextCtrl (_metadata_panel, wxID_ANY);
-       table->Add (_creator, 1, wxEXPAND);
-       Config* config = Config::instance ();
-       _issuer->SetValue (std_to_wx (config->dcp_metadata().issuer));
-       _issuer->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::issuer_changed, this));
-       _creator->SetValue (std_to_wx (config->dcp_metadata().creator));
-       _creator->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::creator_changed, this));
- }
- static string 
- server_column (string s)
- {
-       return s;
- }
- void
- ConfigDialog::make_servers_panel ()
- {
-       _servers_panel = new wxPanel (_notebook);
-       wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-       _servers_panel->SetSizer (s);
-       _use_any_servers = new wxCheckBox (_servers_panel, wxID_ANY, _("Use all servers"));
-       s->Add (_use_any_servers, 0, wxALL, DCPOMATIC_SIZER_X_GAP);
+       void creator_changed ()
+       {
+               libdcp::XMLMetadata m = Config::instance()->dcp_metadata ();
+               m.creator = wx_to_std (_creator->GetValue ());
+               Config::instance()->set_dcp_metadata (m);
+       }
        
-       vector<string> columns;
-       columns.push_back (wx_to_std (_("IP address / host name")));
-       _servers_list = new EditableList<std::string, ServerDialog> (
-               _servers_panel,
-               columns,
-               boost::bind (&Config::servers, Config::instance()),
-               boost::bind (&Config::set_servers, Config::instance(), _1),
-               boost::bind (&server_column, _1)
-               );
-       s->Add (_servers_list, 1, wxEXPAND | wxALL, DCPOMATIC_SIZER_X_GAP);
-       _use_any_servers->SetValue (Config::instance()->use_any_servers ());
-       _use_any_servers->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&ConfigDialog::use_any_servers_changed, this));
- }
- void
- ConfigDialog::use_any_servers_changed ()
- {
-       Config::instance()->set_use_any_servers (_use_any_servers->GetValue ());
- }
- void
- ConfigDialog::language_changed ()
- {
-       switch (_language->GetSelection ()) {
-       case 0:
-               Config::instance()->set_language ("en");
-               break;
-       case 1:
-               Config::instance()->set_language ("fr");
-               break;
-       case 2:
-               Config::instance()->set_language ("it");
-               break;
-       case 3:
-               Config::instance()->set_language ("es");
-               break;
-       case 4:
-               Config::instance()->set_language ("sv");
-               break;
-       case 5:
-               Config::instance()->set_language ("de");
-               break;
+       wxSpinCtrl* _j2k_bandwidth;
+       wxSpinCtrl* _audio_delay;
+       wxButton* _dci_metadata_button;
+       wxSpinCtrl* _still_length;
+ #ifdef DCPOMATIC_USE_OWN_DIR_PICKER
+       DirPickerCtrl* _directory;
+ #else
+       wxDirPickerCtrl* _directory;
+ #endif
+       wxChoice* _container;
+       wxChoice* _dcp_content_type;
+       wxTextCtrl* _issuer;
+       wxTextCtrl* _creator;
+ };
+ class EncodingServersPage : public wxPreferencesPage, public Page
+ {
+ public:
+       EncodingServersPage (wxSize panel_size, int border)
+               : Page (panel_size, border)
+       {}
+       
+       wxString GetName () const
+       {
+               return _("Servers");
        }
- }
- void
- ConfigDialog::tms_ip_changed ()
- {
-       Config::instance()->set_tms_ip (wx_to_std (_tms_ip->GetValue ()));
- }
- void
- ConfigDialog::tms_path_changed ()
- {
-       Config::instance()->set_tms_path (wx_to_std (_tms_path->GetValue ()));
- }
- void
- ConfigDialog::tms_user_changed ()
- {
-       Config::instance()->set_tms_user (wx_to_std (_tms_user->GetValue ()));
- }
  
- void
- ConfigDialog::tms_password_changed ()
- {
-       Config::instance()->set_tms_password (wx_to_std (_tms_password->GetValue ()));
- }
+ #ifdef DCPOMATIC_OSX  
+       wxBitmap GetLargeIcon () const
+       {
+               return wxBitmap ("servers", wxBITMAP_TYPE_PNG_RESOURCE);
+       }
+ #endif        
  
- void
- ConfigDialog::num_local_encoding_threads_changed ()
- {
-       Config::instance()->set_num_local_encoding_threads (_num_local_encoding_threads->GetValue ());
- }
+       wxWindow* CreateWindow (wxWindow* parent)
+       {
+               wxPanel* panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
+               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
+               panel->SetSizer (s);
+               
+               _use_any_servers = new wxCheckBox (panel, wxID_ANY, _("Use all servers"));
+               s->Add (_use_any_servers, 0, wxALL, _border);
+               
+               vector<string> columns;
+               columns.push_back (wx_to_std (_("IP address / host name")));
+               _servers_list = new EditableList<string, ServerDialog> (
+                       panel,
+                       columns,
+                       boost::bind (&Config::servers, Config::instance()),
+                       boost::bind (&Config::set_servers, Config::instance(), _1),
+                       boost::bind (&EncodingServersPage::server_column, this, _1)
+                       );
+               
+               s->Add (_servers_list, 1, wxEXPAND | wxALL, _border);
+               
+               _use_any_servers->SetValue (Config::instance()->use_any_servers ());
+               _use_any_servers->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&EncodingServersPage::use_any_servers_changed, this));
+               return panel;
+       }
  
- void
- ConfigDialog::default_directory_changed ()
- {
-       Config::instance()->set_default_directory (wx_to_std (_default_directory->GetPath ()));
- }
+ private:      
  
- void
- ConfigDialog::edit_default_dci_metadata_clicked ()
- {
-       DCIMetadataDialog* d = new DCIMetadataDialog (this, Config::instance()->default_dci_metadata ());
-       d->ShowModal ();
-       Config::instance()->set_default_dci_metadata (d->dci_metadata ());
-       d->Destroy ();
- }
- void
- ConfigDialog::set_language_changed ()
- {
-       setup_language_sensitivity ();
-       if (_set_language->GetValue ()) {
-               language_changed ();
-       } else {
-               Config::instance()->unset_language ();
+       void use_any_servers_changed ()
+       {
+               Config::instance()->set_use_any_servers (_use_any_servers->GetValue ());
        }
- }
- void
- ConfigDialog::setup_language_sensitivity ()
- {
-       _language->Enable (_set_language->GetValue ());
- }
  
- void
- ConfigDialog::default_still_length_changed ()
- {
-       Config::instance()->set_default_still_length (_default_still_length->GetValue ());
- }
+       string server_column (string s)
+       {
+               return s;
+       }
  
- void
- ConfigDialog::default_container_changed ()
- {
-       vector<Ratio const *> ratio = Ratio::all ();
-       Config::instance()->set_default_container (ratio[_default_container->GetSelection()]);
- }
+       wxCheckBox* _use_any_servers;
+       EditableList<string, ServerDialog>* _servers_list;
+ };
  
- void
- ConfigDialog::default_dcp_content_type_changed ()
+ class ColourConversionsPage : public wxPreferencesPage, public Page
  {
-       vector<DCPContentType const *> ct = DCPContentType::all ();
-       Config::instance()->set_default_dcp_content_type (ct[_default_dcp_content_type->GetSelection()]);
- }
+ public:
+       ColourConversionsPage (wxSize panel_size, int border)
+               : Page (panel_size, border)
+       {}
+       
+       wxString GetName () const
+       {
+               return _("Colour Conversions");
+       }
  
- void
- ConfigDialog::issuer_changed ()
- {
-       dcp::XMLMetadata m = Config::instance()->dcp_metadata ();
-       m.issuer = wx_to_std (_issuer->GetValue ());
-       Config::instance()->set_dcp_metadata (m);
- }
+ #ifdef DCPOMATIC_OSX  
+       wxBitmap GetLargeIcon () const
+       {
+               return wxBitmap ("colour_conversions", wxBITMAP_TYPE_PNG_RESOURCE);
+       }
+ #endif        
+       wxWindow* CreateWindow (wxWindow* parent)
+       {
+               wxPanel* panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
+               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
+               panel->SetSizer (s);
+               vector<string> columns;
+               columns.push_back (wx_to_std (_("Name")));
+               wxPanel* list = new EditableList<PresetColourConversion, PresetColourConversionDialog> (
+                       panel,
+                       columns,
+                       boost::bind (&Config::colour_conversions, Config::instance()),
+                       boost::bind (&Config::set_colour_conversions, Config::instance(), _1),
+                       boost::bind (&ColourConversionsPage::colour_conversion_column, this, _1),
+                       300
+                       );
+               s->Add (list, 1, wxEXPAND | wxALL, _border);
+               return panel;
+       }
  
- void
- ConfigDialog::creator_changed ()
- {
-       dcp::XMLMetadata m = Config::instance()->dcp_metadata ();
-       m.creator = wx_to_std (_creator->GetValue ());
-       Config::instance()->set_dcp_metadata (m);
- }
+ private:
+       string colour_conversion_column (PresetColourConversion c)
+       {
+               return c.name;
+       }
+ };
  
- void
- ConfigDialog::default_j2k_bandwidth_changed ()
+ class TMSPage : public wxPreferencesPage, public Page
  {
-       Config::instance()->set_default_j2k_bandwidth (_default_j2k_bandwidth->GetValue() * 1000000);
- }
+ public:
+       TMSPage (wxSize panel_size, int border)
+               : Page (panel_size, border)
+       {}
  
- void
- ConfigDialog::default_audio_delay_changed ()
- {
-       Config::instance()->set_default_audio_delay (_default_audio_delay->GetValue());
- }
+       wxString GetName () const
+       {
+               return _("TMS");
+       }
  
- static std::string
- colour_conversion_column (PresetColourConversion c)
- {
-       return c.name;
- }
+ #ifdef DCPOMATIC_OSX  
+       wxBitmap GetLargeIcon () const
+       {
+               return wxBitmap ("tms", wxBITMAP_TYPE_PNG_RESOURCE);
+       }
+ #endif        
  
- void
- ConfigDialog::make_colour_conversions_panel ()
- {
-       vector<string> columns;
-       columns.push_back (wx_to_std (_("Name")));
-       _colour_conversions_panel = new EditableList<PresetColourConversion, PresetColourConversionDialog> (
-               _notebook,
-               columns,
-               boost::bind (&Config::colour_conversions, Config::instance()),
-               boost::bind (&Config::set_colour_conversions, Config::instance(), _1),
-               boost::bind (&colour_conversion_column, _1),
-               300
-               );
- }
+       wxWindow* CreateWindow (wxWindow* parent)
+       {
+               wxPanel* panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size);
+               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
+               panel->SetSizer (s);
+               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+               table->AddGrowableCol (1, 1);
+               s->Add (table, 1, wxALL | wxEXPAND, _border);
+               
+               add_label_to_sizer (table, panel, _("IP address"), true);
+               _tms_ip = new wxTextCtrl (panel, wxID_ANY);
+               table->Add (_tms_ip, 1, wxEXPAND);
+               
+               add_label_to_sizer (table, panel, _("Target path"), true);
+               _tms_path = new wxTextCtrl (panel, wxID_ANY);
+               table->Add (_tms_path, 1, wxEXPAND);
+               
+               add_label_to_sizer (table, panel, _("User name"), true);
+               _tms_user = new wxTextCtrl (panel, wxID_ANY);
+               table->Add (_tms_user, 1, wxEXPAND);
+               
+               add_label_to_sizer (table, panel, _("Password"), true);
+               _tms_password = new wxTextCtrl (panel, wxID_ANY);
+               table->Add (_tms_password, 1, wxEXPAND);
+               
+               Config* config = Config::instance ();
+               
+               _tms_ip->SetValue (std_to_wx (config->tms_ip ()));
+               _tms_ip->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_ip_changed, this));
+               _tms_path->SetValue (std_to_wx (config->tms_path ()));
+               _tms_path->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_path_changed, this));
+               _tms_user->SetValue (std_to_wx (config->tms_user ()));
+               _tms_user->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_user_changed, this));
+               _tms_password->SetValue (std_to_wx (config->tms_password ()));
+               _tms_password->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TMSPage::tms_password_changed, this));
+               return panel;
+       }
  
- void
- ConfigDialog::mail_server_changed ()
- {
-       Config::instance()->set_mail_server (wx_to_std (_mail_server->GetValue ()));
- }
+ private:
+       void tms_ip_changed ()
+       {
+               Config::instance()->set_tms_ip (wx_to_std (_tms_ip->GetValue ()));
+       }
+       
+       void tms_path_changed ()
+       {
+               Config::instance()->set_tms_path (wx_to_std (_tms_path->GetValue ()));
+       }
+       
+       void tms_user_changed ()
+       {
+               Config::instance()->set_tms_user (wx_to_std (_tms_user->GetValue ()));
+       }
+       
+       void tms_password_changed ()
+       {
+               Config::instance()->set_tms_password (wx_to_std (_tms_password->GetValue ()));
+       }
  
- void
- ConfigDialog::mail_user_changed ()
- {
-       Config::instance()->set_mail_user (wx_to_std (_mail_user->GetValue ()));
- }
+       wxTextCtrl* _tms_ip;
+       wxTextCtrl* _tms_path;
+       wxTextCtrl* _tms_user;
+       wxTextCtrl* _tms_password;
+ };
  
- void
- ConfigDialog::mail_password_changed ()
+ class KDMEmailPage : public wxPreferencesPage, public Page
  {
-       Config::instance()->set_mail_password (wx_to_std (_mail_password->GetValue ()));
- }
+ public:
  
- void
- ConfigDialog::kdm_from_changed ()
- {
-       Config::instance()->set_kdm_from (wx_to_std (_kdm_from->GetValue ()));
- }
+       KDMEmailPage (wxSize panel_size, int border)
+               : Page (panel_size, border)
+       {}
+       
+       wxString GetName () const
+       {
+               return _("KDM Email");
+       }
  
- void
- ConfigDialog::make_kdm_email_panel ()
- {
-       _kdm_email_panel = new wxPanel (_notebook);
-       wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
-       _kdm_email_panel->SetSizer (s);
+ #ifdef DCPOMATIC_OSX  
+       wxBitmap GetLargeIcon () const
      {
+               return wxBitmap ("kdm_email", wxBITMAP_TYPE_PNG_RESOURCE);
+       }
+ #endif        
  
-       _kdm_email = new wxTextCtrl (_kdm_email_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE);
-       s->Add (_kdm_email, 1, wxEXPAND | wxALL, 12);
+       wxWindow* CreateWindow (wxWindow* parent)
+       {
+               /* We have to force both width and height of this one */
+ #ifdef DCPOMATIC_OSX
+               wxPanel* panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, wxSize (480, 128));
+ #else         
+               wxPanel* panel = new wxPanel (parent);
+ #endif                
+               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
+               panel->SetSizer (s);
+               
+               _kdm_email = new wxTextCtrl (panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (480, 128), wxTE_MULTILINE);
+               s->Add (_kdm_email, 1, wxEXPAND | wxALL, _border);
+               
+               _kdm_email->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&KDMEmailPage::kdm_email_changed, this));
+               _kdm_email->SetValue (wx_to_std (Config::instance()->kdm_email ()));
+               return panel;
+       }
  
-       _kdm_email->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::kdm_email_changed, this));
-       _kdm_email->SetValue (wx_to_std (Config::instance()->kdm_email ()));
- }
+ private:      
+       void kdm_email_changed ()
+       {
+               Config::instance()->set_kdm_email (wx_to_std (_kdm_email->GetValue ()));
+       }
  
- void
- ConfigDialog::kdm_email_changed ()
- {
-       Config::instance()->set_kdm_email (wx_to_std (_kdm_email->GetValue ()));
- }
+       wxTextCtrl* _kdm_email;
+ };
  
- void
ConfigDialog::check_for_updates_changed ()
+ wxPreferencesEditor*
create_config_dialog ()
  {
-       Config::instance()->set_check_for_updates (_check_for_updates->GetValue ());
- }
+       wxPreferencesEditor* e = new wxPreferencesEditor ();
  
- void
- ConfigDialog::check_for_test_updates_changed ()
- {
-       Config::instance()->set_check_for_test_updates (_check_for_test_updates->GetValue ());
+ #ifdef DCPOMATIC_OSX
+       /* Width that we force some of the config panels to be on OSX so that
+          the containing window doesn't shrink too much when we select those panels.
+          This is obviously an unpleasant hack.
+       */
+       wxSize ps = wxSize (480, -1);
+       int const border = 16;
+ #else
+       wxSize ps = wxDefaultSize;
+       int const border = 8;
+ #endif
+       
+       e->AddPage (new GeneralPage (ps, border));
+       e->AddPage (new DefaultsPage (ps, border));
+       e->AddPage (new EncodingServersPage (ps, border));
+       e->AddPage (new ColourConversionsPage (ps, border));
+       e->AddPage (new TMSPage (ps, border));
+       e->AddPage (new KDMEmailPage (ps, border));
+       return e;
  }
diff --combined src/wx/film_viewer.cc
index f426a7c6e9f86c77764b6274586abcb2760655d1,45372d8111f532495d82cd56409aaa22773af1a1..7e1b618119eec5367b0374fa09239886a3f12181
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
@@@ -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"
  
@@@ -51,7 -50,7 +51,7 @@@ using std::make_pair
  using boost::shared_ptr;
  using boost::dynamic_pointer_cast;
  using boost::weak_ptr;
 -using libdcp::Size;
 +using dcp::Size;
  
  FilmViewer::FilmViewer (shared_ptr<Film> f, wxWindow* p)
        : wxPanel (p)
@@@ -122,7 -121,7 +122,7 @@@ FilmViewer::set_film (shared_ptr<Film> 
        _frame.reset ();
        
        _slider->SetValue (0);
 -      set_position_text (0);
 +      set_position_text (DCPTime ());
        
        if (!_film) {
                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));
  
@@@ -174,10 -172,10 +174,10 @@@ 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;
 +      if (len.get ()) {
 +              int const new_slider_position = 4096 * _player->video_position().get() / len.get();
                if (new_slider_position != _slider->GetValue()) {
                        _slider->SetValue (new_slider_position);
                }
@@@ -221,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 (DCPTime (_film->length().get() * _slider->GetValue() / 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.
 +                      */
 +              }
        }
  }
  
@@@ -267,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);
  }
  
@@@ -298,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");
                
        double const fps = _film->video_frame_rate ();
        /* Count frame number from 1 ... not sure if this is the best idea */
 -      _frame_number->SetLabel (wxString::Format (wxT("%d"), int (rint (t * fps / TIME_HZ)) + 1));
 +      _frame_number->SetLabel (wxString::Format (wxT("%d"), int (rint (t.seconds() * fps)) + 1));
        
 -      double w = static_cast<double>(t) / TIME_HZ;
 +      double w = t.seconds ();
        int const h = (w / 3600);
        w -= h * 3600;
        int const m = (w / 60);
@@@ -398,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);
 -      if (p < 0) {
 -              p = 0;
 +      DCPTime p = _player->video_position() - DCPTime::from_frames (2, _film->video_frame_rate ());
 +      if (p < DCPTime ()) {
 +              p = DCPTime ();
        }
        
 -      _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
@@@ -429,7 -404,7 +429,7 @@@ FilmViewer::player_changed (bool freque
        if (frequent) {
                return;
        }
-       
        calculate_sizes ();
        fetch_current_frame_again ();
  }
diff --combined src/wx/video_panel.cc
index eb45d4bc81a0b82ec38c35e342866c1da0cb7432,38604248cb759440160eea127a5ba762420d3df7..533545f64406932be8cc00737646f1d8f9c7d2cf
@@@ -240,12 -240,11 +240,11 @@@ VideoPanel::film_content_changed (int p
                _colour_conversion->SetLabel (preset ? std_to_wx (cc[preset.get()].name) : _("Custom"));
        } else if (property == FFmpegContentProperty::FILTERS) {
                if (fcs) {
-                       pair<string, string> p = Filter::ffmpeg_strings (fcs->filters ());
-                       if (p.first.empty () && p.second.empty ()) {
+                       string const p = Filter::ffmpeg_string (fcs->filters ());
+                       if (p.empty ()) {
                                _filters->SetLabel (_("None"));
                        } else {
-                               string const b = p.first + " " + p.second;
-                               _filters->SetLabel (std_to_wx (b));
+                               _filters->SetLabel (std_to_wx (p));
                        }
                }
        }
@@@ -295,8 -294,8 +294,8 @@@ VideoPanel::setup_description (
        }
  
        Crop const crop = vcs->crop ();
 -      if ((crop.left || crop.right || crop.top || crop.bottom) && vcs->video_size() != libdcp::Size (0, 0)) {
 -              libdcp::Size cropped = vcs->video_size_after_crop ();
 +      if ((crop.left || crop.right || crop.top || crop.bottom) && vcs->video_size() != dcp::Size (0, 0)) {
 +              dcp::Size cropped = vcs->video_size_after_crop ();
                d << wxString::Format (
                        _("Cropped to %dx%d (%.2f:1)\n"),
                        cropped.width, cropped.height,
                ++lines;
        }
  
-       dcp::Size const container_size = fit_ratio_within (_editor->film()->container()->ratio (), _editor->film()->full_frame ());
-       dcp::Size const scaled = vcs->scale().size (vcs, container_size);
 -      libdcp::Size const container_size = _editor->film()->frame_size ();
 -      libdcp::Size const scaled = vcs->scale().size (vcs, container_size, container_size);
++      dcp::Size const container_size = _editor->film()->frame_size ();
++      dcp::Size const scaled = vcs->scale().size (vcs, container_size, container_size);
  
        if (scaled != vcs->video_size_after_crop ()) {
                d << wxString::Format (
  
        d << wxString::Format (_("Content frame rate %.4f\n"), vcs->video_frame_rate ());
        ++lines;
 -      FrameRateConversion frc (vcs->video_frame_rate(), _editor->film()->video_frame_rate ());
 +      FrameRateChange frc (vcs->video_frame_rate(), _editor->film()->video_frame_rate ());
        d << std_to_wx (frc.description) << "\n";
        ++lines;
  
diff --combined wscript
index b66f793e53f140f57ace170b64281f8cf12d3183,aa20e99463b12d32d5548d85d067999ca2a790d8..67f1033c595de95ca845e21bd4cc561686201381
+++ b/wscript
@@@ -3,7 -3,7 +3,7 @@@ import o
  import sys
  
  APPNAME = 'dcpomatic'
 -VERSION = '1.66.1devel'
 +VERSION = '2.0.0devel'
  
  def options(opt):
      opt.load('compiler_cxx')
@@@ -33,8 -33,6 +33,6 @@@ def static_ffmpeg(conf)
      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)
@@@ -43,7 -41,6 +41,6 @@@
      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)
@@@ -55,9 -52,9 +52,9 @@@ def dynamic_openjpeg(conf)
      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.check_cfg(package='libdcp-1.0', 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.STLIB_DCP = ['dcp-1.0', 'asdcp-libdcp', 'kumu-libdcp']
      conf.env.LIB_DCP = ['glibmm-2.4', 'ssl', 'crypto', 'bz2', 'xslt']
  
      if static_boost:
@@@ -81,7 -78,7 +78,7 @@@
          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.check_cfg(package='libdcp-1.0', 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):
@@@ -302,8 -299,6 +299,8 @@@ def configure(conf)
      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>