Merge master.
authorCarl Hetherington <cth@carlh.net>
Sat, 23 Aug 2014 21:50:40 +0000 (22:50 +0100)
committerCarl Hetherington <cth@carlh.net>
Sat, 23 Aug 2014 21:50:40 +0000 (22:50 +0100)
30 files changed:
1  2 
ChangeLog
debian/changelog
src/lib/analyse_audio_job.cc
src/lib/config.cc
src/lib/content.cc
src/lib/dcp_video.cc
src/lib/ffmpeg_content.cc
src/lib/ffmpeg_decoder.cc
src/lib/ffmpeg_examiner.cc
src/lib/film.cc
src/lib/filter_graph.cc
src/lib/image_content.cc
src/lib/job.cc
src/lib/kdm.cc
src/lib/server.cc
src/lib/server_finder.cc
src/lib/sndfile_content.cc
src/lib/transcode_job.cc
src/lib/update.cc
src/lib/util.cc
src/lib/video_content.cc
src/lib/wscript
src/tools/dcpomatic.cc
src/tools/dcpomatic_kdm.cc
src/tools/dcpomatic_server_cli.cc
src/wx/about_dialog.cc
src/wx/film_editor.cc
src/wx/properties_dialog.cc
src/wx/timing_panel.cc
test/film_metadata_test.cc

diff --combined ChangeLog
index 23ccae1902d6f8189f305fa33f221f08f27353ae,8b0a5cd3e990b7cdcdc2410db8cfedadc78cd98b..e100fd23e466e0c395a50fe5168e4ada053915d3
+++ b/ChangeLog
@@@ -1,21 -1,24 +1,43 @@@
 +2014-08-06  Carl Hetherington  <cth@carlh.net>
 +
 +      * Version 2.0.1 released.
 +
 +2014-07-15  Carl Hetherington  <cth@carlh.net>
 +
 +      * A variety of changes were made on the 2.0 branch
 +      but not documented in the ChangeLog.  Most sigificantly:
 +
 +      - DCP import
 +      - Creation of DCPs with proper XML subtitles
 +      - Import of .srt and .xml subtitles
 +      - Audio processing framework (with some basic processors).
 +
 +2014-03-07  Carl Hetherington  <cth@carlh.net>
 +
 +      * Add subtitle view.
 +
+ 2014-08-23  Carl Hetherington  <cth@carlh.net>
+       * Version 1.72.12 released.
+ 2014-08-23  Carl Hetherington  <cth@carlh.net>
+       * Revert previous use of AVFormatContext::start_time when
+       computing the length of video.  I think this is wrong, and
+       causes bits to be missed off the end of videos (and other
+       problems).
+ 2014-08-20  Carl Hetherington  <cth@carlh.net>
+       * Version 1.72.11 released.
+ 2014-08-19  Carl Hetherington  <cth@carlh.net>
+       * Attempt to fix random crashes on OS X (especially during encodes)
+       thought to be caused by multiple threads using (different) stringstreams
+       at the same time; see src/lib/safe_stringstream.
++>>>>>>> origin/master
  2014-08-09  Carl Hetherington  <cth@carlh.net>
  
        * Version 1.72.10 released.
  2014-07-10  Carl Hetherington  <cth@carlh.net>
  
        * Version 1.72.2 released.
 +>>>>>>> origin/master
  
  2014-07-10  Carl Hetherington  <cth@carlh.net>
  
diff --combined debian/changelog
index 1da8a1f91707c709a48beaa08630d3c38924a409,f5d36b9dfd9555b521d03aecc23b92620e702b61..09d40178298a9256211e2b1504703a01fb37445c
@@@ -1,4 -1,4 +1,4 @@@
 -dcpomatic (1.72.12-1) UNRELEASED; urgency=low
 +dcpomatic (2.0.1-1) UNRELEASED; urgency=low
  
    * New upstream release.
    * New upstream release.
    * New upstream release.
    * New upstream release.
    * New upstream release.
- <<<<<<< HEAD
 -  * New upstream release.
 -  * New upstream release.
 -  * New upstream release.
 -  * New upstream release.
  
 - -- Carl Hetherington <carl@d1stkfactory>  Sat, 23 Aug 2014 18:47:52 +0100
 + -- Carl Hetherington <carl@dalglish>  Wed, 06 Aug 2014 18:59:35 +0100
- =======
-   * New upstream release.
-   * New upstream release.
-  -- Carl Hetherington <carl@d1stkfactory>  Sat, 09 Aug 2014 12:38:18 +0100
- >>>>>>> origin/master
  
  dcpomatic (0.87-1) UNRELEASED; urgency=low
  
index 347cc0a0fb362f95b55686ad3fb76744e746ff8f,ab985bdf75468ee557a81f0307d09a919df390e0..60b10e7b6e05ba69dc418894fa10b4ca8ef7781e
@@@ -18,7 -18,6 +18,7 @@@
  */
  
  #include "audio_analysis.h"
 +#include "audio_buffers.h"
  #include "analyse_audio_job.h"
  #include "compose.hpp"
  #include "film.h"
@@@ -60,18 -59,19 +60,18 @@@ AnalyseAudioJob::run (
        shared_ptr<Playlist> playlist (new Playlist);
        playlist->add (content);
        shared_ptr<Player> player (new Player (_film, playlist));
 -      player->disable_video ();
        
 -      player->Audio.connect (bind (&AnalyseAudioJob::audio, this, _1, _2));
 -
 -      _samples_per_point = max (int64_t (1), _film->time_to_audio_frames (_film->length()) / _num_points);
 +      int64_t const len = _film->length().frames (_film->audio_frame_rate());
 +      _samples_per_point = max (int64_t (1), len / _num_points);
  
        _current.resize (_film->audio_channels ());
        _analysis.reset (new AudioAnalysis (_film->audio_channels ()));
  
        _done = 0;
 -      OutputAudioFrame const len = _film->time_to_audio_frames (_film->length ());
 -      while (!player->pass ()) {
 -              set_progress (double (_done) / len);
 +      DCPTime const block = DCPTime::from_seconds (1.0 / 8);
 +      for (DCPTime t; t < _film->length(); t += block) {
 +              analyse (player->get_audio (t, block, false));
 +              set_progress (t.seconds() / _film->length().seconds());
        }
  
        _analysis->write (content->audio_analysis_path ());
  }
  
  void
 -AnalyseAudioJob::audio (shared_ptr<const AudioBuffers> b, Time)
 +AnalyseAudioJob::analyse (shared_ptr<const AudioBuffers> b)
  {
        for (int i = 0; i < b->frames(); ++i) {
                for (int j = 0; j < b->channels(); ++j) {
                        float s = b->data(j)[i];
                        if (fabsf (s) < 10e-7) {
-                               /* stringstream can't serialise and recover inf or -inf, so prevent such
+                               /* SafeStringStream can't serialise and recover inf or -inf, so prevent such
                                   values by replacing with this (140dB down) */
                                s = 10e-7;
                        }
diff --combined src/lib/config.cc
index 14539044d2eb53b11f2465ea4e753e75a0ab1f93,d11bcf983f5365a872027570d840b1c5ff92dc36..4f2a75574414bafa2b13129afe478f2df740edc9
  
  */
  
- #include <sstream>
  #include <cstdlib>
  #include <fstream>
  #include <glib.h>
  #include <boost/filesystem.hpp>
  #include <boost/algorithm/string.hpp>
 -#include <libdcp/colour_matrix.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/colour_matrix.h>
 +#include <dcp/raw_convert.h>
 +#include <dcp/signer.h>
 +#include <dcp/certificate_chain.h>
  #include <libcxml/cxml.h>
  #include "config.h"
  #include "server.h"
  #include "filter.h"
  #include "ratio.h"
  #include "dcp_content_type.h"
 -#include "sound_processor.h"
 +#include "cinema_sound_processor.h"
  #include "colour_conversion.h"
  #include "cinema.h"
  #include "util.h"
 +#include "cross.h"
  
  #include "i18n.h"
  
@@@ -53,7 -49,7 +52,7 @@@ using boost::shared_ptr
  using boost::optional;
  using boost::algorithm::is_any_of;
  using boost::algorithm::split;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  Config* Config::_instance = 0;
  
@@@ -63,7 -59,7 +62,7 @@@ Config::Config (
        , _server_port_base (6192)
        , _use_any_servers (true)
        , _tms_path (".")
 -      , _sound_processor (SoundProcessor::from_id (N_("dolby_cp750")))
 +      , _cinema_sound_processor (CinemaSoundProcessor::from_id (N_("dolby_cp750")))
        , _allow_any_dcp_frame_rate (false)
        , _default_still_length (10)
        , _default_scale (Ratio::from_id ("185"))
@@@ -83,9 -79,9 +82,9 @@@
        _allowed_dcp_frame_rates.push_back (50);
        _allowed_dcp_frame_rates.push_back (60);
  
 -      _colour_conversions.push_back (PresetColourConversion (_("sRGB"), 2.4, true, libdcp::colour_matrix::srgb_to_xyz, 2.6));
 -      _colour_conversions.push_back (PresetColourConversion (_("sRGB non-linearised"), 2.4, false, libdcp::colour_matrix::srgb_to_xyz, 2.6));
 -      _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, libdcp::colour_matrix::rec709_to_xyz, 2.6));
 +      _colour_conversions.push_back (PresetColourConversion (_("sRGB"), 2.4, true, dcp::colour_matrix::srgb_to_xyz, 2.6));
 +      _colour_conversions.push_back (PresetColourConversion (_("sRGB non-linearised"), 2.4, false, dcp::colour_matrix::srgb_to_xyz, 2.6));
 +      _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, dcp::colour_matrix::rec709_to_xyz, 2.6));
  
        reset_kdm_email ();
  }
@@@ -94,6 -90,7 +93,6 @@@ voi
  Config::read ()
  {
        if (!boost::filesystem::exists (file (false))) {
 -              read_old_metadata ();
                return;
        }
  
  
        c = f.optional_string_child ("SoundProcessor");
        if (c) {
 -              _sound_processor = SoundProcessor::from_id (c.get ());
 +              _cinema_sound_processor = CinemaSoundProcessor::from_id (c.get ());
 +      }
 +      c = f.optional_string_child ("CinemaSoundProcessor");
 +      if (c) {
 +              _cinema_sound_processor = CinemaSoundProcessor::from_id (c.get ());
        }
  
        _language = f.optional_string_child ("Language");
                /* Loading version 0 (before Rec. 709 was added as a preset).
                   Add it in.
                */
 -              _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, libdcp::colour_matrix::rec709_to_xyz, 2.6));
 +              _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, dcp::colour_matrix::rec709_to_xyz, 2.6));
        }
  
        list<cxml::NodePtr> cin = f.node_children ("Cinema");
        _allow_any_dcp_frame_rate = f.optional_bool_child ("AllowAnyDCPFrameRate");
  
        _log_types = f.optional_number_child<int> ("LogTypes").get_value_or (Log::TYPE_GENERAL | Log::TYPE_WARNING | Log::TYPE_ERROR);
 -}
  
 -void
 -Config::read_old_metadata ()
 -{
 -      /* XXX: this won't work with non-Latin filenames */
 -      ifstream f (file(true).string().c_str ());
 -      string line;
 -
 -      while (getline (f, line)) {
 -              if (line.empty ()) {
 -                      continue;
 +      cxml::NodePtr signer = f.optional_node_child ("Signer");
 +      dcp::CertificateChain signer_chain;
 +      if (signer) {
 +              /* Read the signing certificates and private key in from the config file */
 +              list<cxml::NodePtr> certificates = signer->node_children ("Certificate");
 +              for (list<cxml::NodePtr>::const_iterator i = certificates.begin(); i != certificates.end(); ++i) {
 +                      signer_chain.add (dcp::Certificate ((*i)->content ()));
                }
  
 -              if (line[0] == '#') {
 -                      continue;
 -              }
 +              _signer.reset (new dcp::Signer (signer_chain, signer->string_child ("PrivateKey")));
 +      } else {
 +              /* Make a new set of signing certificates and key */
 +              _signer.reset (new dcp::Signer (openssl_path ()));
 +      }
  
 -              size_t const s = line.find (' ');
 -              if (s == string::npos) {
 -                      continue;
 -              }
 -              
 -              string const k = line.substr (0, s);
 -              string const v = line.substr (s + 1);
 -
 -              if (k == N_("num_local_encoding_threads")) {
 -                      _num_local_encoding_threads = atoi (v.c_str ());
 -              } else if (k == N_("default_directory")) {
 -                      _default_directory = v;
 -              } else if (k == N_("server_port")) {
 -                      _server_port_base = atoi (v.c_str ());
 -              } else if (k == N_("server")) {
 -                      vector<string> b;
 -                      split (b, v, is_any_of (" "));
 -                      if (b.size() == 2) {
 -                              _servers.push_back (b[0]);
 -                      }
 -              } else if (k == N_("tms_ip")) {
 -                      _tms_ip = v;
 -              } else if (k == N_("tms_path")) {
 -                      _tms_path = v;
 -              } else if (k == N_("tms_user")) {
 -                      _tms_user = v;
 -              } else if (k == N_("tms_password")) {
 -                      _tms_password = v;
 -              } else if (k == N_("sound_processor")) {
 -                      _sound_processor = SoundProcessor::from_id (v);
 -              } else if (k == "language") {
 -                      _language = v;
 -              } else if (k == "default_container") {
 -                      _default_container = Ratio::from_id (v);
 -              } else if (k == "default_dcp_content_type") {
 -                      _default_dcp_content_type = DCPContentType::from_isdcf_name (v);
 -              } else if (k == "dcp_metadata_issuer") {
 -                      _dcp_metadata.issuer = v;
 -              } else if (k == "dcp_metadata_creator") {
 -                      _dcp_metadata.creator = v;
 -              } else if (k == "dcp_metadata_issue_date") {
 -                      _dcp_metadata.issue_date = v;
 -              }
 +      if (f.optional_string_child ("DecryptionCertificate")) {
 +              _decryption_certificate = dcp::Certificate (f.string_child ("DecryptionCertificate"));
 +      }
 +
 +      if (f.optional_string_child ("DecryptionPrivateKey")) {
 +              _decryption_private_key = f.string_child ("DecryptionPrivateKey");
 +      }
  
 -              _default_isdcf_metadata.read_old_metadata (k, v);
 +      if (!f.optional_string_child ("DecryptionCertificate") || !f.optional_string_child ("DecryptionPrivateKey")) {
 +              /* Generate our own decryption certificate and key if either is not present in config */
 +              boost::filesystem::path p = dcp::make_certificate_chain (openssl_path ());
 +              _decryption_certificate = dcp::Certificate (dcp::file_to_string (p / "leaf.signed.pem"));
 +              _decryption_private_key = dcp::file_to_string (p / "leaf.key");
 +              boost::filesystem::remove_all (p);
        }
  }
  
@@@ -262,6 -288,17 +261,6 @@@ Config::file (bool old) cons
        return p;
  }
  
 -boost::filesystem::path
 -Config::signer_chain_directory () const
 -{
 -      boost::filesystem::path p;
 -      p /= g_get_user_config_dir ();
 -      p /= "dcpomatic";
 -      p /= "crypt";
 -      boost::filesystem::create_directories (p);
 -      return p;
 -}
 -
  /** @return Singleton instance */
  Config *
  Config::instance ()
@@@ -304,8 -341,8 +303,8 @@@ Config::write () cons
        root->add_child("TMSPath")->add_child_text (_tms_path);
        root->add_child("TMSUser")->add_child_text (_tms_user);
        root->add_child("TMSPassword")->add_child_text (_tms_password);
 -      if (_sound_processor) {
 -              root->add_child("SoundProcessor")->add_child_text (_sound_processor->id ());
 +      if (_cinema_sound_processor) {
 +              root->add_child("CinemaSoundProcessor")->add_child_text (_cinema_sound_processor->id ());
        }
        if (_language) {
                root->add_child("Language")->add_child_text (_language.get());
        root->add_child("MaximumJ2KBandwidth")->add_child_text (raw_convert<string> (_maximum_j2k_bandwidth));
        root->add_child("AllowAnyDCPFrameRate")->add_child_text (_allow_any_dcp_frame_rate ? "1" : "0");
        root->add_child("LogTypes")->add_child_text (raw_convert<string> (_log_types));
 -      
 +
 +      xmlpp::Element* signer = root->add_child ("Signer");
 +      dcp::CertificateChain::List certs = _signer->certificates().root_to_leaf ();
 +      for (dcp::CertificateChain::List::const_iterator i = certs.begin(); i != certs.end(); ++i) {
 +              signer->add_child("Certificate")->add_child_text (i->certificate (true));
 +      }
 +      signer->add_child("PrivateKey")->add_child_text (_signer->key ());
 +
 +      root->add_child("DecryptionCertificate")->add_child_text (_decryption_certificate.certificate (true));
 +      root->add_child("DecryptionPrivateKey")->add_child_text (_decryption_private_key);
 +
        doc.write_to_file_formatted (file(false).string ());
  }
  
diff --combined src/lib/content.cc
index bbbe9b6ce4bdd4dd5ff6b8dcf231c8d08126e253,11a4b21cca3026706911cb0c51ea9230eabc8797..21e49a2c955ae7e74091ec5c1a43f4e9e238b857
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
 +    Copyright (C) 2013-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
  
  */
  
 +/** @file  src/lib/content.cc
 + *  @brief Content class.
 + */
 +
  #include <boost/thread/mutex.hpp>
  #include <libxml++/libxml++.h>
  #include <libcxml/cxml.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "content.h"
  #include "util.h"
  #include "content_factory.h"
  #include "ui_signaller.h"
  #include "exceptions.h"
  #include "film.h"
+ #include "safe_stringstream.h"
  
  #include "i18n.h"
  
  using std::string;
- using std::stringstream;
  using std::set;
  using std::list;
  using std::cout;
  using std::vector;
  using std::max;
  using boost::shared_ptr;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  int const ContentProperty::PATH = 400;
  int const ContentProperty::POSITION = 401;
@@@ -60,7 -56,7 +60,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)
@@@ -80,7 -76,7 +80,7 @@@ Content::Content (shared_ptr<const Film
        _paths.push_back (p);
  }
  
 -Content::Content (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node)
 +Content::Content (shared_ptr<const Film> f, cxml::ConstNodePtr node)
        : _film (f)
        , _change_signals_frequent (false)
  {
@@@ -89,9 -85,9 +89,9 @@@
                _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."));
                }
  
@@@ -125,9 -121,9 +125,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 (raw_convert<string> (_position));
 -      node->add_child("TrimStart")->add_child_text (raw_convert<string> (_trim_start));
 -      node->add_child("TrimEnd")->add_child_text (raw_convert<string> (_trim_end));
 +      node->add_child("Position")->add_child_text (raw_convert<string> (_position.get ()));
 +      node->add_child("TrimStart")->add_child_text (raw_convert<string> (_trim_start.get ()));
 +      node->add_child("TrimEnd")->add_child_text (raw_convert<string> (_trim_end.get ()));
  }
  
  void
@@@ -152,7 -148,7 +152,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);
@@@ -210,13 -206,22 +210,13 @@@ Content::clone () cons
  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 max (int64_t (0), 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 max (DCPTime (), full_length() - trim_start() - trim_end());
  }
  
  /** @return string which includes everything about how this content affects
  string
  Content::identifier () const
  {
-       stringstream s;
+       SafeStringStream s;
        
        s << Content::digest()
 -        << "_" << position()
 -        << "_" << trim_start()
 -        << "_" << trim_end();
 +        << "_" << position().get()
 +        << "_" << trim_start().get()
 +        << "_" << trim_end().get();
  
        return s.str ();
  }
diff --combined src/lib/dcp_video.cc
index 9b1c8c33eae12cf36d214cbd81b84af99e9e7ae8,0000000000000000000000000000000000000000..d849866512d4cdc0e983ca42201631fe3fe88d70
mode 100644,000000..100644
--- /dev/null
@@@ -1,320 -1,0 +1,317 @@@
- #include <sstream>
 +/*
 +    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
 +    Taken from code Copyright (C) 2010-2011 Terrence Meiczinger
 +
 +    This program is free software; you can redistribute it and/or modify
 +    it under the terms of the GNU General Public License as published by
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +/** @file  src/dcp_video_frame.cc
 + *  @brief A single frame of video destined for a DCP.
 + *
 + *  Given an Image and some settings, this class knows how to encode
 + *  the image to J2K either on the local host or on a remote server.
 + *
 + *  Objects of this class are used for the queue that we keep
 + *  of images that require encoding.
 + */
 +
 +#include <stdint.h>
 +#include <cstring>
 +#include <cstdlib>
 +#include <stdexcept>
 +#include <cstdio>
 +#include <iomanip>
- using std::stringstream;
 +#include <iostream>
 +#include <fstream>
 +#include <unistd.h>
 +#include <errno.h>
 +#include <boost/array.hpp>
 +#include <boost/asio.hpp>
 +#include <boost/filesystem.hpp>
 +#include <boost/lexical_cast.hpp>
 +#include <dcp/gamma_lut.h>
 +#include <dcp/xyz_frame.h>
 +#include <dcp/rgb_xyz.h>
 +#include <dcp/colour_matrix.h>
 +#include <dcp/raw_convert.h>
 +#include <libcxml/cxml.h>
 +#include "film.h"
 +#include "dcp_video.h"
 +#include "config.h"
 +#include "exceptions.h"
 +#include "server.h"
 +#include "util.h"
 +#include "scaler.h"
 +#include "image.h"
 +#include "log.h"
 +#include "cross.h"
 +#include "player_video.h"
 +#include "encoded_data.h"
 +
 +#define LOG_GENERAL(...) _log->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
 +
 +#include "i18n.h"
 +
 +using std::string;
-       stringstream xml;
-       doc.write_to_stream (xml, "UTF-8");
-       socket->write (xml.str().length() + 1);
-       socket->write ((uint8_t *) xml.str().c_str(), xml.str().length() + 1);
 +using std::cout;
 +using boost::shared_ptr;
 +using boost::lexical_cast;
 +using dcp::Size;
 +using dcp::raw_convert;
 +
 +#define DCI_COEFFICENT (48.0 / 52.37)
 +
 +/** Construct a DCP video frame.
 + *  @param frame Input frame.
 + *  @param index Index of the frame within the DCP.
 + *  @param bw J2K bandwidth to use (see Config::j2k_bandwidth ())
 + *  @param l Log to write to.
 + */
 +DCPVideo::DCPVideo (
 +      shared_ptr<const PlayerVideo> frame, int index, int dcp_fps, int bw, Resolution r, bool b, shared_ptr<Log> l
 +      )
 +      : _frame (frame)
 +      , _index (index)
 +      , _frames_per_second (dcp_fps)
 +      , _j2k_bandwidth (bw)
 +      , _resolution (r)
 +      , _burn_subtitles (b)
 +      , _log (l)
 +{
 +      
 +}
 +
 +DCPVideo::DCPVideo (shared_ptr<const PlayerVideo> frame, shared_ptr<const cxml::Node> node, shared_ptr<Log> log)
 +      : _frame (frame)
 +      , _log (log)
 +{
 +      _index = node->number_child<int> ("Index");
 +      _frames_per_second = node->number_child<int> ("FramesPerSecond");
 +      _j2k_bandwidth = node->number_child<int> ("J2KBandwidth");
 +      _resolution = Resolution (node->optional_number_child<int>("Resolution").get_value_or (RESOLUTION_2K));
 +      _burn_subtitles = node->bool_child ("BurnSubtitles");
 +}
 +
 +/** J2K-encode this frame on the local host.
 + *  @return Encoded data.
 + */
 +shared_ptr<EncodedData>
 +DCPVideo::encode_locally ()
 +{
 +      shared_ptr<dcp::GammaLUT> in_lut = dcp::GammaLUT::cache.get (
 +              12, _frame->colour_conversion().input_gamma, _frame->colour_conversion().input_gamma_linearised
 +              );
 +      
 +      /* XXX: libdcp should probably use boost */
 +      
 +      double matrix[3][3];
 +      for (int i = 0; i < 3; ++i) {
 +              for (int j = 0; j < 3; ++j) {
 +                      matrix[i][j] = _frame->colour_conversion().matrix (i, j);
 +              }
 +      }
 +
 +      shared_ptr<dcp::XYZFrame> xyz = dcp::rgb_to_xyz (
 +              _frame->image (_burn_subtitles),
 +              in_lut,
 +              dcp::GammaLUT::cache.get (16, 1 / _frame->colour_conversion().output_gamma, false),
 +              matrix
 +              );
 +
 +      /* Set the max image and component sizes based on frame_rate */
 +      int max_cs_len = ((float) _j2k_bandwidth) / 8 / _frames_per_second;
 +      if (_frame->eyes() == EYES_LEFT || _frame->eyes() == EYES_RIGHT) {
 +              /* In 3D we have only half the normal bandwidth per eye */
 +              max_cs_len /= 2;
 +      }
 +      int const max_comp_size = max_cs_len / 1.25;
 +
 +      /* get a J2K compressor handle */
 +      opj_cinfo_t* cinfo = opj_create_compress (CODEC_J2K);
 +      if (cinfo == 0) {
 +              throw EncodeError (N_("could not create JPEG2000 encoder"));
 +      }
 +
 +      /* Set encoding parameters to default values */
 +      opj_cparameters_t parameters;
 +      opj_set_default_encoder_parameters (&parameters);
 +
 +      /* Set default cinema parameters */
 +      parameters.tile_size_on = false;
 +      parameters.cp_tdx = 1;
 +      parameters.cp_tdy = 1;
 +      
 +      /* Tile part */
 +      parameters.tp_flag = 'C';
 +      parameters.tp_on = 1;
 +      
 +      /* Tile and Image shall be at (0,0) */
 +      parameters.cp_tx0 = 0;
 +      parameters.cp_ty0 = 0;
 +      parameters.image_offset_x0 = 0;
 +      parameters.image_offset_y0 = 0;
 +
 +      /* Codeblock size = 32x32 */
 +      parameters.cblockw_init = 32;
 +      parameters.cblockh_init = 32;
 +      parameters.csty |= 0x01;
 +      
 +      /* The progression order shall be CPRL */
 +      parameters.prog_order = CPRL;
 +      
 +      /* No ROI */
 +      parameters.roi_compno = -1;
 +      
 +      parameters.subsampling_dx = 1;
 +      parameters.subsampling_dy = 1;
 +      
 +      /* 9-7 transform */
 +      parameters.irreversible = 1;
 +      
 +      parameters.tcp_rates[0] = 0;
 +      parameters.tcp_numlayers++;
 +      parameters.cp_disto_alloc = 1;
 +      parameters.cp_rsiz = _resolution == RESOLUTION_2K ? CINEMA2K : CINEMA4K;
 +      if (_resolution == RESOLUTION_4K) {
 +              parameters.numpocs = 2;
 +              parameters.POC[0].tile = 1;
 +              parameters.POC[0].resno0 = 0; 
 +              parameters.POC[0].compno0 = 0;
 +              parameters.POC[0].layno1 = 1;
 +              parameters.POC[0].resno1 = parameters.numresolution - 1;
 +              parameters.POC[0].compno1 = 3;
 +              parameters.POC[0].prg1 = CPRL;
 +              parameters.POC[1].tile = 1;
 +              parameters.POC[1].resno0 = parameters.numresolution - 1; 
 +              parameters.POC[1].compno0 = 0;
 +              parameters.POC[1].layno1 = 1;
 +              parameters.POC[1].resno1 = parameters.numresolution;
 +              parameters.POC[1].compno1 = 3;
 +              parameters.POC[1].prg1 = CPRL;
 +      }
 +      
 +      parameters.cp_comment = strdup (N_("DCP-o-matic"));
 +      parameters.cp_cinema = _resolution == RESOLUTION_2K ? CINEMA2K_24 : CINEMA4K_24;
 +
 +      /* 3 components, so use MCT */
 +      parameters.tcp_mct = 1;
 +      
 +      /* set max image */
 +      parameters.max_comp_size = max_comp_size;
 +      parameters.tcp_rates[0] = ((float) (3 * xyz->size().width * xyz->size().height * 12)) / (max_cs_len * 8);
 +
 +      /* Set event manager to null (openjpeg 1.3 bug) */
 +      cinfo->event_mgr = 0;
 +
 +      /* Setup the encoder parameters using the current image and user parameters */
 +      opj_setup_encoder (cinfo, &parameters, xyz->opj_image ());
 +
 +      opj_cio_t* cio = opj_cio_open ((opj_common_ptr) cinfo, 0, 0);
 +      if (cio == 0) {
 +              opj_destroy_compress (cinfo);
 +              throw EncodeError (N_("could not open JPEG2000 stream"));
 +      }
 +
 +      int const r = opj_encode (cinfo, cio, xyz->opj_image(), 0);
 +      if (r == 0) {
 +              opj_cio_close (cio);
 +              opj_destroy_compress (cinfo);
 +              throw EncodeError (N_("JPEG2000 encoding failed"));
 +      }
 +
 +      switch (_frame->eyes()) {
 +      case EYES_BOTH:
 +              LOG_GENERAL (N_("Finished locally-encoded frame %1 for mono"), _index);
 +              break;
 +      case EYES_LEFT:
 +              LOG_GENERAL (N_("Finished locally-encoded frame %1 for L"), _index);
 +              break;
 +      case EYES_RIGHT:
 +              LOG_GENERAL (N_("Finished locally-encoded frame %1 for R"), _index);
 +              break;
 +      default:
 +              break;
 +      }
 +
 +      shared_ptr<EncodedData> enc (new LocallyEncodedData (cio->buffer, cio_tell (cio)));
 +
 +      opj_cio_close (cio);
 +      free (parameters.cp_comment);
 +      opj_destroy_compress (cinfo);
 +
 +      return enc;
 +}
 +
 +/** Send this frame to a remote server for J2K encoding, then read the result.
 + *  @param serv Server to send to.
 + *  @return Encoded data.
 + */
 +shared_ptr<EncodedData>
 +DCPVideo::encode_remotely (ServerDescription serv)
 +{
 +      boost::asio::io_service io_service;
 +      boost::asio::ip::tcp::resolver resolver (io_service);
 +      boost::asio::ip::tcp::resolver::query query (serv.host_name(), raw_convert<string> (Config::instance()->server_port_base ()));
 +      boost::asio::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve (query);
 +
 +      shared_ptr<Socket> socket (new Socket);
 +
 +      socket->connect (*endpoint_iterator);
 +
 +      /* Collect all XML metadata */
 +      xmlpp::Document doc;
 +      xmlpp::Element* root = doc.create_root_node ("EncodingRequest");
 +      root->add_child("Version")->add_child_text (raw_convert<string> (SERVER_LINK_VERSION));
 +      add_metadata (root);
 +
 +      LOG_GENERAL (N_("Sending frame %1 to remote"), _index);
 +      
 +      /* Send XML metadata */
++      string xml = doc.write_to_string ("UTF-8");
++      socket->write (xml.length() + 1);
++      socket->write ((uint8_t *) xml.c_str(), xml.length() + 1);
 +
 +      /* Send binary data */
 +      _frame->send_binary (socket, _burn_subtitles);
 +
 +      /* Read the response (JPEG2000-encoded data); this blocks until the data
 +         is ready and sent back.
 +      */
 +      shared_ptr<EncodedData> e (new RemotelyEncodedData (socket->read_uint32 ()));
 +      socket->read (e->data(), e->size());
 +
 +      LOG_GENERAL (N_("Finished remotely-encoded frame %1"), _index);
 +      
 +      return e;
 +}
 +
 +void
 +DCPVideo::add_metadata (xmlpp::Element* el) const
 +{
 +      el->add_child("Index")->add_child_text (raw_convert<string> (_index));
 +      el->add_child("FramesPerSecond")->add_child_text (raw_convert<string> (_frames_per_second));
 +      el->add_child("J2KBandwidth")->add_child_text (raw_convert<string> (_j2k_bandwidth));
 +      el->add_child("Resolution")->add_child_text (raw_convert<string> (int (_resolution)));
 +      el->add_child("BurnSubtitles")->add_child_text (_burn_subtitles ? "1" : "0");
 +      _frame->add_metadata (el, _burn_subtitles);
 +}
 +
 +Eyes
 +DCPVideo::eyes () const
 +{
 +      return _frame->eyes ();
 +}
 +
index 0af53d8830a52ab8ca8518b0bae5591be12c6127,a4209f5b648306e734861c56ea96329b79a25a4a..bb4e022308dc9882dff57d5a0959d0a014cf9cab
@@@ -21,11 -21,9 +21,11 @@@ extern "C" 
  #include <libavformat/avformat.h>
  }
  #include <libcxml/cxml.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "ffmpeg_content.h"
  #include "ffmpeg_examiner.h"
 +#include "ffmpeg_subtitle_stream.h"
 +#include "ffmpeg_audio_stream.h"
  #include "compose.hpp"
  #include "job.h"
  #include "util.h"
  #include "log.h"
  #include "exceptions.h"
  #include "frame_rate_change.h"
+ #include "safe_stringstream.h"
  
  #include "i18n.h"
  
  #define LOG_GENERAL(...) film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
  
  using std::string;
- using std::stringstream;
  using std::vector;
  using std::list;
  using std::cout;
  using std::pair;
  using boost::shared_ptr;
  using boost::dynamic_pointer_cast;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  int const FFmpegContentProperty::SUBTITLE_STREAMS = 100;
  int const FFmpegContentProperty::SUBTITLE_STREAM = 101;
@@@ -64,7 -62,7 +64,7 @@@ FFmpegContent::FFmpegContent (shared_pt
  
  }
  
 -FFmpegContent::FFmpegContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version, list<string>& notes)
 +FFmpegContent::FFmpegContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version, list<string>& notes)
        : Content (f, node)
        , VideoContent (f, node, version)
        , AudioContent (f, node)
@@@ -110,7 -108,7 +110,7 @@@ FFmpegContent::FFmpegContent (shared_pt
  
        for (size_t i = 0; i < c.size(); ++i) {
                shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (c[i]);
 -              if (f->with_subtitles() && *(fc->_subtitle_stream.get()) != *(ref->_subtitle_stream.get())) {
 +              if (fc->use_subtitles() && *(fc->_subtitle_stream.get()) != *(ref->_subtitle_stream.get())) {
                        throw JoinError (_("Content to be joined must use the same subtitle stream."));
                }
  
@@@ -158,7 -156,7 +158,7 @@@ FFmpegContent::as_xml (xmlpp::Node* nod
        }
  
        if (_first_video) {
 -              node->add_child("FirstVideo")->add_child_text (raw_convert<string> (_first_video.get ()));
 +              node->add_child("FirstVideo")->add_child_text (raw_convert<string> (_first_video.get().get()));
        }
  }
  
@@@ -169,15 -167,20 +169,15 @@@ 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);
  
 -      VideoContent::Frame video_length = 0;
 -      video_length = examiner->video_length ();
 -      LOG_GENERAL ("Video length obtained from header as %1 frames", video_length);
 +      shared_ptr<const Film> film = _film.lock ();
 +      assert (film);
  
        {
                boost::mutex::scoped_lock lm (_mutex);
  
 -              _video_length = video_length;
 -
                _subtitle_streams = examiner->subtitle_streams ();
                if (!_subtitle_streams.empty ()) {
                        _subtitle_stream = _subtitle_streams.front ();
                _first_video = examiner->first_video ();
        }
  
 -      take_from_video_examiner (examiner);
 -
 -      signal_changed (ContentProperty::LENGTH);
        signal_changed (FFmpegContentProperty::SUBTITLE_STREAMS);
        signal_changed (FFmpegContentProperty::SUBTITLE_STREAM);
        signal_changed (FFmpegContentProperty::AUDIO_STREAMS);
@@@ -231,13 -237,13 +231,13 @@@ FFmpegContent::technical_summary () con
  string
  FFmpegContent::information () const
  {
 -      if (video_length() == 0 || video_frame_rate() == 0) {
 +      if (video_length() == ContentTime (0) || video_frame_rate() == 0) {
                return "";
        }
        
-       stringstream s;
+       SafeStringStream s;
        
 -      s << String::compose (_("%1 frames; %2 frames per second"), video_length_after_3d_combine(), video_frame_rate()) << "\n";
 +      s << String::compose (_("%1 frames; %2 frames per second"), video_length_after_3d_combine().frames (video_frame_rate()), video_frame_rate()) << "\n";
        s << VideoContent::information ();
  
        return s.str ();
@@@ -265,14 -271,19 +265,14 @@@ FFmpegContent::set_audio_stream (shared
        signal_changed (FFmpegContentProperty::AUDIO_STREAM);
  }
  
 -AudioContent::Frame
 +ContentTime
  FFmpegContent::audio_length () const
  {
 -      int const cafr = content_audio_frame_rate ();
 -      float const vfr = video_frame_rate ();
 -      VideoContent::Frame const vl = video_length_after_3d_combine ();
 -
 -      boost::mutex::scoped_lock lm (_mutex);
 -      if (!_audio_stream) {
 -              return 0;
 +      if (!audio_stream ()) {
 +              return ContentTime ();
        }
 -      
 -      return video_frames_to_audio_frames (vl, cafr, vfr);
 +
 +      return video_length ();
  }
  
  int
@@@ -284,11 -295,11 +284,11 @@@ FFmpegContent::audio_channels () cons
                return 0;
        }
  
 -      return _audio_stream->channels;
 +      return _audio_stream->channels ();
  }
  
  int
 -FFmpegContent::content_audio_frame_rate () const
 +FFmpegContent::audio_frame_rate () const
  {
        boost::mutex::scoped_lock lm (_mutex);
  
                return 0;
        }
  
 -      return _audio_stream->frame_rate;
 +      return _audio_stream->frame_rate ();
  }
  
  bool
@@@ -311,12 -322,94 +311,12 @@@ operator!= (FFmpegStream const & a, FFm
        return a._id != b._id;
  }
  
 -FFmpegStream::FFmpegStream (shared_ptr<const cxml::Node> node)
 -      : name (node->string_child ("Name"))
 -      , _id (node->number_child<int> ("Id"))
 -{
 -
 -}
 -
 -void
 -FFmpegStream::as_xml (xmlpp::Node* root) const
 -{
 -      root->add_child("Name")->add_child_text (name);
 -      root->add_child("Id")->add_child_text (raw_convert<string> (_id));
 -}
 -
 -FFmpegAudioStream::FFmpegAudioStream (shared_ptr<const cxml::Node> node, int version)
 -      : FFmpegStream (node)
 -      , mapping (node->node_child ("Mapping"), version)
 -{
 -      frame_rate = node->number_child<int> ("FrameRate");
 -      channels = node->number_child<int64_t> ("Channels");
 -      first_audio = node->optional_number_child<double> ("FirstAudio");
 -}
 -
 -void
 -FFmpegAudioStream::as_xml (xmlpp::Node* root) const
 -{
 -      FFmpegStream::as_xml (root);
 -      root->add_child("FrameRate")->add_child_text (raw_convert<string> (frame_rate));
 -      root->add_child("Channels")->add_child_text (raw_convert<string> (channels));
 -      if (first_audio) {
 -              root->add_child("FirstAudio")->add_child_text (raw_convert<string> (first_audio.get ()));
 -      }
 -      mapping.as_xml (root->add_child("Mapping"));
 -}
 -
 -bool
 -FFmpegStream::uses_index (AVFormatContext const * fc, int index) const
 -{
 -      size_t i = 0;
 -      while (i < fc->nb_streams) {
 -              if (fc->streams[i]->id == _id) {
 -                      return int (i) == index;
 -              }
 -              ++i;
 -      }
 -
 -      return false;
 -}
 -
 -AVStream *
 -FFmpegStream::stream (AVFormatContext const * fc) const
 -{
 -      size_t i = 0;
 -      while (i < fc->nb_streams) {
 -              if (fc->streams[i]->id == _id) {
 -                      return fc->streams[i];
 -              }
 -              ++i;
 -      }
 -
 -      assert (false);
 -      return 0;
 -}
 -
 -/** Construct a SubtitleStream from a value returned from to_string().
 - *  @param t String returned from to_string().
 - *  @param v State file version.
 - */
 -FFmpegSubtitleStream::FFmpegSubtitleStream (shared_ptr<const cxml::Node> node)
 -      : FFmpegStream (node)
 -{
 -      
 -}
 -
 -void
 -FFmpegSubtitleStream::as_xml (xmlpp::Node* root) const
 -{
 -      FFmpegStream::as_xml (root);
 -}
 -
 -Time
 +DCPTime
  FFmpegContent::full_length () const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
 -      
 -      FrameRateChange frc (video_frame_rate (), film->video_frame_rate ());
 -      return video_length_after_3d_combine() * frc.factor() * TIME_HZ / film->video_frame_rate ();
 +      return DCPTime (video_length_after_3d_combine(), FrameRateChange (video_frame_rate (), film->video_frame_rate ()));
  }
  
  AudioMapping
@@@ -328,7 -421,7 +328,7 @@@ FFmpegContent::audio_mapping () cons
                return AudioMapping ();
        }
  
 -      return _audio_stream->mapping;
 +      return _audio_stream->mapping ();
  }
  
  void
@@@ -345,14 -438,14 +345,14 @@@ FFmpegContent::set_filters (vector<Filt
  void
  FFmpegContent::set_audio_mapping (AudioMapping m)
  {
 -      audio_stream()->mapping = m;
 -      signal_changed (AudioContentProperty::AUDIO_MAPPING);
 +      audio_stream()->set_mapping (m);
 +      AudioContent::set_audio_mapping (m);
  }
  
  string
  FFmpegContent::identifier () const
  {
-       stringstream s;
+       SafeStringStream s;
  
        s << VideoContent::identifier();
  
@@@ -389,29 -482,3 +389,29 @@@ FFmpegContent::audio_analysis_path () c
        p /= name;
        return p;
  }
 +
 +list<ContentTimePeriod>
 +FFmpegContent::subtitles_during (ContentTimePeriod period, bool starting) const
 +{
 +      list<ContentTimePeriod> d;
 +      
 +      shared_ptr<FFmpegSubtitleStream> stream = subtitle_stream ();
 +      if (!stream) {
 +              return d;
 +      }
 +
 +      /* XXX: inefficient */
 +      for (vector<ContentTimePeriod>::const_iterator i = stream->periods.begin(); i != stream->periods.end(); ++i) {
 +              if ((starting && period.contains (i->from)) || (!starting && period.overlaps (*i))) {
 +                      d.push_back (*i);
 +              }
 +      }
 +
 +      return d;
 +}
 +
 +bool
 +FFmpegContent::has_subtitles () const
 +{
 +      return !subtitle_streams().empty ();
 +}
index 4e5d9bb1ed271760e858b4a37d62fcde110fa1d4,d40b798baafb77058f019e2bc6cf71817b5bd2f8..15443c346b3d302cce263165588044e641879160
@@@ -23,7 -23,6 +23,6 @@@
  
  #include <stdexcept>
  #include <vector>
- #include <sstream>
  #include <iomanip>
  #include <iostream>
  #include <stdint.h>
@@@ -32,26 -31,23 +31,26 @@@ extern "C" 
  #include <libavcodec/avcodec.h>
  #include <libavformat/avformat.h>
  }
 -#include "film.h"
  #include "filter.h"
  #include "exceptions.h"
  #include "image.h"
  #include "util.h"
  #include "log.h"
  #include "ffmpeg_decoder.h"
 +#include "ffmpeg_audio_stream.h"
 +#include "ffmpeg_subtitle_stream.h"
  #include "filter_graph.h"
  #include "audio_buffers.h"
  #include "ffmpeg_content.h"
 -#include "image_proxy.h"
 +#include "raw_image_proxy.h"
 +#include "film.h"
 +#include "timer.h"
  
  #include "i18n.h"
  
 -#define LOG_GENERAL(...) film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
 -#define LOG_ERROR(...) film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_ERROR);
 -#define LOG_WARNING(...) film->log()->log (__VA_ARGS__, Log::TYPE_WARNING);
 +#define LOG_GENERAL(...) _video_content->film()->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
 +#define LOG_ERROR(...) _video_content->film()->log()->log (String::compose (__VA_ARGS__), Log::TYPE_ERROR);
 +#define LOG_WARNING(...) _video_content->film()->log()->log (__VA_ARGS__, Log::TYPE_WARNING);
  
  using std::cout;
  using std::string;
@@@ -59,19 -55,26 +58,19 @@@ using std::vector
  using std::list;
  using std::min;
  using std::pair;
 +using std::make_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)
 +      : VideoDecoder (c)
 +      , AudioDecoder (c)
 +      , SubtitleDecoder (c)
        , FFmpeg (c)
 -      , _subtitle_codec_context (0)
 -      , _subtitle_codec (0)
 -      , _decode_video (video)
 -      , _decode_audio (audio)
 -      , _pts_offset (0)
 -      , _just_sought (false)
 +      , _log (log)
  {
 -      setup_subtitle ();
 -
        /* Audio and video frame PTS values may not start with 0.  We want
           to fiddle them so that:
  
           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_video = c->first_video();
 +      bool const have_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;
 -      }
 -}
 -
 -FFmpegDecoder::~FFmpegDecoder ()
 -{
 -      boost::mutex::scoped_lock lm (_mutex);
 -
 -      if (_subtitle_codec_context) {
 -              avcodec_close (_subtitle_codec_context);
 +              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;
        }
  }
  
@@@ -115,15 -135,20 +114,15 @@@ FFmpegDecoder::flush (
        
        /* XXX: should we reset _packet.data and size after each *_decode_* call? */
        
 -      if (_decode_video) {
 -              while (decode_video_packet ()) {}
 -      }
 +      while (decode_video_packet ()) {}
        
 -      if (_ffmpeg_content->audio_stream() && _decode_audio) {
 +      if (_ffmpeg_content->audio_stream()) {
                decode_audio_packet ();
 +              AudioDecoder::flush ();
        }
 -
 -      /* Stop us being asked for any more data */
 -      _video_position = _ffmpeg_content->video_length_after_3d_combine ();
 -      _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);
                        LOG_ERROR (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) {
 +
 +      if (si == _video_stream) {
                decode_video_packet ();
 -      } else if (_ffmpeg_content->audio_stream() && _ffmpeg_content->audio_stream()->uses_index (_format_context, si) && _decode_audio) {
 +      } else if (_ffmpeg_content->audio_stream() && _ffmpeg_content->audio_stream()->uses_index (_format_context, si)) {
                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_subtitle_packet ();
        }
  
        av_free_packet (&_packet);
 +      return false;
  }
  
  /** @param data pointer to array of pointers to buffers.
@@@ -284,40 -313,82 +283,40 @@@ FFmpegDecoder::bytes_per_audio_sample (
  }
  
  void
 -FFmpegDecoder::seek (VideoContent::Frame frame, bool accurate)
 +FFmpegDecoder::seek (ContentTime time, bool accurate)
  {
 -      double const time_base = av_q2d (_format_context->streams[_video_stream]->time_base);
 -
 -      /* 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.
 +      VideoDecoder::seek (time, accurate);
 +      AudioDecoder::seek (time, accurate);
 +      
 +      /* If we are doing an `accurate' seek, we need to use pre-roll, as
 +         we don't really know what the seek will give us.
        */
 -      int initial = frame;
  
 -      if (accurate) {
 -              initial -= 5;
 -      }
 +      ContentTime pre_roll = accurate ? ContentTime::from_seconds (2) : ContentTime (0);
 +      time -= pre_roll;
  
 -      if (initial < 0) {
 -              initial = 0;
 -      }
 -
 -      /* Initial seek time in the stream's timebase */
 -      int64_t const initial_vt = ((initial / _ffmpeg_content->original_video_frame_rate()) - _pts_offset) / time_base;
 -
 -      av_seek_frame (_format_context, _video_stream, initial_vt, AVSEEK_FLAG_BACKWARD);
 -
 -      avcodec_flush_buffers (video_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.
 +      /* XXX: it seems debatable whether PTS should be used here...
 +         http://www.mjbshaw.com/2012/04/seeking-in-ffmpeg-know-your-timestamp.html
        */
 -      if (!accurate) {
 -              _just_sought = true;
 -      }
 -      
 -      _video_position = frame;
        
 -      if (frame == 0 || !accurate) {
 -              /* We're already there, or we're as close as we need to be */
 -              return;
 -      }
 +      ContentTime const u = time - _pts_offset;
 +      int64_t s = u.seconds() / av_q2d (_format_context->streams[_video_stream]->time_base);
  
 -      while (true) {
 -              int r = av_read_frame (_format_context, &_packet);
 -              if (r < 0) {
 -                      return;
 -              }
 +      if (_ffmpeg_content->audio_stream ()) {
 +              s = min (
 +                      s, int64_t (u.seconds() / av_q2d (_ffmpeg_content->audio_stream()->stream(_format_context)->time_base))
 +                      );
 +      }
  
 -              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->original_video_frame_rate()
 -                              );
 +      av_seek_frame (_format_context, _video_stream, s, 0);
  
 -                      if (_video_position >= (frame - 1)) {
 -                              av_free_packet (&_packet);
 -                              break;
 -                      }
 -              }
 -              
 -              av_free_packet (&_packet);
 +      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 ());
        }
 -
 -      /* _video_position should be the next thing to be emitted, which will the one after the thing
 -         we just saw.
 -      */
 -      _video_position++;
  }
  
  void
@@@ -333,23 -404,42 +332,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);
                        LOG_ERROR ("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 */
 -                                      int64_t frames = pts * _ffmpeg_content->content_audio_frame_rate ();
 -                                      while (frames > 0) {
 -                                              int64_t const this_time = min (frames, (int64_t) _ffmpeg_content->content_audio_frame_rate() / 2);
 -                                              
 -                                              shared_ptr<AudioBuffers> silence (
 -                                                      new AudioBuffers (_ffmpeg_content->audio_channels(), this_time)
 -                                                      );
 -                                      
 -                                              silence->make_silent ();
 -                                              audio (silence, _audio_position);
 -                                              frames -= this_time;
 -                                      }
 -                              }
 -                      }
 +                      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;
@@@ -370,13 -460,17 +369,13 @@@ 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);
 -
                LOG_GENERAL (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);
  
 -      shared_ptr<const Film> film = _film.lock ();
 -      assert (film);
 -
        for (list<pair<shared_ptr<Image>, int64_t> >::iterator i = images.begin(); i != images.end(); ++i) {
  
                shared_ptr<Image> image = i->first;
                
                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->original_video_frame_rate ());
 -                              _just_sought = false;
 -                      }
 -
 -                      double const next = _video_position / _ffmpeg_content->original_video_frame_rate();
 -                      double const one_frame = 1 / _ffmpeg_content->original_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
 -                                              )
 -                                      );
 -                              
 -                              shared_ptr<const Film> film = _film.lock ();
 -                              assert (film);
 -
 -                              black->make_black ();
 -                              video (shared_ptr<ImageProxy> (new RawImageProxy (image, film->log())), 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 (shared_ptr<ImageProxy> (new RawImageProxy (image, film->log())), false, _video_position);
 -                      }
 -                              
 +                      double const pts = i->second * av_q2d (_format_context->streams[_video_stream]->time_base) + _pts_offset.seconds ();
 +                      video (
 +                              shared_ptr<ImageProxy> (new RawImageProxy (image, _video_content->film()->log())),
 +                              rint (pts * _ffmpeg_content->video_frame_rate ())
 +                              );
                } else {
                        LOG_WARNING ("Dropping frame without PTS");
                }
  
        return true;
  }
 -
 -      
 -void
 -FFmpegDecoder::setup_subtitle ()
 -{
 -      boost::mutex::scoped_lock lm (_mutex);
 -      
 -      if (!_ffmpeg_content->subtitle_stream()) {
 -              return;
 -      }
 -
 -      _subtitle_codec_context = _ffmpeg_content->subtitle_stream()->stream(_format_context)->codec;
 -      if (_subtitle_codec_context == 0) {
 -              throw DecodeError (N_("could not find subtitle stream"));
 -      }
 -
 -      _subtitle_codec = avcodec_find_decoder (_subtitle_codec_context->codec_id);
 -
 -      if (_subtitle_codec == 0) {
 -              throw DecodeError (N_("could not find subtitle decoder"));
 -      }
 -      
 -      if (avcodec_open2 (_subtitle_codec_context, _subtitle_codec, 0) < 0) {
 -              throw DecodeError (N_("could not open subtitle decoder"));
 -      }
 -}
 -
 -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 ()
  {
        int got_subtitle;
        AVSubtitle sub;
 -      if (avcodec_decode_subtitle2 (_subtitle_codec_context, &sub, &got_subtitle, &_packet) < 0 || !got_subtitle) {
 +      if (avcodec_decode_subtitle2 (subtitle_codec_context(), &sub, &got_subtitle, &_packet) < 0 || !got_subtitle) {
                return;
        }
  
           indicate that the previous subtitle should stop.
        */
        if (sub.num_rects <= 0) {
 -              subtitle (shared_ptr<Image> (), dcpomatic::Rect<double> (), 0, 0);
 +              image_subtitle (ContentTimePeriod (), shared_ptr<Image> (), dcpomatic::Rect<double> ());
                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;
 -
 -      /* 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;
 +      ContentTimePeriod period = subtitle_period (sub) + _pts_offset;
  
        AVSubtitleRect const * rect = sub.rects[0];
  
        if (rect->type != SUBTITLE_BITMAP) {
 -              throw DecodeError (_("non-bitmap subtitles not yet supported"));
 +              /* XXX */
 +              // throw DecodeError (_("non-bitmap subtitles not yet supported"));
 +              return;
        }
  
        /* 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 (
 +              period,
                image,
                dcpomatic::Rect<double> (
                        static_cast<double> (rect->x) / vs.width,
                        static_cast<double> (rect->y) / vs.height,
                        static_cast<double> (rect->w) / vs.width,
                        static_cast<double> (rect->h) / vs.height
 -                      ),
 -              from,
 -              to
 +                      )
                );
 -                        
        
        avsubtitle_free (&sub);
  }
 +
 +list<ContentTimePeriod>
 +FFmpegDecoder::subtitles_during (ContentTimePeriod p, bool starting) const
 +{
 +      return _ffmpeg_content->subtitles_during (p, starting);
 +}
index 62b909b1d65f3708293ca8dcbe91fe4a371d4608,5ccc8028b6ebae14dfbf6f1434b94e0c49a7e681..48d85da6f11113cb6cabc83215505c8df766110a
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
 +    Copyright (C) 2013-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
@@@ -23,16 -23,13 +23,16 @@@ extern "C" 
  }
  #include "ffmpeg_examiner.h"
  #include "ffmpeg_content.h"
 +#include "ffmpeg_audio_stream.h"
 +#include "ffmpeg_subtitle_stream.h"
 +#include "util.h"
+ #include "safe_stringstream.h"
  
  #include "i18n.h"
  
  using std::string;
  using std::cout;
  using std::max;
- using std::stringstream;
  using boost::shared_ptr;
  using boost::optional;
  
@@@ -64,93 -61,55 +64,93 @@@ FFmpegExaminer::FFmpegExaminer (shared_
                }
        }
  
 -      /* Run through until we find the first audio (for each stream) and video */
 -
 +      /* Run through until we find:
 +       *   - the first video.
 +       *   - the first audio for each stream.
 +       *   - the subtitle periods for each stream.
 +       *
 +       * We have to note subtitle periods as otherwise we have no way of knowing
 +       * where we should look for subtitles (video and audio are always present,
 +       * so they are ok).
 +       */
        while (true) {
                int r = av_read_frame (_format_context, &_packet);
                if (r < 0) {
                        break;
                }
  
 -              int frame_finished;
 -
                AVCodecContext* context = _format_context->streams[_packet.stream_index]->codec;
  
 -              if (_packet.stream_index == _video_stream && !_first_video) {
 -                      if (avcodec_decode_video2 (context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) {
 -                              _first_video = frame_time (_format_context->streams[_video_stream]);
 -                      }
 -              } else {
 -                      for (size_t i = 0; i < _audio_streams.size(); ++i) {
 -                              if (_audio_streams[i]->uses_index (_format_context, _packet.stream_index) && !_audio_streams[i]->first_audio) {
 -                                      if (avcodec_decode_audio4 (context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) {
 -                                              _audio_streams[i]->first_audio = frame_time (_audio_streams[i]->stream (_format_context));
 -                                      }
 -                              }
 +              if (_packet.stream_index == _video_stream) {
 +                      video_packet (context);
 +              }
 +              
 +              for (size_t i = 0; i < _audio_streams.size(); ++i) {
 +                      if (_audio_streams[i]->uses_index (_format_context, _packet.stream_index)) {
 +                              audio_packet (context, _audio_streams[i]);
                        }
                }
  
 -              bool have_all_audio = true;
 -              size_t i = 0;
 -              while (i < _audio_streams.size() && have_all_audio) {
 -                      have_all_audio = _audio_streams[i]->first_audio;
 -                      ++i;
 +              for (size_t i = 0; i < _subtitle_streams.size(); ++i) {
 +                      if (_subtitle_streams[i]->uses_index (_format_context, _packet.stream_index)) {
 +                              subtitle_packet (context, _subtitle_streams[i]);
 +                      }
                }
  
                av_free_packet (&_packet);
 -              
 -              if (_first_video && have_all_audio) {
 -                      break;
 +      }
 +}
 +
 +void
 +FFmpegExaminer::video_packet (AVCodecContext* context)
 +{
 +      if (_first_video) {
 +              return;
 +      }
 +
 +      int frame_finished;
 +      if (avcodec_decode_video2 (context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) {
 +              _first_video = frame_time (_format_context->streams[_video_stream]);
 +      }
 +}
 +
 +void
 +FFmpegExaminer::audio_packet (AVCodecContext* context, shared_ptr<FFmpegAudioStream> stream)
 +{
 +      if (stream->first_audio) {
 +              return;
 +      }
 +
 +      int frame_finished;
 +      if (avcodec_decode_audio4 (context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) {
 +              stream->first_audio = frame_time (stream->stream (_format_context));
 +      }
 +}
 +
 +void
 +FFmpegExaminer::subtitle_packet (AVCodecContext* context, shared_ptr<FFmpegSubtitleStream> stream)
 +{
 +      int frame_finished;
 +      AVSubtitle sub;
 +      if (avcodec_decode_subtitle2 (context, &sub, &frame_finished, &_packet) >= 0 && frame_finished) {
 +              ContentTimePeriod const period = subtitle_period (sub);
 +              if (sub.num_rects == 0 && !stream->periods.empty () && stream->periods.back().to > period.from) {
 +                      /* Finish the last subtitle */
 +                      stream->periods.back().to = period.from;
 +              } else if (sub.num_rects == 1) {
 +                      stream->periods.push_back (period);
                }
        }
  }
  
 -optional<double>
 +optional<ContentTime>
  FFmpegExaminer::frame_time (AVStream* s) const
  {
 -      optional<double> t;
 +      optional<ContentTime> t;
        
        int64_t const bet = av_frame_get_best_effort_timestamp (_frame);
        if (bet != AV_NOPTS_VALUE) {
 -              t = bet * av_q2d (s->time_base);
 +              t = ContentTime::from_seconds (bet * av_q2d (s->time_base));
        }
  
        return t;
  float
  FFmpegExaminer::video_frame_rate () const
  {
 -      AVStream* s = _format_context->streams[_video_stream];
 -
 -      if (s->avg_frame_rate.num && s->avg_frame_rate.den) {
 -              return av_q2d (s->avg_frame_rate);
 -      }
 -
 -      return av_q2d (s->r_frame_rate);
 +      /* This use of r_frame_rate is debateable; there's a few different
 +       * frame rates in the format context, but this one seems to be the most
 +       * reliable.
 +       */
 +      return av_q2d (av_stream_get_r_frame_rate (_format_context->streams[_video_stream]));
  }
  
 -libdcp::Size
 +dcp::Size
  FFmpegExaminer::video_size () const
  {
 -      return libdcp::Size (video_codec_context()->width, video_codec_context()->height);
 +      return dcp::Size (video_codec_context()->width, video_codec_context()->height);
  }
  
 -/** @return Length (in video frames) according to our content's header */
 -VideoContent::Frame
 +/** @return Length according to our content's header */
 +ContentTime
  FFmpegExaminer::video_length () const
  {
-       ContentTime const length = ContentTime::from_seconds (double (_format_context->duration - _format_context->start_time) / AV_TIME_BASE);
 -      VideoContent::Frame const length = (double (_format_context->duration) / AV_TIME_BASE) * video_frame_rate();
 -      return max (1, length);
++      ContentTime const length = ContentTime::from_seconds (double (_format_context->duration) / AV_TIME_BASE);
 +      return ContentTime (max (ContentTime::Type (1), length.get ()));
  }
  
  string
  FFmpegExaminer::audio_stream_name (AVStream* s) const
  {
-       stringstream n;
+       SafeStringStream n;
  
        n << stream_name (s);
  
  string
  FFmpegExaminer::subtitle_stream_name (AVStream* s) const
  {
-       stringstream n;
+       SafeStringStream n;
  
        n << stream_name (s);
  
  string
  FFmpegExaminer::stream_name (AVStream* s) const
  {
-       stringstream n;
+       SafeStringStream n;
  
        if (s->metadata) {
                AVDictionaryEntry const * lang = av_dict_get (s->metadata, "language", 0, 0);
diff --combined src/lib/film.cc
index 0891838ff930674548ea90c23e7f117fcdc05905,2701d81b8374f619c205aa63589933da63717f39..577c29e3a55afeb8a7c85e29883c2bf7c30bcff6
  #include <algorithm>
  #include <fstream>
  #include <cstdlib>
- #include <sstream>
  #include <iomanip>
  #include <unistd.h>
  #include <boost/filesystem.hpp>
  #include <boost/algorithm/string.hpp>
 -#include <boost/date_time.hpp>
 +#include <boost/lexical_cast.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 <libdcp/raw_convert.h>
 +#include <dcp/cpl.h>
 +#include <dcp/signer.h>
 +#include <dcp/util.h>
 +#include <dcp/local_time.h>
 +#include <dcp/raw_convert.h>
  #include "film.h"
  #include "job.h"
  #include "util.h"
  #include "ratio.h"
  #include "cross.h"
  #include "cinema.h"
+ #include "safe_stringstream.h"
  
  #include "i18n.h"
  
  using std::string;
- using std::stringstream;
  using std::multimap;
  using std::pair;
  using std::map;
@@@ -77,10 -77,9 +76,10 @@@ using boost::ends_with
  using boost::starts_with;
  using boost::optional;
  using boost::is_any_of;
 -using libdcp::Size;
 -using libdcp::Signer;
 -using libdcp::raw_convert;
 +using dcp::Size;
 +using dcp::Signer;
 +using dcp::raw_convert;
 +using dcp::raw_convert;
  
  #define LOG_GENERAL(...) log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
  #define LOG_GENERAL_NC(...) log()->log (__VA_ARGS__, Log::TYPE_GENERAL);
   * Use <Scale> tag in <VideoContent> rather than <Ratio>.
   * 8 -> 9
   * DCI -> ISDCF
 + *
 + * Bumped to 32 for 2.0 branch; some times are expressed in Times rather
 + * than frames now.
   */
 -int const Film::current_state_version = 9;
 +int const Film::current_state_version = 32;
  
  /** Construct a Film object in a given directory.
   *
@@@ -111,6 -107,7 +110,6 @@@ Film::Film (boost::filesystem::path dir
        , _container (Config::instance()->default_container ())
        , _resolution (RESOLUTION_2K)
        , _scaler (Scaler::from_id ("bicubic"))
 -      , _with_subtitles (false)
        , _signed (true)
        , _encrypted (false)
        , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ())
        , _three_d (false)
        , _sequence_video (true)
        , _interop (false)
 +      , _burn_subtitles (false)
        , _state_version (current_state_version)
        , _dirty (false)
  {
@@@ -162,7 -158,7 +161,7 @@@ Film::video_identifier () cons
  {
        assert (container ());
  
-       stringstream s;
+       SafeStringStream s;
        s.imbue (std::locale::classic ());
        
        s << container()->id()
                s << "_S";
        }
  
 -      if (_three_d) {
 -              s << "_3D";
 +      if (_burn_subtitles) {
 +              s << "_B";
        }
  
 -      if (_with_subtitles) {
 -              s << "_WS";
 +      if (_three_d) {
 +              s << "_3D";
        }
  
        return s.str ();
@@@ -229,12 -225,6 +228,12 @@@ Film::audio_mxf_filename () cons
        return filename_safe_name() + "_audio.mxf";
  }
  
 +boost::filesystem::path
 +Film::subtitle_xml_filename () const
 +{
 +      return filename_safe_name() + "_subtitle.xml";
 +}
 +
  string
  Film::filename_safe_name () const
  {
@@@ -377,6 -367,7 +376,6 @@@ Film::metadata () cons
  
        root->add_child("Resolution")->add_child_text (resolution_to_string (_resolution));
        root->add_child("Scaler")->add_child_text (_scaler->id ());
 -      root->add_child("WithSubtitles")->add_child_text (_with_subtitles ? "1" : "0");
        root->add_child("J2KBandwidth")->add_child_text (raw_convert<string> (_j2k_bandwidth));
        _isdcf_metadata.as_xml (root->add_child ("ISDCFMetadata"));
        root->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate));
        root->add_child("ThreeD")->add_child_text (_three_d ? "1" : "0");
        root->add_child("SequenceVideo")->add_child_text (_sequence_video ? "1" : "0");
        root->add_child("Interop")->add_child_text (_interop ? "1" : "0");
 +      root->add_child("BurnSubtitles")->add_child_text (_burn_subtitles ? "1" : "0");
        root->add_child("Signed")->add_child_text (_signed ? "1" : "0");
        root->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0");
        root->add_child("Key")->add_child_text (_key.hex ());
@@@ -449,6 -439,7 +448,6 @@@ Film::read_metadata (
  
        _resolution = string_to_resolution (f.string_child ("Resolution"));
        _scaler = Scaler::from_id (f.string_child ("Scaler"));
 -      _with_subtitles = f.bool_child ("WithSubtitles");
        _j2k_bandwidth = f.number_child<int> ("J2KBandwidth");
        _video_frame_rate = f.number_child<int> ("VideoFrameRate");
        _signed = f.optional_bool_child("Signed").get_value_or (true);
        _sequence_video = f.bool_child ("SequenceVideo");
        _three_d = f.bool_child ("ThreeD");
        _interop = f.bool_child ("Interop");
 -      _key = libdcp::Key (f.string_child ("Key"));
 +      if (_state_version >= 32) {
 +              _burn_subtitles = f.bool_child ("BurnSubtitles");
 +      }
 +      _key = dcp::Key (f.string_child ("Key"));
  
        list<string> notes;
        /* This method is the only one that can return notes (so far) */
@@@ -504,7 -492,7 +503,7 @@@ Film::file (boost::filesystem::path f) 
  string
  Film::isdcf_name (bool if_created_now) const
  {
-       stringstream d;
+       SafeStringStream d;
  
        string raw_name = name ();
  
        */
  
        /* The standard says we don't do this for trailers, for some strange reason */
 -      if (dcp_content_type() && dcp_content_type()->libdcp_kind() != libdcp::TRAILER) {
 +      if (dcp_content_type() && dcp_content_type()->libdcp_kind() != dcp::TRAILER) {
                ContentList cl = content ();
                Ratio const * content_ratio = 0;
                for (ContentList::const_iterator i = cl.begin(); i != cl.end(); ++i) {
@@@ -695,6 -683,7 +694,6 @@@ Film::dcp_name (bool if_created_now) co
        return name();
  }
  
 -
  void
  Film::set_directory (boost::filesystem::path d)
  {
@@@ -744,6 -733,13 +743,6 @@@ Film::set_scaler (Scaler const * s
        signal_changed (SCALER);
  }
  
 -void
 -Film::set_with_subtitles (bool w)
 -{
 -      _with_subtitles = w;
 -      signal_changed (WITH_SUBTITLES);
 -}
 -
  void
  Film::set_j2k_bandwidth (int b)
  {
@@@ -786,13 -782,6 +785,13 @@@ Film::set_interop (bool i
        signal_changed (INTEROP);
  }
  
 +void
 +Film::set_burn_subtitles (bool b)
 +{
 +      _burn_subtitles = b;
 +      signal_changed (BURN_SUBTITLES);
 +}
 +
  void
  Film::signal_changed (Property p)
  {
@@@ -827,7 -816,7 +826,7 @@@ Film::info_path (int f, Eyes e) cons
        boost::filesystem::path p;
        p /= info_dir ();
  
-       stringstream s;
+       SafeStringStream s;
        s.width (8);
        s << setfill('0') << f;
  
@@@ -854,7 -843,7 +853,7 @@@ Film::j2c_path (int f, Eyes e, bool t) 
        p /= "j2c";
        p /= video_identifier ();
  
-       stringstream s;
+       SafeStringStream s;
        s.width (8);
        s << setfill('0') << f;
  
        return file (p);
  }
  
 -/** Find all the DCPs in our directory that can be libdcp::DCP::read() and return details of their CPLs */
 +/** Find all the DCPs in our directory that can be dcp::DCP::read() and return details of their CPLs */
  vector<CPLSummary>
  Film::cpls () const
  {
                        ) {
  
                        try {
 -                              libdcp::DCP dcp (*i);
 +                              dcp::DCP dcp (*i);
                                dcp.read ();
                                out.push_back (
                                        CPLSummary (
 -                                              i->path().leaf().string(), dcp.cpls().front()->id(), dcp.cpls().front()->name(), dcp.cpls().front()->filename()
 +                                              i->path().leaf().string(),
 +                                              dcp.cpls().front()->id(),
 +                                              dcp.cpls().front()->annotation_text(),
 +                                              dcp.cpls().front()->file()
                                                )
                                        );
                        } catch (...) {
@@@ -939,13 -925,6 +938,13 @@@ Film::content () cons
        return _playlist->content ();
  }
  
 +void
 +Film::examine_content (shared_ptr<Content> c)
 +{
 +      shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c));
 +      JobManager::instance()->add (j);
 +}
 +
  void
  Film::examine_and_add_content (shared_ptr<Content> c)
  {
@@@ -1001,22 -980,22 +1000,22 @@@ Film::move_content_later (shared_ptr<Co
        _playlist->move_later (c);
  }
  
 -Time
 +DCPTime
  Film::length () const
  {
        return _playlist->length ();
  }
  
 -bool
 -Film::has_subtitles () const
 +int
 +Film::best_video_frame_rate () const
  {
 -      return _playlist->has_subtitles ();
 +      return _playlist->best_dcp_frame_rate ();
  }
  
 -OutputVideoFrame
 -Film::best_video_frame_rate () const
 +FrameRateChange
 +Film::active_frame_rate_change (DCPTime t) const
  {
 -      return _playlist->best_dcp_frame_rate ();
 +      return _playlist->active_frame_rate_change (t, video_frame_rate ());
  }
  
  void
@@@ -1037,7 -1016,31 +1036,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 */
@@@ -1053,62 -1056,61 +1052,62 @@@ Film::set_sequence_video (bool s
  }
  
  /** @return Size of the largest possible image in whatever resolution we are using */
 -libdcp::Size
 +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 ();
  }
  
  /** @return Size of the frame */
 -libdcp::Size
 +dcp::Size
  Film::frame_size () const
  {
 -      return fit_ratio_within (container()->ratio(), full_frame ());
 +      return fit_ratio_within (container()->ratio(), full_frame (), 1);
  }
  
 -/** @param from KDM from time in local time.
 - *  @param to KDM to time in local time.
 - */
 -libdcp::KDM
 +dcp::EncryptedKDM
  Film::make_kdm (
 -      shared_ptr<libdcp::Certificate> target,
 +      dcp::Certificate target,
        boost::filesystem::path cpl_file,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime until,
 -      libdcp::KDM::Formulation formulation
 +      dcp::LocalTime from,
 +      dcp::LocalTime until,
 +      dcp::Formulation formulation
        ) const
  {
 -      shared_ptr<const Signer> signer = make_signer ();
 -
 -      time_t now = time (0);
 -      struct tm* tm = localtime (&now);
 -      string const issue_date = libdcp::tm_to_string (tm);
 +      shared_ptr<const dcp::CPL> cpl (new dcp::CPL (cpl_file));
 +      shared_ptr<const dcp::Signer> signer = Config::instance()->signer();
 +      if (!signer->valid ()) {
 +              throw InvalidSignerError ();
 +      }
        
 -      return libdcp::KDM (cpl_file, signer, target, key (), from, until, "DCP-o-matic", issue_date, formulation);
 +      return dcp::DecryptedKDM (
 +              cpl, key(), from, until, "DCP-o-matic", cpl->content_title_text(), dcp::LocalTime().as_string()
 +              ).encrypt (signer, target, formulation);
  }
  
 -list<libdcp::KDM>
 +list<dcp::EncryptedKDM>
  Film::make_kdms (
        list<shared_ptr<Screen> > screens,
        boost::filesystem::path dcp,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime until,
 -      libdcp::KDM::Formulation formulation
 +      dcp::LocalTime from,
 +      dcp::LocalTime until,
 +      dcp::Formulation formulation
        ) const
  {
 -      list<libdcp::KDM> kdms;
 +      list<dcp::EncryptedKDM> kdms;
  
        for (list<shared_ptr<Screen> >::iterator i = screens.begin(); i != screens.end(); ++i) {
 -              kdms.push_back (make_kdm ((*i)->certificate, dcp, from, until, formulation));
 +              if ((*i)->certificate) {
 +                      kdms.push_back (make_kdm ((*i)->certificate.get(), dcp, from, until, formulation));
 +              }
        }
  
        return kdms;
  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
@@@ -1138,3 -1140,10 +1137,3 @@@ Film::should_be_enough_disk_space (doub
        available = double (s.available) / 1073741824.0f;
        return (available - required) > 1;
  }
 -
 -FrameRateChange
 -Film::active_frame_rate_change (Time t) const
 -{
 -      return _playlist->active_frame_rate_change (t, video_frame_rate ());
 -}
 -
diff --combined src/lib/filter_graph.cc
index f6a6c4529df2376bc08f81a2adaf721d5516459d,0d72eacdfd2311c2a793065078bf178ff4e86a15..d2427c31faf35957b251320d1d091fce2e54b161
@@@ -34,10 -34,10 +34,10 @@@ extern "C" 
  #include "exceptions.h"
  #include "image.h"
  #include "ffmpeg_content.h"
+ #include "safe_stringstream.h"
  
  #include "i18n.h"
  
- using std::stringstream;
  using std::string;
  using std::list;
  using std::pair;
@@@ -45,29 -45,26 +45,29 @@@ 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)
 -      : _buffer_src_context (0)
 +FilterGraph::FilterGraph (shared_ptr<const FFmpegContent> content, dcp::Size s, AVPixelFormat p)
 +      : _copy (false)
 +      , _buffer_src_context (0)
        , _buffer_sink_context (0)
        , _size (s)
        , _pixel_format (p)
 +      , _frame (0)
  {
 -      _frame = av_frame_alloc ();
 -      
 -      string filters = Filter::ffmpeg_string (content->filters());
 +      string const filters = Filter::ffmpeg_string (content->filters());
        if (filters.empty ()) {
 -              filters = "copy";
 +              _copy = true;
 +              return;
        }
  
 +      _frame = av_frame_alloc ();
 +      
        AVFilterGraph* graph = avfilter_graph_alloc();
        if (graph == 0) {
                throw DecodeError (N_("could not create filter graph."));
@@@ -83,7 -80,7 +83,7 @@@
                throw DecodeError (N_("Could not create buffer sink filter"));
        }
  
-       stringstream a;
+       SafeStringStream a;
        a << "video_size=" << _size.width << "x" << _size.height << ":"
          << "pix_fmt=" << _pixel_format << ":"
          << "time_base=1/1:"
                throw DecodeError (N_("could not configure filter graph."));
        }
  
 -      /* XXX: leaking `inputs' / `outputs' ? */
 +      avfilter_inout_free (&inputs);
 +      avfilter_inout_free (&outputs);
  }
  
  FilterGraph::~FilterGraph ()
  {
 -      av_frame_free (&_frame);
 +      if (_frame) {
 +              av_frame_free (&_frame);
 +      }
  }
  
  /** Take an AVFrame and process it using our configured filters, returning a
@@@ -144,23 -138,19 +144,23 @@@ FilterGraph::process (AVFrame* frame
  {
        list<pair<shared_ptr<Image>, int64_t> > images;
  
 -      if (av_buffersrc_write_frame (_buffer_src_context, frame) < 0) {
 -              throw DecodeError (N_("could not push buffer into filter chain."));
 -      }
 -
 -      while (true) {
 -              if (av_buffersink_get_frame (_buffer_sink_context, _frame) < 0) {
 -                      break;
 +      if (_copy) {
 +              images.push_back (make_pair (shared_ptr<Image> (new Image (frame)), av_frame_get_best_effort_timestamp (frame)));
 +      } else {
 +              if (av_buffersrc_write_frame (_buffer_src_context, frame) < 0) {
 +                      throw DecodeError (N_("could not push buffer into filter chain."));
 +              }
 +              
 +              while (true) {
 +                      if (av_buffersink_get_frame (_buffer_sink_context, _frame) < 0) {
 +                              break;
 +                      }
 +                      
 +                      images.push_back (make_pair (shared_ptr<Image> (new Image (_frame)), av_frame_get_best_effort_timestamp (_frame)));
 +                      av_frame_unref (_frame);
                }
 -
 -              images.push_back (make_pair (shared_ptr<Image> (new Image (_frame)), av_frame_get_best_effort_timestamp (_frame)));
 -              av_frame_unref (_frame);
        }
 -      
 +              
        return images;
  }
  
   *  @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_content.cc
index 70d777bca11f6e52a466dbf44414b7a1ab048163,915da7beba33b70687e0536d751d4e8e6e96a65f..84b0b75c9732a4cd47dc30e1698ee1a82335c675
  #include <libcxml/cxml.h>
  #include "image_content.h"
  #include "image_examiner.h"
 -#include "config.h"
  #include "compose.hpp"
  #include "film.h"
  #include "job.h"
  #include "frame_rate_change.h"
 +#include "exceptions.h"
+ #include "safe_stringstream.h"
  
  #include "i18n.h"
  
  using std::string;
  using std::cout;
- using std::stringstream;
  using boost::shared_ptr;
  
  ImageContent::ImageContent (shared_ptr<const Film> f, boost::filesystem::path p)
@@@ -55,7 -55,7 +55,7 @@@
  }
  
  
 -ImageContent::ImageContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version)
 +ImageContent::ImageContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version)
        : Content (f, node)
        , VideoContent (f, node, version)
  {
@@@ -109,11 -109,13 +109,11 @@@ ImageContent::examine (shared_ptr<Job> 
        assert (film);
        
        shared_ptr<ImageExaminer> examiner (new ImageExaminer (film, shared_from_this(), job));
 -
        take_from_video_examiner (examiner);
 -      set_video_length (examiner->video_length ());
  }
  
  void
 -ImageContent::set_video_length (VideoContent::Frame len)
 +ImageContent::set_video_length (ContentTime len)
  {
        {
                boost::mutex::scoped_lock lm (_mutex);
        signal_changed (ContentProperty::LENGTH);
  }
  
 -Time
 +DCPTime
  ImageContent::full_length () const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
 -      
 -      FrameRateChange frc (video_frame_rate(), film->video_frame_rate ());
 -      return video_length_after_3d_combine() * frc.factor() * TIME_HZ / video_frame_rate();
 +      return DCPTime (video_length_after_3d_combine(), FrameRateChange (video_frame_rate(), film->video_frame_rate()));
  }
  
  string
  ImageContent::identifier () const
  {
-       stringstream s;
+       SafeStringStream s;
        s << VideoContent::identifier ();
 -      s << "_" << video_length();
 +      s << "_" << video_length().get();
        return s.str ();
  }
  
diff --combined src/lib/job.cc
index 31a10a44bd6af2343ca4ddfa525ed3b4e35226ad,52ec1426c0bd0300a6658293471ef795e2e995ae..1d3cb73b6fcf39f276e9aefc3fbdd91841397051
@@@ -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/thread.hpp>
  #include <boost/filesystem.hpp>
 -#include <libdcp/exceptions.h>
 +#include <dcp/exceptions.h>
  #include "job.h"
  #include "util.h"
  #include "cross.h"
  #include "ui_signaller.h"
  #include "exceptions.h"
 -#include "safe_stringstream.h"
 +#include "film.h"
 +#include "log.h"
  
  #include "i18n.h"
  
  using std::string;
  using std::list;
  using std::cout;
- using std::stringstream;
  using boost::shared_ptr;
  
  Job::Job (shared_ptr<const Film> f)
@@@ -68,8 -66,8 +67,8 @@@ Job::run_wrapper (
  
                run ();
  
 -      } catch (libdcp::FileError& e) {
 -              
 +      } catch (dcp::FileError& e) {
 +
                string m = String::compose (_("An error occurred whilst handling the file %1."), boost::filesystem::path (e.filename()).leaf());
  
                try {
@@@ -206,7 -204,7 +205,7 @@@ Job::set_state (State s
        }       
  }
  
 -/** @return Time (in seconds) that this sub-job has been running */
 +/** @return DCPTime (in seconds) that this sub-job has been running */
  int
  Job::elapsed_time () const
  {
@@@ -281,7 -279,6 +280,7 @@@ Job::error_summary () cons
  void
  Job::set_error (string s, string d)
  {
 +      _film->log()->log (String::compose ("Error in job: %1 (%2)", s, d), Log::TYPE_ERROR);
        boost::mutex::scoped_lock lm (_state_mutex);
        _error_summary = s;
        _error_details = d;
@@@ -309,7 -306,7 +308,7 @@@ Job::status () cons
                pc = 99;
        }
  
-       stringstream s;
+       SafeStringStream s;
        if (!finished ()) {
                s << pc << N_("%");
                if (p >= 0 && t > 10 && r > 0) {
diff --combined src/lib/kdm.cc
index a2ed1be73498dd924752f998fa154e479ae18cb9,f5054b8ed0c11d4d0522b14ba420348da90284a0..108860594e2592144760965a02aeb1220f8cf02c
  #include <boost/shared_ptr.hpp>
  #include <quickmail.h>
  #include <zip.h>
 -#include <libdcp/kdm.h>
 +#include <dcp/encrypted_kdm.h>
 +#include <dcp/types.h>
  #include "kdm.h"
  #include "cinema.h"
  #include "exceptions.h"
  #include "util.h"
  #include "film.h"
  #include "config.h"
+ #include "safe_stringstream.h"
  
  using std::list;
  using std::string;
- using std::stringstream;
  using std::cout;
  using boost::shared_ptr;
  
  struct ScreenKDM
  {
 -      ScreenKDM (shared_ptr<Screen> s, libdcp::KDM k)
 +      ScreenKDM (shared_ptr<Screen> s, dcp::EncryptedKDM k)
                : screen (s)
                , kdm (k)
        {}
        
        shared_ptr<Screen> screen;
 -      libdcp::KDM kdm;
 +      dcp::EncryptedKDM kdm;
  };
  
  static string
@@@ -105,17 -104,17 +105,17 @@@ make_screen_kdms 
        shared_ptr<const Film> film,
        list<shared_ptr<Screen> > screens,
        boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to,
 -      libdcp::KDM::Formulation formulation
 +      dcp::LocalTime from,
 +      dcp::LocalTime to,
 +      dcp::Formulation formulation
        )
  {
 -      list<libdcp::KDM> kdms = film->make_kdms (screens, cpl, from, to, formulation);
 +      list<dcp::EncryptedKDM> kdms = film->make_kdms (screens, cpl, from, to, formulation);
           
        list<ScreenKDM> screen_kdms;
        
        list<shared_ptr<Screen> >::iterator i = screens.begin ();
 -      list<libdcp::KDM>::iterator j = kdms.begin ();
 +      list<dcp::EncryptedKDM>::iterator j = kdms.begin ();
        while (i != screens.end() && j != kdms.end ()) {
                screen_kdms.push_back (ScreenKDM (*i, *j));
                ++i;
@@@ -130,9 -129,9 +130,9 @@@ make_cinema_kdms 
        shared_ptr<const Film> film,
        list<shared_ptr<Screen> > screens,
        boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to,
 -      libdcp::KDM::Formulation formulation
 +      dcp::LocalTime from,
 +      dcp::LocalTime to,
 +      dcp::Formulation formulation
        )
  {
        list<ScreenKDM> screen_kdms = make_screen_kdms (film, screens, cpl, from, to, formulation);
@@@ -176,9 -175,9 +176,9 @@@ write_kdm_files 
        shared_ptr<const Film> film,
        list<shared_ptr<Screen> > screens,
        boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to,
 -      libdcp::KDM::Formulation formulation,
 +      dcp::LocalTime from,
 +      dcp::LocalTime to,
 +      dcp::Formulation formulation,
        boost::filesystem::path directory
        )
  {
@@@ -197,9 -196,9 +197,9 @@@ write_kdm_zip_files 
        shared_ptr<const Film> film,
        list<shared_ptr<Screen> > screens,
        boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to,
 -      libdcp::KDM::Formulation formulation,
 +      dcp::LocalTime from,
 +      dcp::LocalTime to,
 +      dcp::Formulation formulation,
        boost::filesystem::path directory
        )
  {
@@@ -217,9 -216,9 +217,9 @@@ email_kdms 
        shared_ptr<const Film> film,
        list<shared_ptr<Screen> > screens,
        boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to,
 -      libdcp::KDM::Formulation formulation
 +      dcp::LocalTime from,
 +      dcp::LocalTime to,
 +      dcp::Formulation formulation
        )
  {
        list<CinemaKDMs> cinema_kdms = make_cinema_kdms (film, screens, cpl, from, to, formulation);
                
                quickmail_initialize ();
  
-               stringstream start;
+               SafeStringStream start;
                start << from.date() << " " << from.time_of_day();
-               stringstream end;
+               SafeStringStream end;
                end << to.date() << " " << to.time_of_day();
                
                string subject = Config::instance()->kdm_subject();
                boost::algorithm::replace_all (body, "$END_TIME", end.str ());
                boost::algorithm::replace_all (body, "$CINEMA_NAME", i->cinema->name);
                
-               stringstream screens;
+               SafeStringStream screens;
                for (list<ScreenKDM>::const_iterator j = i->screen_kdms.begin(); j != i->screen_kdms.end(); ++j) {
                        screens << j->screen->name << ", ";
                }
diff --combined src/lib/server.cc
index 5598ef69f8388aae66098669ad9361e369691ab9,9591be1886414dc77f6f335dbb976c0c857bad93..d2c573c1b404c2e37f07269897511be56f327851
  
  #include <string>
  #include <vector>
- #include <sstream>
  #include <iostream>
  #include <boost/algorithm/string.hpp>
  #include <boost/scoped_array.hpp>
  #include <libcxml/cxml.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "server.h"
  #include "util.h"
  #include "scaler.h"
  #include "image.h"
 -#include "dcp_video_frame.h"
 +#include "dcp_video.h"
  #include "config.h"
  #include "cross.h"
 -#include "player_video_frame.h"
 +#include "player_video.h"
 +#include "encoded_data.h"
+ #include "safe_stringstream.h"
  
  #include "i18n.h"
  
@@@ -48,7 -47,6 +48,6 @@@
  #define LOG_ERROR_NC(...)   _log->log (__VA_ARGS__, Log::TYPE_ERROR);
  
  using std::string;
- using std::stringstream;
  using std::multimap;
  using std::vector;
  using std::list;
@@@ -63,37 -61,16 +62,37 @@@ using boost::thread
  using boost::bind;
  using boost::scoped_array;
  using boost::optional;
 -using libdcp::Size;
 -using libdcp::raw_convert;
 +using dcp::Size;
 +using dcp::raw_convert;
  
  Server::Server (shared_ptr<Log> log, bool verbose)
 -      : _log (log)
 +      : _terminate (false)
 +      , _log (log)
        , _verbose (verbose)
 +      , _acceptor (_io_service, boost::asio::ip::tcp::endpoint (boost::asio::ip::tcp::v4(), Config::instance()->server_port_base()))
  {
  
  }
  
 +Server::~Server ()
 +{
 +      {
 +              boost::mutex::scoped_lock lm (_worker_mutex);
 +              _terminate = true;
 +              _empty_condition.notify_all ();
 +      }
 +
 +      for (vector<boost::thread*>::iterator i = _worker_threads.begin(); i != _worker_threads.end(); ++i) {
 +              (*i)->join ();
 +              delete *i;
 +      }
 +
 +      _io_service.stop ();
 +
 +      _broadcast.io_service.stop ();
 +      _broadcast.thread->join ();
 +}
 +
  /** @param after_read Filled in with gettimeofday() after reading the input from the network.
   *  @param after_encode Filled in with gettimeofday() after encoding the image.
   */
@@@ -104,18 -81,18 +103,18 @@@ Server::process (shared_ptr<Socket> soc
        scoped_array<char> buffer (new char[length]);
        socket->read (reinterpret_cast<uint8_t*> (buffer.get()), length);
  
-       stringstream s (buffer.get());
+       string s (buffer.get());
        shared_ptr<cxml::Document> xml (new cxml::Document ("EncodingRequest"));
-       xml->read_stream (s);
+       xml->read_string (s);
        if (xml->number_child<int> ("Version") != SERVER_LINK_VERSION) {
                cerr << "Mismatched server/client versions\n";
                LOG_ERROR_NC ("Mismatched server/client versions");
                return -1;
        }
  
 -      shared_ptr<PlayerVideoFrame> pvf (new PlayerVideoFrame (xml, socket, _log));
 +      shared_ptr<PlayerVideo> pvf (new PlayerVideo (xml, socket, _log));
  
 -      DCPVideoFrame dcp_video_frame (pvf, xml, _log);
 +      DCPVideo dcp_video_frame (pvf, xml, _log);
  
        gettimeofday (&after_read, 0);
        
@@@ -139,14 -116,10 +138,14 @@@ Server::worker_thread (
  {
        while (true) {
                boost::mutex::scoped_lock lock (_worker_mutex);
 -              while (_queue.empty ()) {
 +              while (_queue.empty () && !_terminate) {
                        _empty_condition.wait (lock);
                }
  
 +              if (_terminate) {
 +                      return;
 +              }
 +
                shared_ptr<Socket> socket = _queue.front ();
                _queue.pop_front ();
                
                        struct timeval end;
                        gettimeofday (&end, 0);
  
-                       stringstream message;
+                       SafeStringStream message;
                        message.precision (2);
                        message << fixed
                                << "Encoded frame " << frame << " from " << ip << ": "
@@@ -213,18 -186,39 +212,18 @@@ Server::run (int num_threads
  
        _broadcast.thread = new thread (bind (&Server::broadcast_thread, this));
        
 -      boost::asio::io_service io_service;
 -
 -      boost::asio::ip::tcp::acceptor acceptor (
 -              io_service,
 -              boost::asio::ip::tcp::endpoint (boost::asio::ip::tcp::v4(), Config::instance()->server_port_base ())
 -              );
 -      
 -      while (true) {
 -              shared_ptr<Socket> socket (new Socket);
 -              acceptor.accept (socket->socket ());
 -
 -              boost::mutex::scoped_lock lock (_worker_mutex);
 -              
 -              /* Wait until the queue has gone down a bit */
 -              while (int (_queue.size()) >= num_threads * 2) {
 -                      _full_condition.wait (lock);
 -              }
 -              
 -              _queue.push_back (socket);
 -              _empty_condition.notify_all ();
 -      }
 +      start_accept ();
 +      _io_service.run ();
  }
  
  void
  Server::broadcast_thread ()
  try
  {
 -      boost::asio::io_service io_service;
 -
        boost::asio::ip::address address = boost::asio::ip::address_v4::any ();
        boost::asio::ip::udp::endpoint listen_endpoint (address, Config::instance()->server_port_base() + 1);
  
 -      _broadcast.socket = new boost::asio::ip::udp::socket (io_service);
 +      _broadcast.socket = new boost::asio::ip::udp::socket (_broadcast.io_service);
        _broadcast.socket->open (listen_endpoint.protocol ());
        _broadcast.socket->bind (listen_endpoint);
  
                boost::bind (&Server::broadcast_received, this)
                );
  
 -      io_service.run ();
 +      _broadcast.io_service.run ();
  }
  catch (...)
  {
@@@ -251,14 -245,13 +250,13 @@@ Server::broadcast_received (
                xmlpp::Document doc;
                xmlpp::Element* root = doc.create_root_node ("ServerAvailable");
                root->add_child("Threads")->add_child_text (raw_convert<string> (_worker_threads.size ()));
-               stringstream xml;
-               doc.write_to_stream (xml, "UTF-8");
+               string xml = doc.write_to_string ("UTF-8");
  
                shared_ptr<Socket> socket (new Socket);
                try {
                        socket->connect (boost::asio::ip::tcp::endpoint (_broadcast.send_endpoint.address(), Config::instance()->server_port_base() + 1));
-                       socket->write (xml.str().length() + 1);
-                       socket->write ((uint8_t *) xml.str().c_str(), xml.str().length() + 1);
+                       socket->write (xml.length() + 1);
+                       socket->write ((uint8_t *) xml.c_str(), xml.length() + 1);
                } catch (...) {
  
                }
                _broadcast.send_endpoint, boost::bind (&Server::broadcast_received, this)
                );
  }
 +
 +void
 +Server::start_accept ()
 +{
 +      if (_terminate) {
 +              return;
 +      }
 +
 +      shared_ptr<Socket> socket (new Socket);
 +      _acceptor.async_accept (socket->socket (), boost::bind (&Server::handle_accept, this, socket, boost::asio::placeholders::error));
 +}
 +
 +void
 +Server::handle_accept (shared_ptr<Socket> socket, boost::system::error_code const & error)
 +{
 +      if (error) {
 +              return;
 +      }
 +
 +      boost::mutex::scoped_lock lock (_worker_mutex);
 +      
 +      /* Wait until the queue has gone down a bit */
 +      while (_queue.size() >= _worker_threads.size() * 2 && !_terminate) {
 +              _full_condition.wait (lock);
 +      }
 +      
 +      _queue.push_back (socket);
 +      _empty_condition.notify_all ();
 +
 +      start_accept ();
 +}
 +      
diff --combined src/lib/server_finder.cc
index 9b0cd037679f9351c5a9112537dbef283235a4ed,744a65f597300fcc809b6daab70e32f8ccd0f0a0..14cb3af5999ac5ff7ccb434d39371addcbb18a6d
@@@ -18,7 -18,7 +18,7 @@@
  */
  
  #include <libcxml/cxml.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "server_finder.h"
  #include "exceptions.h"
  #include "util.h"
  #include "ui_signaller.h"
  
  using std::string;
- using std::stringstream;
  using std::list;
  using std::vector;
  using std::cout;
  using boost::shared_ptr;
  using boost::scoped_array;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  ServerFinder* ServerFinder::_instance = 0;
  
@@@ -116,9 -115,9 +115,9 @@@ tr
                scoped_array<char> buffer (new char[length]);
                sock->read (reinterpret_cast<uint8_t*> (buffer.get()), length);
                
-               stringstream s (buffer.get());
+               string s (buffer.get());
                shared_ptr<cxml::Document> xml (new cxml::Document ("ServerAvailable"));
-               xml->read_stream (s);
+               xml->read_string (s);
  
                string const ip = sock->socket().remote_endpoint().address().to_string ();
                if (!server_found (ip)) {
index a573d43c407f8ac8a825190a1784600ba37ac873,ba3bd0a772d489714a01bebd7c90d56ab1b063e6..1a17976657c42759a10566aaec00559de94e1ba9
  */
  
  #include <libcxml/cxml.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "sndfile_content.h"
  #include "sndfile_decoder.h"
  #include "film.h"
  #include "compose.hpp"
  #include "job.h"
  #include "util.h"
+ #include "safe_stringstream.h"
  
  #include "i18n.h"
  
  using std::string;
- using std::stringstream;
  using std::cout;
  using boost::shared_ptr;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  SndfileContent::SndfileContent (shared_ptr<const Film> f, boost::filesystem::path p)
        : Content (f, p)
 -      , AudioContent (f, p)
 -      , _audio_channels (0)
 -      , _audio_length (0)
 -      , _audio_frame_rate (0)
 +      , SingleStreamAudioContent (f, p)
  {
  
  }
  
 -SndfileContent::SndfileContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version)
 +SndfileContent::SndfileContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version)
        : Content (f, node)
 -      , AudioContent (f, node)
 -      , _audio_mapping (node->node_child ("AudioMapping"), version)
 +      , SingleStreamAudioContent (f, node, version)
  {
 -      _audio_channels = node->number_child<int> ("AudioChannels");
 -      _audio_length = node->number_child<AudioContent::Frame> ("AudioLength");
 -      _audio_frame_rate = node->number_child<int> ("AudioFrameRate");
 +      
  }
  
 +void
 +SndfileContent::as_xml (xmlpp::Node* node) const
 +{
 +      node->add_child("Type")->add_child_text ("Sndfile");
 +      Content::as_xml (node);
 +      SingleStreamAudioContent::as_xml (node);
 +}
 +
 +
  string
  SndfileContent::summary () const
  {
@@@ -79,13 -76,13 +79,13 @@@ SndfileContent::information () cons
                return "";
        }
        
-       stringstream s;
+       SafeStringStream s;
  
        s << String::compose (
                _("%1 channels, %2kHz, %3 samples"),
                audio_channels(),
 -              content_audio_frame_rate() / 1000.0,
 -              audio_length()
 +              audio_frame_rate() / 1000.0,
 +              audio_length().frames (audio_frame_rate ())
                );
        
        return s.str ();
@@@ -105,15 -102,69 +105,15 @@@ SndfileContent::examine (shared_ptr<Job
  {
        job->set_progress_unknown ();
        Content::examine (job);
 -
 -      shared_ptr<const Film> film = _film.lock ();
 -      assert (film);
 -
 -      SndfileDecoder dec (film, shared_from_this());
 -
 -      {
 -              boost::mutex::scoped_lock lm (_mutex);
 -              _audio_channels = dec.audio_channels ();
 -              _audio_length = dec.audio_length ();
 -              _audio_frame_rate = dec.audio_frame_rate ();
 -      }
 -
 -      signal_changed (AudioContentProperty::AUDIO_CHANNELS);
 -      signal_changed (AudioContentProperty::AUDIO_LENGTH);
 -      signal_changed (AudioContentProperty::AUDIO_FRAME_RATE);
 -
 -      {
 -              boost::mutex::scoped_lock lm (_mutex);
 -              /* XXX: do this in signal_changed...? */
 -              _audio_mapping = AudioMapping (_audio_channels);
 -              _audio_mapping.make_default ();
 -      }
 -      
 -      signal_changed (AudioContentProperty::AUDIO_MAPPING);
 -}
 -
 -void
 -SndfileContent::as_xml (xmlpp::Node* node) const
 -{
 -      node->add_child("Type")->add_child_text ("Sndfile");
 -      Content::as_xml (node);
 -      AudioContent::as_xml (node);
 -
 -      node->add_child("AudioChannels")->add_child_text (raw_convert<string> (audio_channels ()));
 -      node->add_child("AudioLength")->add_child_text (raw_convert<string> (audio_length ()));
 -      node->add_child("AudioFrameRate")->add_child_text (raw_convert<string> (content_audio_frame_rate ()));
 -      _audio_mapping.as_xml (node->add_child("AudioMapping"));
 +      shared_ptr<AudioExaminer> dec (new SndfileDecoder (shared_from_this()));
 +      take_from_audio_examiner (dec);
  }
  
 -Time
 +DCPTime
  SndfileContent::full_length () const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
 -
 -      FrameRateChange frc = film->active_frame_rate_change (position ());
 -
 -      OutputAudioFrame const len = divide_with_round (
 -              audio_length() * output_audio_frame_rate() * frc.source,
 -              content_audio_frame_rate() * film->video_frame_rate()
 -              );
 -      
 -      return film->audio_frames_to_time (len);
 +      return DCPTime (audio_length(), film->active_frame_rate_change (position ()));
  }
  
 -void
 -SndfileContent::set_audio_mapping (AudioMapping m)
 -{
 -      {
 -              boost::mutex::scoped_lock lm (_mutex);
 -              _audio_mapping = m;
 -      }
 -
 -      signal_changed (AudioContentProperty::AUDIO_MAPPING);
 -}
diff --combined src/lib/transcode_job.cc
index 1a162b6544ad1ba6442acbfbdcaab87d52b4bd95,831c74b3b244d9a78b4369eb3d089852c74c9174..23a46d06dddcb7875a9247911d387028a0da802e
@@@ -27,6 -27,7 +27,7 @@@
  #include "film.h"
  #include "transcoder.h"
  #include "log.h"
+ #include "safe_stringstream.h"
  
  #include "i18n.h"
  
  #define LOG_ERROR_NC(...)   _film->log()->log (__VA_ARGS__, Log::TYPE_ERROR);
  
  using std::string;
- using std::stringstream;
  using std::fixed;
  using std::setprecision;
 +using std::cout;
  using boost::shared_ptr;
  
  /** @param s Film to use.
@@@ -90,7 -89,7 +90,7 @@@ TranscodeJob::status () cons
                return Job::status ();
        }
  
-       stringstream s;
+       SafeStringStream s;
  
        s << Job::status ();
  
        return s.str ();
  }
  
 +/** @return Approximate remaining time in seconds */
  int
  TranscodeJob::remaining_time () const
  {
        }
  
        /* Compute approximate proposed length here, as it's only here that we need it */
 -      OutputVideoFrame const left = _film->time_to_video_frames (_film->length ()) - t->video_frames_out();
 -      return left / fps;
 +      return (_film->length().frames (_film->video_frame_rate ()) - t->video_frames_out()) / fps;
  }
diff --combined src/lib/update.cc
index 44ecbb232c26b03214f23d3191c54fedd9a9071a,7bec061e9292e15d044c360e8c13d60dc6a0faab..0146df48430deff9a410763e6a46bf7894b9af4d
  */
  
  #include <string>
- #include <sstream>
  #include <boost/algorithm/string.hpp>
  #include <curl/curl.h>
  #include <libcxml/cxml.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "update.h"
  #include "version.h"
  #include "ui_signaller.h"
+ #include "safe_stringstream.h"
  
  #define BUFFER_SIZE 1024
  
  using std::cout;
  using std::min;
  using std::string;
 -using libdcp::raw_convert;
 +using std::stringstream;
 +using dcp::raw_convert;
  
 +/** Singleton instance */
  UpdateChecker* UpdateChecker::_instance = 0;
  
  static size_t
@@@ -44,9 -42,6 +44,9 @@@ write_callback_wrapper (void* data, siz
        return reinterpret_cast<UpdateChecker*>(user)->write_callback (data, size, nmemb);
  }
  
 +/** Construct an UpdateChecker.  This sets things up and starts a thread to
 + *  do the work.
 + */
  UpdateChecker::UpdateChecker ()
        : _buffer (new char[BUFFER_SIZE])
        , _offset (0)
@@@ -78,7 -73,6 +78,7 @@@ UpdateChecker::~UpdateChecker (
        delete[] _buffer;
  }
  
 +/** Start running the update check */
  void
  UpdateChecker::run ()
  {
@@@ -91,7 -85,6 +91,7 @@@ voi
  UpdateChecker::thread ()
  {
        while (true) {
 +              /* Block until there is something to do */
                boost::mutex::scoped_lock lock (_process_mutex);
                while (_to_do == 0) {
                        _condition.wait (lock);
                
                try {
                        _offset = 0;
 +
 +                      /* Perform the request */
                        
                        int r = curl_easy_perform (_curl);
                        if (r != CURLE_OK) {
                                set_state (FAILED);
                                return;
                        }
 +
 +                      /* Parse the reply */
                        
                        _buffer[_offset] = '\0';
-                       stringstream s;
-                       s << _buffer;
+                       string s (_buffer);
                        cxml::Document doc ("Update");
-                       doc.read_stream (s);
+                       doc.read_string (s);
                        
                        {
                                boost::mutex::scoped_lock lm (_data_mutex);
diff --combined src/lib/util.cc
index 60953b544d1a0c26b3816c674db3f9612b4ee181,d96001d13916fcf3eda519c625ce0d68f15b512a..c9685aa44ff3ee811e1f7e77e052b8047ca8ff1d
@@@ -22,7 -22,6 +22,6 @@@
   *  @brief Some utility functions and classes.
   */
  
- #include <sstream>
  #include <iomanip>
  #include <iostream>
  #include <fstream>
  #endif
  #include <glib.h>
  #include <openjpeg.h>
 +#include <pangomm/init.h>
  #include <magick/MagickCore.h>
  #include <magick/version.h>
 -#include <libdcp/version.h>
 -#include <libdcp/util.h>
 -#include <libdcp/signer_chain.h>
 -#include <libdcp/signer.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/version.h>
 +#include <dcp/util.h>
 +#include <dcp/signer.h>
 +#include <dcp/raw_convert.h>
  extern "C" {
  #include <libavcodec/avcodec.h>
  #include <libavformat/avformat.h>
  #include "scaler.h"
  #include "dcp_content_type.h"
  #include "filter.h"
 -#include "sound_processor.h"
 +#include "cinema_sound_processor.h"
  #include "config.h"
  #include "ratio.h"
  #include "job.h"
  #include "cross.h"
  #include "video_content.h"
 +#include "rect.h"
  #include "md5_digester.h"
 +#include "audio_processor.h"
+ #include "safe_stringstream.h"
  #ifdef DCPOMATIC_WINDOWS
  #include "stack.hpp"
  #endif
@@@ -79,7 -77,6 +79,6 @@@
  #include "i18n.h"
  
  using std::string;
- using std::stringstream;
  using std::setfill;
  using std::ostream;
  using std::endl;
@@@ -102,8 -99,8 +101,8 @@@ using std::set_terminate
  using boost::shared_ptr;
  using boost::thread;
  using boost::optional;
 -using libdcp::Size;
 -using libdcp::raw_convert;
 +using dcp::Size;
 +using dcp::raw_convert;
  
  static boost::thread::id ui_thread;
  static boost::filesystem::path backtrace_file;
@@@ -123,7 -120,7 +122,7 @@@ seconds_to_hms (int s
        int h = m / 60;
        m -= (h * 60);
  
-       stringstream hms;
+       SafeStringStream hms;
        hms << h << N_(":");
        hms.width (2);
        hms << std::setfill ('0') << m << N_(":");
@@@ -144,7 -141,7 +143,7 @@@ seconds_to_approximate_hms (int s
        int h = m / 60;
        m -= (h * 60);
  
-       stringstream ap;
+       SafeStringStream ap;
  
        bool const hours = h > 0;
        bool const minutes = h < 10 && m > 0;
@@@ -263,11 -260,29 +262,11 @@@ stacktrace (ostream& out, int levels
  static string
  ffmpeg_version_to_string (int v)
  {
-       stringstream s;
+       SafeStringStream s;
        s << ((v & 0xff0000) >> 16) << N_(".") << ((v & 0xff00) >> 8) << N_(".") << (v & 0xff);
        return s.str ();
  }
  
 -/** Return a user-readable string summarising the versions of our dependencies */
 -string
 -dependency_version_summary ()
 -{
 -      SafeStringStream s;
 -      s << N_("libopenjpeg ") << opj_version () << N_(", ")
 -        << N_("libavcodec ") << ffmpeg_version_to_string (avcodec_version()) << N_(", ")
 -        << N_("libavfilter ") << ffmpeg_version_to_string (avfilter_version()) << N_(", ")
 -        << N_("libavformat ") << ffmpeg_version_to_string (avformat_version()) << N_(", ")
 -        << N_("libavutil ") << ffmpeg_version_to_string (avutil_version()) << N_(", ")
 -        << N_("libswscale ") << ffmpeg_version_to_string (swscale_version()) << N_(", ")
 -        << MagickVersion << N_(", ")
 -        << N_("libssh ") << ssh_version (0) << N_(", ")
 -        << N_("libdcp ") << libdcp::version << N_(" git ") << libdcp::git_commit;
 -
 -      return s.str ();
 -}
 -
  double
  seconds (struct timeval t)
  {
@@@ -356,16 -371,14 +355,16 @@@ dcpomatic_setup (
  
        set_terminate (terminate);
  
 -      libdcp::init ();
 +      Pango::init ();
 +      dcp::init ();
        
        Ratio::setup_ratios ();
        VideoContentScale::setup_scales ();
        DCPContentType::setup_dcp_content_types ();
        Scaler::setup_scalers ();
        Filter::setup_filters ();
 -      SoundProcessor::setup_sound_processors ();
 +      CinemaSoundProcessor::setup_cinema_sound_processors ();
 +      AudioProcessor::setup_audio_processors ();
  
        ui_thread = boost::this_thread::get_id ();
  }
@@@ -636,17 -649,6 +635,17 @@@ stride_round_up (int c, int const * str
        return a - (a % t);
  }
  
 +/** @param n A number.
 + *  @param r Rounding `boundary' (must be a power of 2)
 + *  @return n rounded to the nearest r
 + */
 +int
 +round_to (float n, int r)
 +{
 +      assert (r == 1 || r == 2 || r == 4);
 +      return int (n + float(r) / 2) &~ (r - 1);
 +}
 +
  /** Read a sequence of key / value pairs from a text stream;
   *  the keys are the first words on the line, and the values are
   *  the remainder of the line following the key.  Lines beginning
@@@ -751,6 -753,17 +750,6 @@@ ensure_ui_thread (
        assert (boost::this_thread::get_id() == ui_thread);
  }
  
 -/** @param v Content video frame.
 - *  @param audio_sample_rate Source audio sample rate.
 - *  @param frames_per_second Number of video frames per second.
 - *  @return Equivalent number of audio frames for `v'.
 - */
 -int64_t
 -video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second)
 -{
 -      return ((int64_t) v * audio_sample_rate / frames_per_second);
 -}
 -
  string
  audio_channel_name (int c)
  {
@@@ -801,6 -814,59 +800,6 @@@ tidy_for_filename (string f
        return t;
  }
  
 -shared_ptr<const libdcp::Signer>
 -make_signer ()
 -{
 -      boost::filesystem::path const sd = Config::instance()->signer_chain_directory ();
 -
 -      /* Remake the chain if any of it is missing */
 -      
 -      list<boost::filesystem::path> files;
 -      files.push_back ("ca.self-signed.pem");
 -      files.push_back ("intermediate.signed.pem");
 -      files.push_back ("leaf.signed.pem");
 -      files.push_back ("leaf.key");
 -
 -      list<boost::filesystem::path>::const_iterator i = files.begin();
 -      while (i != files.end()) {
 -              boost::filesystem::path p (sd);
 -              p /= *i;
 -              if (!boost::filesystem::exists (p)) {
 -                      boost::filesystem::remove_all (sd);
 -                      boost::filesystem::create_directories (sd);
 -                      libdcp::make_signer_chain (sd, openssl_path ());
 -                      break;
 -              }
 -
 -              ++i;
 -      }
 -      
 -      libdcp::CertificateChain chain;
 -
 -      {
 -              boost::filesystem::path p (sd);
 -              p /= "ca.self-signed.pem";
 -              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p)));
 -      }
 -
 -      {
 -              boost::filesystem::path p (sd);
 -              p /= "intermediate.signed.pem";
 -              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p)));
 -      }
 -
 -      {
 -              boost::filesystem::path p (sd);
 -              p /= "leaf.signed.pem";
 -              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p)));
 -      }
 -
 -      boost::filesystem::path signer_key (sd);
 -      signer_key /= "leaf.key";
 -
 -      return shared_ptr<const libdcp::Signer> (new libdcp::Signer (chain, signer_key));
 -}
 -
  map<string, string>
  split_get_request (string url)
  {
        return r;
  }
  
 -libdcp::Size
 -fit_ratio_within (float ratio, libdcp::Size full_frame)
 +dcp::Size
 +fit_ratio_within (float ratio, dcp::Size full_frame, int round)
  {
        if (ratio < full_frame.ratio ()) {
 -              return libdcp::Size (rint (full_frame.height * ratio), full_frame.height);
 +              return dcp::Size (round_to (full_frame.height * ratio, round), full_frame.height);
        }
        
 -      return libdcp::Size (full_frame.width, rint (full_frame.width / ratio));
 +      return dcp::Size (full_frame.width, round_to (full_frame.width / ratio, round));
  }
  
  void *
@@@ -885,34 -951,12 +884,34 @@@ divide_with_round (int64_t a, int64_t b
        }
  }
  
-       stringstream s;
 +/** Return a user-readable string summarising the versions of our dependencies */
 +string
 +dependency_version_summary ()
 +{
++      SafeStringStream s;
 +      s << N_("libopenjpeg ") << opj_version () << N_(", ")
 +        << N_("libavcodec ") << ffmpeg_version_to_string (avcodec_version()) << N_(", ")
 +        << N_("libavfilter ") << ffmpeg_version_to_string (avfilter_version()) << N_(", ")
 +        << N_("libavformat ") << ffmpeg_version_to_string (avformat_version()) << N_(", ")
 +        << N_("libavutil ") << ffmpeg_version_to_string (avutil_version()) << N_(", ")
 +        << N_("libswscale ") << ffmpeg_version_to_string (swscale_version()) << N_(", ")
 +        << MagickVersion << N_(", ")
 +        << N_("libssh ") << ssh_version (0) << N_(", ")
 +        << N_("libdcp ") << dcp::version << N_(" git ") << dcp::git_commit;
 +
 +      return s.str ();
 +}
 +
 +/** Construct a ScopedTemporary.  A temporary filename is decided but the file is not opened
 + *  until ::open() is called.
 + */
  ScopedTemporary::ScopedTemporary ()
        : _open (0)
  {
        _file = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
  }
  
 +/** Close and delete the temporary file */
  ScopedTemporary::~ScopedTemporary ()
  {
        close ();       
        boost::filesystem::remove (_file, ec);
  }
  
 +/** @return temporary filename */
  char const *
  ScopedTemporary::c_str () const
  {
        return _file.string().c_str ();
  }
  
 +/** Open the temporary file.
 + *  @return File's FILE pointer.
 + */
  FILE*
  ScopedTemporary::open (char const * params)
  {
        return _open;
  }
  
 +/** Close the file */
  void
  ScopedTemporary::close ()
  {
                _open = 0;
        }
  }
 +
 +ContentTimePeriod
 +subtitle_period (AVSubtitle const & sub)
 +{
 +      ContentTime const packet_time = ContentTime::from_seconds (static_cast<double> (sub.pts) / AV_TIME_BASE);
 +
 +      ContentTimePeriod period (
 +              packet_time + ContentTime::from_seconds (sub.start_display_time / 1e3),
 +              packet_time + ContentTime::from_seconds (sub.end_display_time / 1e3)
 +              );
 +
 +      return period;
 +}
diff --combined src/lib/video_content.cc
index 0a3e378eecb1fda2f05ef3b6894659c6849386fe,b0d4c2de506cb2b4abd828639365b8d786342354..796e6575a2e62f2088affaafea41a7240adb866b
@@@ -19,8 -19,8 +19,8 @@@
  
  #include <iomanip>
  #include <libcxml/cxml.h>
 -#include <libdcp/colour_matrix.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/colour_matrix.h>
 +#include <dcp/raw_convert.h>
  #include "video_content.h"
  #include "video_examiner.h"
  #include "compose.hpp"
  #include "film.h"
  #include "exceptions.h"
  #include "frame_rate_change.h"
 +#include "log.h"
+ #include "safe_stringstream.h"
  
  #include "i18n.h"
  
 +#define LOG_GENERAL(...) film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
 +
  int const VideoContentProperty::VIDEO_SIZE      = 0;
  int const VideoContentProperty::VIDEO_FRAME_RATE  = 1;
  int const VideoContentProperty::VIDEO_FRAME_TYPE  = 2;
@@@ -45,7 -43,6 +46,6 @@@ int const VideoContentProperty::VIDEO_S
  int const VideoContentProperty::COLOUR_CONVERSION = 5;
  
  using std::string;
- using std::stringstream;
  using std::setprecision;
  using std::cout;
  using std::vector;
@@@ -54,13 -51,14 +54,13 @@@ using std::max
  using boost::shared_ptr;
  using boost::optional;
  using boost::dynamic_pointer_cast;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  vector<VideoContentScale> VideoContentScale::_scales;
  
  VideoContent::VideoContent (shared_ptr<const Film> f)
        : Content (f)
        , _video_length (0)
 -      , _original_video_frame_rate (0)
        , _video_frame_rate (0)
        , _video_frame_type (VIDEO_FRAME_TYPE_2D)
        , _scale (Config::instance()->default_scale ())
        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)
 -      , _original_video_frame_rate (0)
        , _video_frame_rate (0)
        , _video_frame_type (VIDEO_FRAME_TYPE_2D)
        , _scale (Config::instance()->default_scale ())
@@@ -81,6 -80,7 +81,6 @@@
  VideoContent::VideoContent (shared_ptr<const Film> f, boost::filesystem::path p)
        : Content (f, p)
        , _video_length (0)
 -      , _original_video_frame_rate (0)
        , _video_frame_rate (0)
        , _video_frame_type (VIDEO_FRAME_TYPE_2D)
        , _scale (Config::instance()->default_scale ())
        setup_default_colour_conversion ();
  }
  
 -VideoContent::VideoContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version)
 +VideoContent::VideoContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version)
        : Content (f, node)
  {
 -      _video_length = node->number_child<VideoContent::Frame> ("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");
 -      _original_video_frame_rate = node->optional_number_child<float> ("OriginalVideoFrameRate").get_value_or (_video_frame_rate);
 +
 +      if (version < 32) {
 +              /* DCP-o-matic 1.0 branch */
 +              _video_length = ContentTime::from_frames (node->number_child<int64_t> ("VideoLength"), _video_frame_rate);
 +      } else {
 +              _video_length = ContentTime (node->number_child<ContentTime::Type> ("VideoLength"));
 +      }
 +      
        _video_frame_type = static_cast<VideoFrameType> (node->number_child<int> ("VideoFrameType"));
        _crop.left = node->number_child<int> ("LeftCrop");
        _crop.right = node->number_child<int> ("RightCrop");
@@@ -158,6 -152,7 +158,6 @@@ VideoContent::VideoContent (shared_ptr<
        }
  
        _video_size = ref->video_size ();
 -      _original_video_frame_rate = ref->original_video_frame_rate ();
        _video_frame_rate = ref->video_frame_rate ();
        _video_frame_type = ref->video_frame_type ();
        _crop = ref->crop ();
@@@ -169,10 -164,11 +169,10 @@@ voi
  VideoContent::as_xml (xmlpp::Node* node) const
  {
        boost::mutex::scoped_lock lm (_mutex);
 -      node->add_child("VideoLength")->add_child_text (raw_convert<string> (_video_length));
 +      node->add_child("VideoLength")->add_child_text (raw_convert<string> (_video_length.get ()));
        node->add_child("VideoWidth")->add_child_text (raw_convert<string> (_video_size.width));
        node->add_child("VideoHeight")->add_child_text (raw_convert<string> (_video_size.height));
        node->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate));
 -      node->add_child("OriginalVideoFrameRate")->add_child_text (raw_convert<string> (_original_video_frame_rate));
        node->add_child("VideoFrameType")->add_child_text (raw_convert<string> (static_cast<int> (_video_frame_type)));
        _crop.as_xml (node);
        _scale.as_xml (node->add_child("Scale"));
  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 ();
 -      
 +      ContentTime vl = d->video_length ();
 +
        {
                boost::mutex::scoped_lock lm (_mutex);
                _video_size = vs;
                _video_frame_rate = vfr;
 -              _original_video_frame_rate = vfr;
 +              _video_length = vl;
        }
 +
 +      shared_ptr<const Film> film = _film.lock ();
 +      assert (film);
 +      LOG_GENERAL ("Video length obtained from header as %1 frames", _video_length.frames (_video_frame_rate));
        
        signal_changed (VideoContentProperty::VIDEO_SIZE);
        signal_changed (VideoContentProperty::VIDEO_FRAME_RATE);
 +      signal_changed (ContentProperty::LENGTH);
  }
  
  
@@@ -217,7 -207,7 +217,7 @@@ VideoContent::information () cons
                return "";
        }
        
-       stringstream s;
+       SafeStringStream s;
  
        s << String::compose (
                _("%1x%2 pixels (%3:1)"),
@@@ -309,7 -299,7 +309,7 @@@ VideoContent::set_scale (VideoContentSc
  string
  VideoContent::identifier () const
  {
-       stringstream s;
+       SafeStringStream s;
        s << Content::identifier()
          << "_" << crop().left
          << "_" << crop().right
@@@ -337,17 -327,14 +337,17 @@@ VideoContent::technical_summary () cons
  {
        return String::compose (
                "video: length %1, size %2x%3, rate %4",
 -              video_length_after_3d_combine(), video_size().width, video_size().height, video_frame_rate()
 +              video_length_after_3d_combine().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:
        case VIDEO_FRAME_TYPE_3D_ALTERNATE:
        case VIDEO_FRAME_TYPE_3D_RIGHT:
                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);
@@@ -375,21 -362,28 +375,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);
 -      
 -      FrameRateChange 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()));
  }
  
  void
@@@ -454,7 -448,7 +454,7 @@@ VideoContentScale::VideoContentScale (b
  
  }
  
 -VideoContentScale::VideoContentScale (shared_ptr<cxml::Node> node)
 +VideoContentScale::VideoContentScale (cxml::NodePtr node)
        : _ratio (0)
        , _scale (true)
  {
@@@ -479,7 -473,7 +479,7 @@@ VideoContentScale::as_xml (xmlpp::Node
  string
  VideoContentScale::id () const
  {
-       stringstream s;
+       SafeStringStream s;
        
        if (_ratio) {
                s << _ratio->id () << "_";
@@@ -507,26 -501,26 +507,26 @@@ VideoContentScale::name () cons
  /** @param display_container Size of the container that we are displaying this content in.
   *  @param film_container The size of the film's image.
   */
 -libdcp::Size
 -VideoContentScale::size (shared_ptr<const VideoContent> c, libdcp::Size display_container, libdcp::Size film_container) const
 +dcp::Size
 +VideoContentScale::size (shared_ptr<const VideoContent> c, dcp::Size display_container, dcp::Size film_container, int round) const
  {
        if (_ratio) {
 -              return fit_ratio_within (_ratio->ratio (), display_container);
 +              return fit_ratio_within (_ratio->ratio (), display_container, round);
        }
  
 -      libdcp::Size const ac = c->video_size_after_crop ();
 +      dcp::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 fit_ratio_within (ac.ratio (), display_container, 1);
        }
  
        /* 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
 +      return dcp::Size (
 +              round_to (c->video_size().width  * float(display_container.width)  / film_container.width, round),
 +              round_to (c->video_size().height * float(display_container.height) / film_container.height, round)
                );
  }
  
diff --combined src/lib/wscript
index 15f26c34f2dcc91cfeef95d19a142301eebd5e38,1e4efddc4fbb2e49986265a2a877f44344ecd930..2510ebe2a481b75d4d8b0c90f3916420f43350f7
@@@ -7,39 -7,26 +7,39 @@@ sources = ""
            audio_buffers.cc
            audio_content.cc
            audio_decoder.cc
 +          audio_filter.cc
            audio_mapping.cc
 +          audio_processor.cc
            cinema.cc
 +          cinema_sound_processor.cc
            colour_conversion.cc
            config.cc
            content.cc
            content_factory.cc
 +          content_subtitle.cc
            cross.cc
 +          dcp_content.cc
            dcp_content_type.cc
 -          dcp_video_frame.cc
 -          decoder.cc
 +          dcp_decoder.cc
 +          dcp_examiner.cc
 +          dcp_subtitle_content.cc
 +          dcp_subtitle_decoder.cc
 +          dcp_video.cc
 +          dcpomatic_time.cc
            dolby_cp750.cc
            encoder.cc
 +          encoded_data.cc
            examine_content_job.cc
            exceptions.cc
            file_group.cc
            filter_graph.cc
            ffmpeg.cc
 +          ffmpeg_audio_stream.cc
            ffmpeg_content.cc
            ffmpeg_decoder.cc
            ffmpeg_examiner.cc
 +          ffmpeg_stream.cc
 +          ffmpeg_subtitle_stream.cc
            film.cc
            filter.cc
            frame_rate_change.cc
            image_examiner.cc
            image_proxy.cc
            isdcf_metadata.cc
 +          j2k_image_proxy.cc
            job.cc
            job_manager.cc
            kdm.cc
            log.cc
 +          magick_image_proxy.cc
            md5_digester.cc
 -          piece.cc
 +          mid_side_decoder.cc
            player.cc
 -          player_video_frame.cc
 +          player_video.cc
            playlist.cc
            ratio.cc
 +          raw_image_proxy.cc
 +          render_subtitles.cc
            resampler.cc
+           safe_stringstream.cc
            scp_dcp_job.cc
            scaler.cc
            send_kdm_email_job.cc
            server.cc
            server_finder.cc
 +          single_stream_audio_content.cc
            sndfile_content.cc
            sndfile_decoder.cc
 -          sound_processor.cc
 -          subtitle.cc
 +          subrip.cc
 +          subrip_content.cc
 +          subrip_decoder.cc
            subtitle_content.cc
            subtitle_decoder.cc
            timer.cc
@@@ -84,7 -66,6 +85,7 @@@
            types.cc
            ui_signaller.cc
            update.cc
 +          upmixer_a.cc
            util.cc
            video_content.cc
            video_decoder.cc
@@@ -97,13 -78,13 +98,13 @@@ def build(bld)
      else:
          obj = bld(features = 'cxx cxxshlib')
  
 -    obj.name = 'libdcpomatic'
 +    obj.name = 'libdcpomatic2'
      obj.export_includes = ['..']
      obj.uselib = """
                   AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE SWRESAMPLE 
                   BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2
 -                 SNDFILE OPENJPEG POSTPROC TIFF MAGICK SSH DCP CXML GLIB LZMA XMLPP
 -                 CURL ZIP QUICKMAIL XMLSEC
 +                 SNDFILE OPENJPEG POSTPROC TIFF MAGICK SSH DCP CXML GLIB LZMA XML++
 +                 CURL ZIP QUICKMAIL PANGOMM CAIROMM XMLSEC
                   """
  
      if bld.env.TARGET_OSX:
      if bld.env.BUILD_STATIC:
          obj.uselib += ' XMLPP'
  
 -    obj.target = 'dcpomatic'
 +    obj.target = 'dcpomatic2'
  
 -    i18n.po_to_mo(os.path.join('src', 'lib'), 'libdcpomatic', bld)
 +    i18n.po_to_mo(os.path.join('src', 'lib'), 'libdcpomatic2', bld)
  
  def pot(bld):
      i18n.pot(os.path.join('src', 'lib'), sources, 'libdcpomatic')
diff --combined src/tools/dcpomatic.cc
index 8c7f09ae77de6fd4e8344ca12f3a528617991a6a,c92f5b6cf8b182004a9879076dd5b2305db8fd20..4246455378fb8c401aec5ac96206d54ff093ff0b
@@@ -30,7 -30,7 +30,7 @@@
  #include <wx/stdpaths.h>
  #include <wx/cmdline.h>
  #include <wx/preferences.h>
 -#include <libdcp/exceptions.h>
 +#include <dcp/exceptions.h>
  #include "wx/film_viewer.h"
  #include "wx/film_editor.h"
  #include "wx/job_manager_view.h"
@@@ -45,7 -45,6 +45,7 @@@
  #include "wx/servers_list_dialog.h"
  #include "wx/hints_dialog.h"
  #include "wx/update_dialog.h"
 +#include "wx/content_panel.h"
  #include "lib/film.h"
  #include "lib/config.h"
  #include "lib/util.h"
@@@ -65,7 -64,6 +65,6 @@@
  using std::cout;
  using std::string;
  using std::wstring;
- using std::stringstream;
  using std::map;
  using std::make_pair;
  using std::list;
@@@ -476,7 -474,7 +475,7 @@@ private
                                        shared_ptr<Job> (new SendKDMEmailJob (film, d->screens (), d->cpl (), d->from (), d->until (), d->formulation ()))
                                        );
                        }
 -              } catch (libdcp::NotEncryptedError& e) {
 +              } catch (dcp::NotEncryptedError& e) {
                        error_dialog (this, _("CPL's content is not encrypted."));
                } catch (exception& e) {
                        error_dialog (this, e.what ());
  
        void content_scale_to_fit_width ()
        {
 -              VideoContentList vc = _film_editor->selected_video_content ();
 +              VideoContentList vc = _film_editor->content_panel()->selected_video ();
                for (VideoContentList::iterator i = vc.begin(); i != vc.end(); ++i) {
                        (*i)->scale_and_crop_to_fit_width ();
                }
  
        void content_scale_to_fit_height ()
        {
 -              VideoContentList vc = _film_editor->selected_video_content ();
 +              VideoContentList vc = _film_editor->content_panel()->selected_video ();
                for (VideoContentList::iterator i = vc.begin(); i != vc.end(); ++i) {
                        (*i)->scale_and_crop_to_fit_height ();
                }
                }
                bool const dcp_creation = (i != jobs.end ()) && !(*i)->finished ();
                bool const have_cpl = film && !film->cpls().empty ();
 -              bool const have_selected_video_content = !_film_editor->selected_video_content().empty();
 +              bool const have_selected_video_content = !_film_editor->content_panel()->selected_video().empty();
                
                for (map<wxMenuItem*, int>::iterator j = menu_items.begin(); j != menu_items.end(); ++j) {
                        
@@@ -643,9 -641,6 +642,9 @@@ static const wxCmdLineEntryDesc command
        { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
  };
  
 +/** @class App
 + *  @brief The magic App class for wxWidgets.
 + */
  class App : public wxApp
  {
        bool OnInit ()
index f9ed155445b5f1852c154b807195602c3d4567b6,758060a08e7aea7e49ce27594c03ece111926d64..6257d60af3f636e2a25eaf3464eebafa993c5ee3
  */
  
  #include <getopt.h>
 -#include <libdcp/certificates.h>
 +#include <dcp/certificates.h>
  #include "lib/film.h"
  #include "lib/cinema.h"
  #include "lib/kdm.h"
  #include "lib/config.h"
  #include "lib/exceptions.h"
+ #include "lib/safe_stringstream.h"
  
  using std::string;
- using std::stringstream;
  using std::cout;
  using std::cerr;
  using std::list;
@@@ -41,8 -41,8 +41,8 @@@ help (
        cerr << "Syntax: " << program_name << " [OPTION] [<FILM>]\n"
                "  -h, --help             show this help\n"
                "  -o, --output           output file or directory\n"
 -              "  -f, --valid-from       valid from time (e.g. \"2013-09-28 01:41:51\") or \"now\"\n"
 -              "  -t, --valid-to         valid to time (e.g. \"2014-09-28 01:41:51\")\n"
 +              "  -f, --valid-from       valid from time (in local time zone) (e.g. \"2013-09-28 01:41:51\") or \"now\"\n"
 +              "  -t, --valid-to         valid to time (in local time zone) (e.g. \"2014-09-28 01:41:51\")\n"
                "  -d, --valid-duration   valid duration (e.g. \"1 day\", \"4 hours\", \"2 weeks\")\n"
                "      --formulation      modified-transitional-1, dci-any or dci-specific [default modified-transitional-1]\n"
                "  -z, --zip              ZIP each cinema's KDMs into its own file\n"
@@@ -76,7 -76,7 +76,7 @@@ time_from_string (string t
  static boost::posix_time::time_duration
  duration_from_string (string d)
  {
-       stringstream s (d);
+       SafeStringStream s (d);
        int N;
        string unit;
        s >> N >> unit;
@@@ -111,7 -111,7 +111,7 @@@ int main (int argc, char* argv[]
        bool cinemas = false;
        string duration_string;
        bool verbose = false;
 -      libdcp::KDM::Formulation formulation = libdcp::KDM::MODIFIED_TRANSITIONAL_1;
 +      dcp::Formulation formulation = dcp::MODIFIED_TRANSITIONAL_1;
  
        program_name = argv[0];
        
                        break;
                case 'C':
                        if (string (optarg) == "modified-transitional-1") {
 -                              formulation = libdcp::KDM::MODIFIED_TRANSITIONAL_1;
 +                              formulation = dcp::MODIFIED_TRANSITIONAL_1;
                        } else if (string (optarg) == "dci-any") {
 -                              formulation = libdcp::KDM::DCI_ANY;
 +                              formulation = dcp::DCI_ANY;
                        } else if (string (optarg) == "dci-specific") {
 -                              formulation = libdcp::KDM::DCI_SPECIFIC;
 +                              formulation = dcp::DCI_SPECIFIC;
                        } else {
 -                              error ("unrecognised KDM formulation " + formulation);
 +                              error ("unrecognised KDM formulation " + string (optarg));
                        }
                }
        }
                        error ("you must specify --output");
                }
                
 -              shared_ptr<libdcp::Certificate> certificate (new libdcp::Certificate (boost::filesystem::path (certificate_file)));
 -              libdcp::KDM kdm = film->make_kdm (certificate, cpl, valid_from.get(), valid_to.get(), formulation);
 +              dcp::Certificate certificate (dcp::file_to_string (certificate_file));
 +              dcp::EncryptedKDM kdm = film->make_kdm (certificate, cpl, valid_from.get(), valid_to.get(), formulation);
                kdm.as_xml (output);
                if (verbose) {
                        cout << "Generated KDM " << output << " for certificate.\n";
  
                try {
                        if (zip) {
 -                              write_kdm_zip_files (film, (*i)->screens(), cpl, valid_from.get(), valid_to.get(), formulation, output);
 +                              write_kdm_zip_files (
 +                                      film, (*i)->screens(), cpl, dcp::LocalTime (valid_from.get()), dcp::LocalTime (valid_to.get()), formulation, output
 +                                      );
 +                              
                                if (verbose) {
                                        cout << "Wrote ZIP files to " << output << "\n";
                                }
                        } else {
 -                              write_kdm_files (film, (*i)->screens(), cpl, valid_from.get(), valid_to.get(), formulation, output);
 +                              write_kdm_files (
 +                                      film, (*i)->screens(), cpl, dcp::LocalTime (valid_from.get()), dcp::LocalTime (valid_to.get()), formulation, output
 +                                      );
 +                              
                                if (verbose) {
                                        cout << "Wrote KDM files to " << output << "\n";
                                }
index c74e258e8ff081680f3b799f354c5ee344ad937c,f35797954ae907ff237146c2480ecc3289574bff..b816460a3376daadea18467caf49fb5e1a20ffd9
@@@ -20,7 -20,6 +20,6 @@@
  #include "lib/server.h"
  #include <iostream>
  #include <stdexcept>
- #include <sstream>
  #include <cstring>
  #include <vector>
  #include <unistd.h>
@@@ -33,7 -32,7 +32,7 @@@
  #include <boost/thread/mutex.hpp>
  #include <boost/thread/condition.hpp>
  #include "lib/config.h"
 -#include "lib/dcp_video_frame.h"
 +#include "lib/dcp_video.h"
  #include "lib/exceptions.h"
  #include "lib/util.h"
  #include "lib/config.h"
diff --combined src/wx/about_dialog.cc
index 1d9b038997542374bd34f128921447cee8822671,2113d3a9eae80b3aaa681cf2f003f1d1f4fdfc86..5d1e546f7b92c78005e1df387344bd9c8d210c8d
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
 +    Copyright (C) 2013-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
  
  */
  
 +/** @file  src/wx/about_dialog.cc
 + *  @brief The "about DCP-o-matic" dialogue box.
 + */
 +
  #include <wx/notebook.h>
  #include <wx/hyperlink.h>
  #include "lib/version.h"
@@@ -116,6 -112,7 +116,7 @@@ AboutDialog::AboutDialog (wxWindow* par
        wxArrayString supported_by;
        supported_by.Add (wxT ("Manual AC"));
        supported_by.Add (wxT ("Kambiz Afshar"));
+       supported_by.Add (wxT ("Alex Asp"));
        supported_by.Add (wxT ("Louis Belloisy"));
        supported_by.Add (wxT ("Mike Blakesley"));
        supported_by.Add (wxT ("Jeff Boot"));
        SetSizerAndFit (overall_sizer);
  }
  
 +/** Add a section of credits.
 + *  @param name Name of section.
 + *  @param credits List of names.
 + */
  void
  AboutDialog::add_section (wxString name, wxArrayString credits)
  {
diff --combined src/wx/film_editor.cc
index 37a8c880847391eb68d29bdbe7063c5940c4a7cc,87310d21ae598ac93d1b67a8e40d3b2e8c61082c..1ce4695d6bf83c2e00c09df358667e5551374da1
  #include "lib/ffmpeg_content.h"
  #include "lib/sndfile_content.h"
  #include "lib/dcp_content_type.h"
 -#include "lib/sound_processor.h"
  #include "lib/scaler.h"
  #include "lib/playlist.h"
  #include "lib/content.h"
  #include "lib/content_factory.h"
 +#include "lib/dcp_content.h"
+ #include "lib/safe_stringstream.h"
  #include "timecode.h"
  #include "wx_util.h"
  #include "film_editor.h"
 -#include "isdcf_metadata_dialog.h"
  #include "timeline_dialog.h"
  #include "timing_panel.h"
  #include "subtitle_panel.h"
  #include "audio_panel.h"
  #include "video_panel.h"
 +#include "content_panel.h"
 +#include "dcp_panel.h"
  
  using std::string;
  using std::cout;
- using std::stringstream;
  using std::pair;
  using std::fixed;
  using std::setprecision;
@@@ -73,26 -72,340 +73,26 @@@ using boost::lexical_cast
  /** @param f Film to edit */
  FilmEditor::FilmEditor (shared_ptr<Film> f, wxWindow* parent)
        : wxPanel (parent)
 -      , _menu (this)
 -      , _generally_sensitive (true)
 -      , _timeline_dialog (0)
  {
        wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
  
        _main_notebook = new wxNotebook (this, wxID_ANY);
        s->Add (_main_notebook, 1);
  
 -      make_content_panel ();
 -      _main_notebook->AddPage (_content_panel, _("Content"), true);
 -      make_dcp_panel ();
 -      _main_notebook->AddPage (_dcp_panel, _("DCP"), false);
 +      _content_panel = new ContentPanel (_main_notebook, _film);
 +      _main_notebook->AddPage (_content_panel->panel (), _("Content"), true);
 +      _dcp_panel = new DCPPanel (_main_notebook, _film);
 +      _main_notebook->AddPage (_dcp_panel->panel (), _("DCP"), false);
        
        set_film (f);
 -      connect_to_widgets ();
  
        JobManager::instance()->ActiveJobsChanged.connect (
                bind (&FilmEditor::active_jobs_changed, this, _1)
                );
  
 -      Config::instance()->Changed.connect (boost::bind (&FilmEditor::config_changed, this));
 -      
        SetSizerAndFit (s);
  }
  
 -void
 -FilmEditor::make_dcp_panel ()
 -{
 -      _dcp_panel = new wxPanel (_main_notebook);
 -      _dcp_sizer = new wxBoxSizer (wxVERTICAL);
 -      _dcp_panel->SetSizer (_dcp_sizer);
 -
 -      wxGridBagSizer* grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
 -      _dcp_sizer->Add (grid, 0, wxEXPAND | wxALL, 8);
 -
 -      int r = 0;
 -      
 -      add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Name"), true, wxGBPosition (r, 0));
 -      _name = new wxTextCtrl (_dcp_panel, wxID_ANY);
 -      grid->Add (_name, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND | wxLEFT | wxRIGHT);
 -      ++r;
 -      
 -      add_label_to_grid_bag_sizer (grid, _dcp_panel, _("DCP Name"), true, wxGBPosition (r, 0));
 -      _dcp_name = new wxStaticText (_dcp_panel, wxID_ANY, wxT (""));
 -      grid->Add (_dcp_name, wxGBPosition(r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
 -      ++r;
 -
 -      int flags = wxALIGN_CENTER_VERTICAL;
 -#ifdef __WXOSX__
 -      flags |= wxALIGN_RIGHT;
 -#endif        
 -
 -      _use_isdcf_name = new wxCheckBox (_dcp_panel, wxID_ANY, _("Use ISDCF name"));
 -      grid->Add (_use_isdcf_name, wxGBPosition (r, 0), wxDefaultSpan, flags);
 -      _edit_isdcf_button = new wxButton (_dcp_panel, wxID_ANY, _("Details..."));
 -      grid->Add (_edit_isdcf_button, wxGBPosition (r, 1), wxDefaultSpan);
 -      ++r;
 -
 -      add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Container"), true, wxGBPosition (r, 0));
 -      _container = new wxChoice (_dcp_panel, wxID_ANY);
 -      grid->Add (_container, wxGBPosition (r, 1), wxDefaultSpan, wxEXPAND);
 -      ++r;
 -
 -      add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Content Type"), true, wxGBPosition (r, 0));
 -      _dcp_content_type = new wxChoice (_dcp_panel, wxID_ANY);
 -      grid->Add (_dcp_content_type, wxGBPosition (r, 1));
 -      ++r;
 -
 -      {
 -              add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Frame Rate"), true, wxGBPosition (r, 0));
 -              _frame_rate_sizer = new wxBoxSizer (wxHORIZONTAL);
 -              _frame_rate_choice = new wxChoice (_dcp_panel, wxID_ANY);
 -              _frame_rate_sizer->Add (_frame_rate_choice, 1, wxALIGN_CENTER_VERTICAL);
 -              _frame_rate_spin = new wxSpinCtrl (_dcp_panel, wxID_ANY);
 -              _frame_rate_sizer->Add (_frame_rate_spin, 1, wxALIGN_CENTER_VERTICAL);
 -              setup_frame_rate_widget ();
 -              _best_frame_rate = new wxButton (_dcp_panel, wxID_ANY, _("Use best"));
 -              _frame_rate_sizer->Add (_best_frame_rate, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND);
 -              grid->Add (_frame_rate_sizer, wxGBPosition (r, 1));
 -      }
 -      ++r;
 -
 -      _signed = new wxCheckBox (_dcp_panel, wxID_ANY, _("Signed"));
 -      grid->Add (_signed, wxGBPosition (r, 0), wxGBSpan (1, 2));
 -      ++r;
 -      
 -      _encrypted = new wxCheckBox (_dcp_panel, wxID_ANY, _("Encrypted"));
 -      grid->Add (_encrypted, wxGBPosition (r, 0), wxGBSpan (1, 2));
 -      ++r;
 -
 -      add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Audio channels"), true, wxGBPosition (r, 0));
 -      _audio_channels = new wxSpinCtrl (_dcp_panel, wxID_ANY);
 -      grid->Add (_audio_channels, wxGBPosition (r, 1));
 -      ++r;
 -
 -      _three_d = new wxCheckBox (_dcp_panel, wxID_ANY, _("3D"));
 -      grid->Add (_three_d, wxGBPosition (r, 0), wxGBSpan (1, 2));
 -      ++r;
 -
 -      add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Resolution"), true, wxGBPosition (r, 0));
 -      _resolution = new wxChoice (_dcp_panel, wxID_ANY);
 -      grid->Add (_resolution, wxGBPosition (r, 1));
 -      ++r;
 -
 -      {
 -              add_label_to_grid_bag_sizer (grid, _dcp_panel, _("JPEG2000 bandwidth"), true, wxGBPosition (r, 0));
 -              wxSizer* s = new wxBoxSizer (wxHORIZONTAL);
 -              _j2k_bandwidth = new wxSpinCtrl (_dcp_panel, wxID_ANY);
 -              s->Add (_j2k_bandwidth, 1);
 -              add_label_to_sizer (s, _dcp_panel, _("Mbit/s"), false);
 -              grid->Add (s, wxGBPosition (r, 1));
 -      }
 -      ++r;
 -
 -      add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Standard"), true, wxGBPosition (r, 0));
 -      _standard = new wxChoice (_dcp_panel, wxID_ANY);
 -      grid->Add (_standard, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
 -      ++r;
 -
 -      add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Scaler"), true, wxGBPosition (r, 0));
 -      _scaler = new wxChoice (_dcp_panel, wxID_ANY);
 -      grid->Add (_scaler, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
 -      ++r;
 -
 -      vector<Scaler const *> const sc = Scaler::all ();
 -      for (vector<Scaler const *>::const_iterator i = sc.begin(); i != sc.end(); ++i) {
 -              _scaler->Append (std_to_wx ((*i)->name()));
 -      }
 -
 -      vector<Ratio const *> const ratio = Ratio::all ();
 -      for (vector<Ratio const *>::const_iterator i = ratio.begin(); i != ratio.end(); ++i) {
 -              _container->Append (std_to_wx ((*i)->nickname ()));
 -      }
 -
 -      vector<DCPContentType const *> const ct = DCPContentType::all ();
 -      for (vector<DCPContentType const *>::const_iterator i = ct.begin(); i != ct.end(); ++i) {
 -              _dcp_content_type->Append (std_to_wx ((*i)->pretty_name ()));
 -      }
 -
 -      list<int> const dfr = Config::instance()->allowed_dcp_frame_rates ();
 -      for (list<int>::const_iterator i = dfr.begin(); i != dfr.end(); ++i) {
 -              _frame_rate_choice->Append (std_to_wx (boost::lexical_cast<string> (*i)));
 -      }
 -
 -      _audio_channels->SetRange (0, MAX_DCP_AUDIO_CHANNELS);
 -      _j2k_bandwidth->SetRange (1, Config::instance()->maximum_j2k_bandwidth() / 1000000);
 -      _frame_rate_spin->SetRange (1, 480);
 -
 -      _resolution->Append (_("2K"));
 -      _resolution->Append (_("4K"));
 -
 -      _standard->Append (_("SMPTE"));
 -      _standard->Append (_("Interop"));
 -}
 -
 -void
 -FilmEditor::connect_to_widgets ()
 -{
 -      _name->Bind             (wxEVT_COMMAND_TEXT_UPDATED,          boost::bind (&FilmEditor::name_changed, this));
 -      _use_isdcf_name->Bind   (wxEVT_COMMAND_CHECKBOX_CLICKED,      boost::bind (&FilmEditor::use_isdcf_name_toggled, this));
 -      _edit_isdcf_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::edit_isdcf_button_clicked, this));
 -      _container->Bind        (wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::container_changed, this));
 -      _content->Bind          (wxEVT_COMMAND_LIST_ITEM_SELECTED,    boost::bind (&FilmEditor::content_selection_changed, this));
 -      _content->Bind          (wxEVT_COMMAND_LIST_ITEM_DESELECTED,  boost::bind (&FilmEditor::content_selection_changed, this));
 -      _content->Bind          (wxEVT_COMMAND_LIST_ITEM_RIGHT_CLICK, boost::bind (&FilmEditor::content_right_click, this, _1));
 -      _content_add_file->Bind (wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::content_add_file_clicked, this));
 -      _content_add_folder->Bind (wxEVT_COMMAND_BUTTON_CLICKED,      boost::bind (&FilmEditor::content_add_folder_clicked, this));
 -      _content_remove->Bind   (wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::content_remove_clicked, this));
 -      _content_earlier->Bind  (wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::content_earlier_clicked, this));
 -      _content_later->Bind    (wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::content_later_clicked, this));
 -      _content_timeline->Bind (wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::content_timeline_clicked, this));
 -      _scaler->Bind           (wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::scaler_changed, this));
 -      _dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::dcp_content_type_changed, this));
 -      _frame_rate_choice->Bind(wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::frame_rate_choice_changed, this));
 -      _frame_rate_spin->Bind  (wxEVT_COMMAND_SPINCTRL_UPDATED,      boost::bind (&FilmEditor::frame_rate_spin_changed, this));
 -      _best_frame_rate->Bind  (wxEVT_COMMAND_BUTTON_CLICKED,        boost::bind (&FilmEditor::best_frame_rate_clicked, this));
 -      _signed->Bind           (wxEVT_COMMAND_CHECKBOX_CLICKED,      boost::bind (&FilmEditor::signed_toggled, this));
 -      _encrypted->Bind        (wxEVT_COMMAND_CHECKBOX_CLICKED,      boost::bind (&FilmEditor::encrypted_toggled, this));
 -      _audio_channels->Bind   (wxEVT_COMMAND_SPINCTRL_UPDATED,      boost::bind (&FilmEditor::audio_channels_changed, this));
 -      _j2k_bandwidth->Bind    (wxEVT_COMMAND_SPINCTRL_UPDATED,      boost::bind (&FilmEditor::j2k_bandwidth_changed, this));
 -      _resolution->Bind       (wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::resolution_changed, this));
 -      _sequence_video->Bind   (wxEVT_COMMAND_CHECKBOX_CLICKED,      boost::bind (&FilmEditor::sequence_video_changed, this));
 -      _three_d->Bind          (wxEVT_COMMAND_CHECKBOX_CLICKED,      boost::bind (&FilmEditor::three_d_changed, this));
 -      _standard->Bind         (wxEVT_COMMAND_CHOICE_SELECTED,       boost::bind (&FilmEditor::standard_changed, this));
 -}
 -
 -void
 -FilmEditor::make_content_panel ()
 -{
 -      _content_panel = new wxPanel (_main_notebook);
 -      _content_sizer = new wxBoxSizer (wxVERTICAL);
 -      _content_panel->SetSizer (_content_sizer);
 -
 -      {
 -              wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
 -              
 -              _content = new wxListCtrl (_content_panel, wxID_ANY, wxDefaultPosition, wxSize (320, 160), wxLC_REPORT | wxLC_NO_HEADER);
 -              s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6);
 -
 -              _content->InsertColumn (0, wxT(""));
 -              _content->SetColumnWidth (0, 512);
 -
 -              wxBoxSizer* b = new wxBoxSizer (wxVERTICAL);
 -              _content_add_file = new wxButton (_content_panel, wxID_ANY, _("Add file(s)..."));
 -              b->Add (_content_add_file, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
 -              _content_add_folder = new wxButton (_content_panel, wxID_ANY, _("Add folder..."));
 -              b->Add (_content_add_folder, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
 -              _content_remove = new wxButton (_content_panel, wxID_ANY, _("Remove"));
 -              b->Add (_content_remove, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
 -              _content_earlier = new wxButton (_content_panel, wxID_ANY, _("Up"));
 -              b->Add (_content_earlier, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
 -              _content_later = new wxButton (_content_panel, wxID_ANY, _("Down"));
 -              b->Add (_content_later, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
 -              _content_timeline = new wxButton (_content_panel, wxID_ANY, _("Timeline..."));
 -              b->Add (_content_timeline, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
 -
 -              s->Add (b, 0, wxALL, 4);
 -
 -              _content_sizer->Add (s, 0, wxEXPAND | wxALL, 6);
 -      }
 -
 -      _sequence_video = new wxCheckBox (_content_panel, wxID_ANY, _("Keep video in sequence"));
 -      _content_sizer->Add (_sequence_video);
 -
 -      _content_notebook = new wxNotebook (_content_panel, wxID_ANY);
 -      _content_sizer->Add (_content_notebook, 1, wxEXPAND | wxTOP, 6);
 -
 -      _video_panel = new VideoPanel (this);
 -      _panels.push_back (_video_panel);
 -      _audio_panel = new AudioPanel (this);
 -      _panels.push_back (_audio_panel);
 -      _subtitle_panel = new SubtitlePanel (this);
 -      _panels.push_back (_subtitle_panel);
 -      _timing_panel = new TimingPanel (this);
 -      _panels.push_back (_timing_panel);
 -}
 -
 -/** Called when the name widget has been changed */
 -void
 -FilmEditor::name_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      _film->set_name (string (_name->GetValue().mb_str()));
 -}
 -
 -void
 -FilmEditor::j2k_bandwidth_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -      
 -      _film->set_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1000000);
 -}
 -
 -void
 -FilmEditor::signed_toggled ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      _film->set_signed (_signed->GetValue ());
 -}
 -
 -void
 -FilmEditor::encrypted_toggled ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      _film->set_encrypted (_encrypted->GetValue ());
 -}
 -                             
 -/** Called when the frame rate choice widget has been changed */
 -void
 -FilmEditor::frame_rate_choice_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      _film->set_video_frame_rate (
 -              boost::lexical_cast<int> (
 -                      wx_to_std (_frame_rate_choice->GetString (_frame_rate_choice->GetSelection ()))
 -                      )
 -              );
 -}
 -
 -/** Called when the frame rate spin widget has been changed */
 -void
 -FilmEditor::frame_rate_spin_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      _film->set_video_frame_rate (_frame_rate_spin->GetValue ());
 -}
 -
 -void
 -FilmEditor::audio_channels_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      _film->set_audio_channels (_audio_channels->GetValue ());
 -}
 -
 -void
 -FilmEditor::resolution_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      _film->set_resolution (_resolution->GetSelection() == 0 ? RESOLUTION_2K : RESOLUTION_4K);
 -}
 -
 -void
 -FilmEditor::standard_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      _film->set_interop (_standard->GetSelection() == 1);
 -}
  
  /** Called when the metadata stored in the Film object has changed;
   *  so that we can update the GUI.
@@@ -107,9 -420,96 +107,8 @@@ FilmEditor::film_changed (Film::Propert
                return;
        }
  
 -      SafeStringStream s;
 -
 -      for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) {
 -              (*i)->film_changed (p);
 -      }
 -              
 -      switch (p) {
 -      case Film::NONE:
 -              break;
 -      case Film::CONTENT:
 -              setup_content ();
 -              break;
 -      case Film::CONTAINER:
 -              setup_container ();
 -              break;
 -      case Film::NAME:
 -              checked_set (_name, _film->name());
 -              setup_dcp_name ();
 -              break;
 -      case Film::WITH_SUBTITLES:
 -              setup_dcp_name ();
 -              break;
 -      case Film::DCP_CONTENT_TYPE:
 -              checked_set (_dcp_content_type, DCPContentType::as_index (_film->dcp_content_type ()));
 -              setup_dcp_name ();
 -              break;
 -      case Film::SCALER:
 -              checked_set (_scaler, Scaler::as_index (_film->scaler ()));
 -              break;
 -      case Film::SIGNED:
 -              checked_set (_signed, _film->is_signed ());
 -              break;
 -      case Film::ENCRYPTED:
 -              checked_set (_encrypted, _film->encrypted ());
 -              if (_film->encrypted ()) {
 -                      _film->set_signed (true);
 -                      _signed->Enable (false);
 -              } else {
 -                      _signed->Enable (_generally_sensitive);
 -              }
 -              break;
 -      case Film::RESOLUTION:
 -              checked_set (_resolution, _film->resolution() == RESOLUTION_2K ? 0 : 1);
 -              setup_dcp_name ();
 -              break;
 -      case Film::J2K_BANDWIDTH:
 -              checked_set (_j2k_bandwidth, _film->j2k_bandwidth() / 1000000);
 -              break;
 -      case Film::USE_ISDCF_NAME:
 -              checked_set (_use_isdcf_name, _film->use_isdcf_name ());
 -              setup_dcp_name ();
 -              break;
 -      case Film::ISDCF_METADATA:
 -              setup_dcp_name ();
 -              break;
 -      case Film::VIDEO_FRAME_RATE:
 -      {
 -              bool done = false;
 -              for (unsigned int i = 0; i < _frame_rate_choice->GetCount(); ++i) {
 -                      if (wx_to_std (_frame_rate_choice->GetString(i)) == boost::lexical_cast<string> (_film->video_frame_rate())) {
 -                              checked_set (_frame_rate_choice, i);
 -                              done = true;
 -                              break;
 -                      }
 -              }
 -
 -              if (!done) {
 -                      checked_set (_frame_rate_choice, -1);
 -              }
 -
 -              _frame_rate_spin->SetValue (_film->video_frame_rate ());
 -
 -              _best_frame_rate->Enable (_film->best_video_frame_rate () != _film->video_frame_rate ());
 -              break;
 -      }
 -      case Film::AUDIO_CHANNELS:
 -              checked_set (_audio_channels, _film->audio_channels ());
 -              setup_dcp_name ();
 -              break;
 -      case Film::SEQUENCE_VIDEO:
 -              checked_set (_sequence_video, _film->sequence_video ());
 -              break;
 -      case Film::THREE_D:
 -              checked_set (_three_d, _film->three_d ());
 -              setup_dcp_name ();
 -              break;
 -      case Film::INTEROP:
 -              checked_set (_standard, _film->interop() ? 1 : 0);
 -              break;
 -      }
 +      _content_panel->film_changed (p);
 +      _dcp_panel->film_changed (p);
  }
  
  void
@@@ -124,8 -524,67 +123,8 @@@ FilmEditor::film_content_changed (int p
                return;
        }
  
 -      for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) {
 -              (*i)->film_content_changed (property);
 -      }
 -
 -      if (property == FFmpegContentProperty::AUDIO_STREAM) {
 -              setup_dcp_name ();
 -      } else if (property == ContentProperty::PATH) {
 -              setup_content ();
 -      } else if (property == ContentProperty::POSITION) {
 -              setup_content ();
 -      }
 -}
 -
 -void
 -FilmEditor::setup_container ()
 -{
 -      int n = 0;
 -      vector<Ratio const *> ratios = Ratio::all ();
 -      vector<Ratio const *>::iterator i = ratios.begin ();
 -      while (i != ratios.end() && *i != _film->container ()) {
 -              ++i;
 -              ++n;
 -      }
 -      
 -      if (i == ratios.end()) {
 -              checked_set (_container, -1);
 -      } else {
 -              checked_set (_container, n);
 -      }
 -      
 -      setup_dcp_name ();
 -}     
 -
 -/** Called when the container widget has been changed */
 -void
 -FilmEditor::container_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      int const n = _container->GetSelection ();
 -      if (n >= 0) {
 -              vector<Ratio const *> ratios = Ratio::all ();
 -              assert (n < int (ratios.size()));
 -              _film->set_container (ratios[n]);
 -      }
 -}
 -
 -/** Called when the DCP content type widget has been changed */
 -void
 -FilmEditor::dcp_content_type_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      int const n = _dcp_content_type->GetSelection ();
 -      if (n != wxNOT_FOUND) {
 -              _film->set_dcp_content_type (DCPContentType::from_index (n));
 -      }
 +      _content_panel->film_content_changed (property);
 +      _dcp_panel->film_content_changed (property);
  }
  
  /** Sets the Film that we are editing */
@@@ -140,9 -599,6 +139,9 @@@ FilmEditor::set_film (shared_ptr<Film> 
        
        _film = f;
  
 +      _content_panel->set_film (_film);
 +      _dcp_panel->set_film (_film);
 +
        if (_film) {
                _film->Changed.connect (bind (&FilmEditor::film_changed, this, _1));
                _film->ContentChanged.connect (bind (&FilmEditor::film_content_changed, this, _2));
                FileChanged ("");
        }
  
 -      film_changed (Film::NAME);
 -      film_changed (Film::USE_ISDCF_NAME);
 -      film_changed (Film::CONTENT);
 -      film_changed (Film::DCP_CONTENT_TYPE);
 -      film_changed (Film::CONTAINER);
 -      film_changed (Film::RESOLUTION);
 -      film_changed (Film::SCALER);
 -      film_changed (Film::WITH_SUBTITLES);
 -      film_changed (Film::SIGNED);
 -      film_changed (Film::ENCRYPTED);
 -      film_changed (Film::J2K_BANDWIDTH);
 -      film_changed (Film::ISDCF_METADATA);
 -      film_changed (Film::VIDEO_FRAME_RATE);
 -      film_changed (Film::AUDIO_CHANNELS);
 -      film_changed (Film::SEQUENCE_VIDEO);
 -      film_changed (Film::THREE_D);
 -      film_changed (Film::INTEROP);
 -
        if (!_film->content().empty ()) {
 -              set_selection (_film->content().front ());
 +              _content_panel->set_selection (_film->content().front ());
        }
 -
 -      content_selection_changed ();
  }
  
  void
  FilmEditor::set_general_sensitivity (bool s)
  {
 -      _generally_sensitive = s;
 -
 -      /* Stuff in the Content / DCP tabs */
 -      _name->Enable (s);
 -      _use_isdcf_name->Enable (s);
 -      _edit_isdcf_button->Enable (s);
 -      _content->Enable (s);
 -      _content_add_file->Enable (s);
 -      _content_add_folder->Enable (s);
 -      _content_remove->Enable (s);
 -      _content_earlier->Enable (s);
 -      _content_later->Enable (s);
 -      _content_timeline->Enable (s);
 -      _dcp_content_type->Enable (s);
 -
 -      bool si = s;
 -      if (_film && _film->encrypted ()) {
 -              si = false;
 -      }
 -      _signed->Enable (si);
 -      
 -      _encrypted->Enable (s);
 -      _frame_rate_choice->Enable (s);
 -      _frame_rate_spin->Enable (s);
 -      _audio_channels->Enable (s);
 -      _j2k_bandwidth->Enable (s);
 -      _container->Enable (s);
 -      _best_frame_rate->Enable (s && _film && _film->best_video_frame_rate () != _film->video_frame_rate ());
 -      _sequence_video->Enable (s);
 -      _resolution->Enable (s);
 -      _scaler->Enable (s);
 -      _three_d->Enable (s);
 -      _standard->Enable (s);
 -
 -      /* Set the panels in the content notebook */
 -      for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) {
 -              (*i)->Enable (s);
 -      }
 -}
 -
 -/** Called when the scaler widget has been changed */
 -void
 -FilmEditor::scaler_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      int const n = _scaler->GetSelection ();
 -      if (n >= 0) {
 -              _film->set_scaler (Scaler::from_index (n));
 -      }
 -}
 -
 -void
 -FilmEditor::use_isdcf_name_toggled ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      _film->set_use_isdcf_name (_use_isdcf_name->GetValue ());
 -}
 -
 -void
 -FilmEditor::edit_isdcf_button_clicked ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      ISDCFMetadataDialog* d = new ISDCFMetadataDialog (this, _film->isdcf_metadata ());
 -      d->ShowModal ();
 -      _film->set_isdcf_metadata (d->isdcf_metadata ());
 -      d->Destroy ();
 +      _content_panel->set_general_sensitivity (s);
 +      _dcp_panel->set_general_sensitivity (s);
  }
  
  void
@@@ -171,4 -720,343 +170,3 @@@ FilmEditor::active_jobs_changed (bool a
  {
        set_general_sensitivity (!a);
  }
 -
 -void
 -FilmEditor::setup_dcp_name ()
 -{
 -      string s = _film->dcp_name (true);
 -      if (s.length() > 28) {
 -              _dcp_name->SetLabel (std_to_wx (s.substr (0, 28)) + N_("..."));
 -              _dcp_name->SetToolTip (std_to_wx (s));
 -      } else {
 -              _dcp_name->SetLabel (std_to_wx (s));
 -      }
 -}
 -
 -void
 -FilmEditor::best_frame_rate_clicked ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -      
 -      _film->set_video_frame_rate (_film->best_video_frame_rate ());
 -}
 -
 -void
 -FilmEditor::setup_content ()
 -{
 -      string selected_summary;
 -      int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
 -      if (s != -1) {
 -              selected_summary = wx_to_std (_content->GetItemText (s));
 -      }
 -      
 -      _content->DeleteAllItems ();
 -
 -      ContentList content = _film->content ();
 -      sort (content.begin(), content.end(), ContentSorter ());
 -      
 -      for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
 -              int const t = _content->GetItemCount ();
 -              bool const valid = (*i)->paths_valid ();
 -
 -              string s = (*i)->summary ();
 -              if (!valid) {
 -                      s = _("MISSING: ") + s;
 -              }
 -
 -              _content->InsertItem (t, std_to_wx (s));
 -
 -              if ((*i)->summary() == selected_summary) {
 -                      _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
 -              }
 -
 -              if (!valid) {
 -                      _content->SetItemTextColour (t, *wxRED);
 -              }
 -      }
 -
 -      if (selected_summary.empty () && !content.empty ()) {
 -              /* Select the item of content if none was selected before */
 -              _content->SetItemState (0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
 -      }
 -}
 -
 -void
 -FilmEditor::content_add_file_clicked ()
 -{
 -      /* The wxFD_CHANGE_DIR here prevents a `could not set working directory' error 123 on Windows when using
 -         non-Latin filenames or paths.
 -      */
 -      wxFileDialog* d = new wxFileDialog (this, _("Choose a file or files"), wxT (""), wxT (""), wxT ("*.*"), wxFD_MULTIPLE | wxFD_CHANGE_DIR);
 -      int const r = d->ShowModal ();
 -
 -      if (r != wxID_OK) {
 -              d->Destroy ();
 -              return;
 -      }
 -
 -      wxArrayString paths;
 -      d->GetPaths (paths);
 -
 -      /* XXX: check for lots of files here and do something */
 -
 -      for (unsigned int i = 0; i < paths.GetCount(); ++i) {
 -              _film->examine_and_add_content (content_factory (_film, wx_to_std (paths[i])));
 -      }
 -
 -      d->Destroy ();
 -}
 -
 -void
 -FilmEditor::content_add_folder_clicked ()
 -{
 -      wxDirDialog* d = new wxDirDialog (this, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST);
 -      int const r = d->ShowModal ();
 -      d->Destroy ();
 -      
 -      if (r != wxID_OK) {
 -              return;
 -      }
 -
 -      shared_ptr<ImageContent> ic;
 -      
 -      try {
 -              ic.reset (new ImageContent (_film, boost::filesystem::path (wx_to_std (d->GetPath ()))));
 -      } catch (FileError& e) {
 -              error_dialog (this, std_to_wx (e.what ()));
 -              return;
 -      }
 -
 -      _film->examine_and_add_content (ic);
 -}
 -
 -void
 -FilmEditor::content_remove_clicked ()
 -{
 -      ContentList c = selected_content ();
 -      for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              _film->remove_content (*i);
 -      }
 -
 -      content_selection_changed ();
 -}
 -
 -void
 -FilmEditor::content_selection_changed ()
 -{
 -      setup_content_sensitivity ();
 -
 -      for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) {
 -              (*i)->content_selection_changed ();
 -      }
 -}
 -
 -/** Set up broad sensitivity based on the type of content that is selected */
 -void
 -FilmEditor::setup_content_sensitivity ()
 -{
 -      _content_add_file->Enable (_generally_sensitive);
 -      _content_add_folder->Enable (_generally_sensitive);
 -
 -      ContentList selection = selected_content ();
 -      VideoContentList video_selection = selected_video_content ();
 -      AudioContentList audio_selection = selected_audio_content ();
 -
 -      _content_remove->Enable   (!selection.empty() && _generally_sensitive);
 -      _content_earlier->Enable  (selection.size() == 1 && _generally_sensitive);
 -      _content_later->Enable    (selection.size() == 1 && _generally_sensitive);
 -      _content_timeline->Enable (!_film->content().empty() && _generally_sensitive);
 -
 -      _video_panel->Enable    (!video_selection.empty() && _generally_sensitive);
 -      _audio_panel->Enable    (!audio_selection.empty() && _generally_sensitive);
 -      _subtitle_panel->Enable (selection.size() == 1 && dynamic_pointer_cast<FFmpegContent> (selection.front()) && _generally_sensitive);
 -      _timing_panel->Enable   (!selection.empty() && _generally_sensitive);
 -}
 -
 -ContentList
 -FilmEditor::selected_content ()
 -{
 -      ContentList sel;
 -
 -      if (!_film) {
 -              return sel;
 -      }
 -
 -      /* The list was populated using a sorted content list, so we must sort it here too
 -         so that we can look up by index and get the right thing.
 -      */
 -      ContentList content = _film->content ();
 -      sort (content.begin(), content.end(), ContentSorter ());
 -      
 -      long int s = -1;
 -      while (true) {
 -              s = _content->GetNextItem (s, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
 -              if (s == -1) {
 -                      break;
 -              }
 -
 -              if (s < int (_film->content().size ())) {
 -                      sel.push_back (content[s]);
 -              }
 -      }
 -
 -      return sel;
 -}
 -
 -VideoContentList
 -FilmEditor::selected_video_content ()
 -{
 -      ContentList c = selected_content ();
 -      VideoContentList vc;
 -      
 -      for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              shared_ptr<VideoContent> t = dynamic_pointer_cast<VideoContent> (*i);
 -              if (t) {
 -                      vc.push_back (t);
 -              }
 -      }
 -
 -      return vc;
 -}
 -
 -AudioContentList
 -FilmEditor::selected_audio_content ()
 -{
 -      ContentList c = selected_content ();
 -      AudioContentList ac;
 -      
 -      for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              shared_ptr<AudioContent> t = dynamic_pointer_cast<AudioContent> (*i);
 -              if (t) {
 -                      ac.push_back (t);
 -              }
 -      }
 -
 -      return ac;
 -}
 -
 -SubtitleContentList
 -FilmEditor::selected_subtitle_content ()
 -{
 -      ContentList c = selected_content ();
 -      SubtitleContentList sc;
 -      
 -      for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              shared_ptr<SubtitleContent> t = dynamic_pointer_cast<SubtitleContent> (*i);
 -              if (t) {
 -                      sc.push_back (t);
 -              }
 -      }
 -
 -      return sc;
 -}
 -
 -FFmpegContentList
 -FilmEditor::selected_ffmpeg_content ()
 -{
 -      ContentList c = selected_content ();
 -      FFmpegContentList sc;
 -      
 -      for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              shared_ptr<FFmpegContent> t = dynamic_pointer_cast<FFmpegContent> (*i);
 -              if (t) {
 -                      sc.push_back (t);
 -              }
 -      }
 -
 -      return sc;
 -}
 -
 -void
 -FilmEditor::content_timeline_clicked ()
 -{
 -      if (_timeline_dialog) {
 -              _timeline_dialog->Destroy ();
 -              _timeline_dialog = 0;
 -      }
 -      
 -      _timeline_dialog = new TimelineDialog (this, _film);
 -      _timeline_dialog->Show ();
 -}
 -
 -void
 -FilmEditor::set_selection (weak_ptr<Content> wc)
 -{
 -      ContentList content = _film->content ();
 -      for (size_t i = 0; i < content.size(); ++i) {
 -              if (content[i] == wc.lock ()) {
 -                      _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
 -              } else {
 -                      _content->SetItemState (i, 0, wxLIST_STATE_SELECTED);
 -              }
 -      }
 -}
 -
 -void
 -FilmEditor::sequence_video_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -      
 -      _film->set_sequence_video (_sequence_video->GetValue ());
 -}
 -
 -void
 -FilmEditor::content_right_click (wxListEvent& ev)
 -{
 -      _menu.popup (_film, selected_content (), ev.GetPoint ());
 -}
 -
 -void
 -FilmEditor::three_d_changed ()
 -{
 -      if (!_film) {
 -              return;
 -      }
 -
 -      _film->set_three_d (_three_d->GetValue ());
 -}
 -
 -void
 -FilmEditor::content_earlier_clicked ()
 -{
 -      ContentList sel = selected_content ();
 -      if (sel.size() == 1) {
 -              _film->move_content_earlier (sel.front ());
 -              content_selection_changed ();
 -      }
 -}
 -
 -void
 -FilmEditor::content_later_clicked ()
 -{
 -      ContentList sel = selected_content ();
 -      if (sel.size() == 1) {
 -              _film->move_content_later (sel.front ());
 -              content_selection_changed ();
 -      }
 -}
 -
 -void
 -FilmEditor::config_changed ()
 -{
 -      _j2k_bandwidth->SetRange (1, Config::instance()->maximum_j2k_bandwidth() / 1000000);
 -      setup_frame_rate_widget ();
 -}
 -
 -void
 -FilmEditor::setup_frame_rate_widget ()
 -{
 -      if (Config::instance()->allow_any_dcp_frame_rate ()) {
 -              _frame_rate_choice->Hide ();
 -              _frame_rate_spin->Show ();
 -      } else {
 -              _frame_rate_choice->Show ();
 -              _frame_rate_spin->Hide ();
 -      }
--
 -      _frame_rate_sizer->Layout ();
 -}
index 801996efa7bcb611111285a847c41848591f69b7,53ca237555af31672f2dd2517038dc829ce270bf..27fc75b1b39c649fd76b9890d1758e374f7af858
  #include <boost/bind.hpp>
  #include "lib/film.h"
  #include "lib/config.h"
+ #include "lib/safe_stringstream.h"
  #include "properties_dialog.h"
  #include "wx_util.h"
  
  using std::string;
- using std::stringstream;
  using std::fixed;
  using std::setprecision;
  using boost::shared_ptr;
@@@ -45,9 -45,10 +45,9 @@@ PropertiesDialog::PropertiesDialog (wxW
        add (_("Frames already encoded"), true);
        _encoded = add (new ThreadedStaticText (this, _("counting..."), boost::bind (&PropertiesDialog::frames_already_encoded, this)));
        _encoded->Finished.connect (boost::bind (&PropertiesDialog::layout, this));
 -      
 -      _frames->SetLabel (std_to_wx (lexical_cast<string> (_film->time_to_video_frames (_film->length()))));
 +      _frames->SetLabel (std_to_wx (lexical_cast<string> (_film->length().frames (_film->video_frame_rate ()))));
        double const disk = double (_film->required_disk_space()) / 1073741824.0f;
-       stringstream s;
+       SafeStringStream s;
        s << fixed << setprecision (1) << disk << wx_to_std (_("Gb"));
        _disk->SetLabel (std_to_wx (s.str ()));
  
  string
  PropertiesDialog::frames_already_encoded () const
  {
-       stringstream u;
+       SafeStringStream u;
        try {
                u << _film->encoded_frames ();
        } catch (boost::thread_interrupted &) {
                return "";
        }
 -      
 -      if (_film->length()) {
 +
 +      uint64_t const frames = _film->length().frames (_film->video_frame_rate ());
 +      if (frames) {
                /* XXX: encoded_frames() should check which frames have been encoded */
 -              u << " (" << (_film->encoded_frames() * 100 / _film->time_to_video_frames (_film->length())) << "%)";
 +              u << " (" << (_film->encoded_frames() * 100 / frames) << "%)";
        }
        return u.str ();
  }
diff --combined src/wx/timing_panel.cc
index a0e1f8f8a4845b8ebc9c852202d89818b55f8f01,aa4f70a81b476c715c8e068a27764eaa30636abc..27d5b9cd35f3f4e87bd771d3f76402c1f674592e
  
  */
  
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "lib/content.h"
  #include "lib/image_content.h"
  #include "timing_panel.h"
  #include "wx_util.h"
  #include "timecode.h"
 -#include "film_editor.h"
 +#include "content_panel.h"
  
  using std::cout;
  using std::string;
  using std::set;
  using boost::shared_ptr;
  using boost::dynamic_pointer_cast;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
 -TimingPanel::TimingPanel (FilmEditor* e)
 +TimingPanel::TimingPanel (ContentPanel* p)
        /* horrid hack for apparent lack of context support with wxWidgets i18n code */
 -      : FilmEditorPanel (e, S_("Timing|Timing"))
 +      : ContentSubPanel (p, S_("Timing|Timing"))
  {
        wxFlexGridSizer* grid = new wxFlexGridSizer (2, 4, 4);
        _sizer->Add (grid, 0, wxALL, 8);
@@@ -78,8 -78,8 +78,8 @@@
  void
  TimingPanel::film_content_changed (int property)
  {
 -      ContentList cl = _editor->selected_content ();
 -      int const film_video_frame_rate = _editor->film()->video_frame_rate ();
 +      ContentList cl = _parent->selected ();
 +      int const film_video_frame_rate = _parent->film()->video_frame_rate ();
  
        /* Here we check to see if we have exactly one different value of various
           properties, and fill the controls with that value if so.
@@@ -87,7 -87,7 +87,7 @@@
        
        if (property == ContentProperty::POSITION) {
  
 -              set<Time> check;
 +              set<DCPTime> check;
                for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
                        check.insert ((*i)->position ());
                }
                property == VideoContentProperty::VIDEO_FRAME_TYPE
                ) {
  
 -              set<Time> check;
 +              set<DCPTime> check;
                for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
                        check.insert ((*i)->full_length ());
                }
  
        } else if (property == ContentProperty::TRIM_START) {
  
 -              set<Time> check;
 +              set<DCPTime> check;
                for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
                        check.insert ((*i)->trim_start ());
                }
                
        } else if (property == ContentProperty::TRIM_END) {
  
 -              set<Time> check;
 +              set<DCPTime> check;
                for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
                        check.insert ((*i)->trim_end ());
                }
                if (check.size() == 1) {
                        _trim_end->set (cl.front()->trim_end (), film_video_frame_rate);
                } else {
 -                      _trim_end->set (0, 24);
 +                      _trim_end->clear ();
                }
        }
  
                property == VideoContentProperty::VIDEO_FRAME_TYPE
                ) {
  
 -              set<Time> check;
 +              set<DCPTime> check;
                for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
                        check.insert ((*i)->length_after_trim ());
                }
                set<float> check;
                shared_ptr<VideoContent> vc;
                for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
-                       vc = dynamic_pointer_cast<VideoContent> (*i);
-                       if (vc) {
-                               check.insert (vc->video_frame_rate ());
+                       shared_ptr<VideoContent> t = dynamic_pointer_cast<VideoContent> (*i);
+                       if (t) {
+                               check.insert (t->video_frame_rate ());
+                               vc = t;
                        }
                }
                if (check.size() == 1) {
  void
  TimingPanel::position_changed ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
        for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              (*i)->set_position (_position->get (_editor->film()->video_frame_rate ()));
 +              (*i)->set_position (_position->get (_parent->film()->video_frame_rate ()));
        }
  }
  
  void
  TimingPanel::full_length_changed ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
        for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
                shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (*i);
                if (ic && ic->still ()) {
 -                      ic->set_video_length (rint (_full_length->get (_editor->film()->video_frame_rate()) * ic->video_frame_rate() / TIME_HZ));
 +                      /* XXX: No effective FRC here... is this right? */
 +                      ic->set_video_length (ContentTime (_full_length->get (_parent->film()->video_frame_rate()), FrameRateChange (1, 1)));
                }
        }
  }
  void
  TimingPanel::trim_start_changed ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
        for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              (*i)->set_trim_start (_trim_start->get (_editor->film()->video_frame_rate ()));
 +              (*i)->set_trim_start (_trim_start->get (_parent->film()->video_frame_rate ()));
        }
  }
  
  void
  TimingPanel::trim_end_changed ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
        for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              (*i)->set_trim_end (_trim_end->get (_editor->film()->video_frame_rate ()));
 +              (*i)->set_trim_end (_trim_end->get (_parent->film()->video_frame_rate ()));
        }
  }
  
  void
  TimingPanel::play_length_changed ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
        for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              (*i)->set_trim_end ((*i)->full_length() - _play_length->get (_editor->film()->video_frame_rate()) - (*i)->trim_start());
 +              (*i)->set_trim_end ((*i)->full_length() - _play_length->get (_parent->film()->video_frame_rate()) - (*i)->trim_start());
        }
  }
  
@@@ -252,7 -252,7 +253,7 @@@ TimingPanel::video_frame_rate_changed (
  void
  TimingPanel::set_video_frame_rate ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
        for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
                shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*i);
                if (vc) {
  void
  TimingPanel::content_selection_changed ()
  {
 -      bool const e = !_editor->selected_content().empty ();
 +      bool const e = !_parent->selected().empty ();
  
        _position->Enable (e);
        _full_length->Enable (e);
index f0a544e50a9f57ed178fb4705696fb14ca810d9f,c9f4a2c38faaab5b4d82bbb0ece8a550b0fc3537..01cff5b06c46fbb993eb397f68bdefc9a1329a0e
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
 +    Copyright (C) 2013-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 <sstream>
 +/** @file  test/film_metadata_test.cc
 + *  @brief Test some basic reading/writing of film metadata.
 + */
 +
  #include <boost/test/unit_test.hpp>
  #include <boost/filesystem.hpp>
  #include <boost/date_time.hpp>
@@@ -36,9 -31,13 +35,9 @@@ using boost::shared_ptr
  
  BOOST_AUTO_TEST_CASE (film_metadata_test)
  {
 -      string const test_film = "build/test/film_metadata_test";
 -      
 -      if (boost::filesystem::exists (test_film)) {
 -              boost::filesystem::remove_all (test_film);
 -      }
 +      shared_ptr<Film> f = new_test_film ("film_metadata_test");
 +      boost::filesystem::path dir = test_film_dir ("film_metadata_test");
  
 -      shared_ptr<Film> f (new Film (test_film));
        f->_isdcf_date = boost::gregorian::from_undelimited_string ("20130211");
        BOOST_CHECK (f->container() == 0);
        BOOST_CHECK (f->dcp_content_type() == 0);
@@@ -51,9 -50,9 +50,9 @@@
  
        list<string> ignore;
        ignore.push_back ("Key");
 -      check_xml ("test/data/metadata.xml.ref", test_film + "/metadata.xml", ignore);
 +      check_xml ("test/data/metadata.xml.ref", dir.string() + "/metadata.xml", ignore);
  
 -      shared_ptr<Film> g (new Film (test_film));
 +      shared_ptr<Film> g (new Film (dir));
        g->read_metadata ();
  
        BOOST_CHECK_EQUAL (g->name(), "fred");
@@@ -61,5 -60,5 +60,5 @@@
        BOOST_CHECK_EQUAL (g->container(), Ratio::from_id ("185"));
        
        g->write_metadata ();
 -      check_xml ("test/data/metadata.xml.ref", test_film + "/metadata.xml", ignore);
 +      check_xml ("test/data/metadata.xml.ref", dir.string() + "/metadata.xml", ignore);
  }