Merge master.
authorCarl Hetherington <cth@carlh.net>
Mon, 2 Jun 2014 11:06:20 +0000 (12:06 +0100)
committerCarl Hetherington <cth@carlh.net>
Mon, 2 Jun 2014 11:06:20 +0000 (12:06 +0100)
34 files changed:
1  2 
ChangeLog
src/lib/config.cc
src/lib/config.h
src/lib/content.h
src/lib/cross.cc
src/lib/dcp_video_frame.cc
src/lib/decoder.h
src/lib/encoder.cc
src/lib/ffmpeg_content.cc
src/lib/ffmpeg_decoder.cc
src/lib/ffmpeg_examiner.cc
src/lib/film.cc
src/lib/film.h
src/lib/image_decoder.cc
src/lib/image_proxy.cc
src/lib/job.cc
src/lib/kdm.cc
src/lib/kdm.h
src/lib/player.cc
src/lib/player_video_frame.cc
src/lib/player_video_frame.h
src/lib/server.cc
src/lib/transcode_job.cc
src/lib/types.h
src/lib/util.h
src/lib/writer.cc
src/tools/dcpomatic.cc
src/tools/dcpomatic_cli.cc
src/tools/dcpomatic_kdm.cc
src/wx/about_dialog.cc
src/wx/config_dialog.cc
src/wx/film_editor.cc
test/client_server_test.cc
test/ffmpeg_dcp_test.cc

diff --combined ChangeLog
index 6f31dd5d24fbcd839fa07968a0e32c77d0e27977,bd7ec7c621edf3f6d1e2cbe3cd67b0aa0cbf23b1..e31fc719ebcf398dddbe0f0ee605bc0620b00612
+++ b/ChangeLog
@@@ -1,7 -1,60 +1,64 @@@
 +2014-03-07  Carl Hetherington  <cth@carlh.net>
 +
 +      * Add subtitle view.
 +
+ 2014-05-29  Carl Hetherington  <cth@carlh.net>
+       * Version 1.69.18 released.
+ 2014-05-28  Carl Hetherington  <cth@carlh.net>
+       * Version 1.69.17 released.
+ 2014-05-28  Carl Hetherington  <cth@carlh.net>
+       * Version 1.69.16 released.
+ 2014-05-28  Carl Hetherington  <cth@carlh.net>
+       * Rework KDM generation to be about CPLs rather than DCPs,
+       and allow specification of any CPL to generate KDMs for.
+       Requested-by: Richard Turner
+ 2014-05-27  Carl Hetherington  <cth@carlh.net>
+       * Version 1.69.15 released.
+ 2014-05-26  Carl Hetherington  <cth@carlh.net>
+       * Version 1.69.14 released.
+ 2014-05-26  Carl Hetherington  <cth@carlh.net>
+       * Fix problems with non-zero FFmpeg content start times.
+ 2014-05-24  Carl Hetherington  <cth@carlh.net>
+       * Version 1.69.13 released.
+ 2014-05-24  Carl Hetherington  <cth@carlh.net>
+       * Fix problems with log setup from config.
+ 2014-05-23  Carl Hetherington  <cth@carlh.net>
+       * Version 1.69.12 released.
+ 2014-05-22  Carl Hetherington  <cth@carlh.net>
+       * Version 1.69.11 released.
+ 2014-05-21  Carl Hetherington  <cth@carlh.net>
+       * Version 1.69.10 released.
+ 2014-05-21  Carl Hetherington  <cth@carlh.net>
+       * Tidy up logging a bit and make it configurable from the GUI
+       (moving a few things into an Advanced preferences tab at
+       the same time).
  2014-05-19  Carl Hetherington  <cth@carlh.net>
  
        * Version 1.69.9 released.
diff --combined src/lib/config.cc
index 754346418e2f4c83b66b0972a1e7868497620fd5,b97ad559dc949c8f21da787e51611e6bf4e48a0b..8be31a329645151bd424c4b77f7f52b13d74cc60
@@@ -23,8 -23,8 +23,8 @@@
  #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 <libcxml/cxml.h>
  #include "config.h"
  #include "server.h"
@@@ -50,7 -50,7 +50,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;
  
@@@ -73,6 -73,7 +73,7 @@@ Config::Config (
        , _check_for_updates (false)
        , _check_for_test_updates (false)
        , _maximum_j2k_bandwidth (250000000)
+       , _log_types (Log::TYPE_GENERAL | Log::TYPE_WARNING | Log::TYPE_ERROR)
  {
        _allowed_dcp_frame_rates.push_back (24);
        _allowed_dcp_frame_rates.push_back (25);
@@@ -81,9 -82,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));
  }
  
  void
@@@ -165,7 -166,7 +166,7 @@@ Config::read (
                /* 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");
  
        _maximum_j2k_bandwidth = f.optional_number_child<int> ("MaximumJ2KBandwidth").get_value_or (250000000);
        _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
@@@ -366,6 -369,7 +369,7 @@@ Config::write () cons
  
        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));
        
        doc.write_to_file_formatted (file(false).string ());
  }
diff --combined src/lib/config.h
index ffaacf8f17870fcfc559732d7b41c9b39b7b29ec,bd8cfe2973b43ddc258c0780a7e279fa82c6da74..ccd37ec1e22a2bc2d133b82138bf0a6268428196
@@@ -28,7 -28,7 +28,7 @@@
  #include <boost/shared_ptr.hpp>
  #include <boost/signals2.hpp>
  #include <boost/filesystem.hpp>
 -#include <libdcp/metadata.h>
 +#include <dcp/metadata.h>
  #include "dci_metadata.h"
  #include "colour_conversion.h"
  #include "server.h"
@@@ -141,7 -141,7 +141,7 @@@ public
                return _default_dcp_content_type;
        }
  
 -      libdcp::XMLMetadata dcp_metadata () const {
 +      dcp::XMLMetadata dcp_metadata () const {
                return _dcp_metadata;
        }
  
        int maximum_j2k_bandwidth () const {
                return _maximum_j2k_bandwidth;
        }
+       int log_types () const {
+               return _log_types;
+       }
        
        /** @param n New number of local encoding threads */
        void set_num_local_encoding_threads (int n) {
                changed ();
        }
  
 -      void set_dcp_metadata (libdcp::XMLMetadata m) {
 +      void set_dcp_metadata (dcp::XMLMetadata m) {
                _dcp_metadata = m;
                changed ();
        }
                _maximum_j2k_bandwidth = b;
                changed ();
        }
+       void set_log_types (int t) {
+               _log_types = t;
+               changed ();
+       }
        
        boost::filesystem::path signer_chain_directory () const;
  
@@@ -386,7 -395,7 +395,7 @@@ private
        int _default_still_length;
        Ratio const * _default_container;
        DCPContentType const * _default_dcp_content_type;
 -      libdcp::XMLMetadata _dcp_metadata;
 +      dcp::XMLMetadata _dcp_metadata;
        int _default_j2k_bandwidth;
        int _default_audio_delay;
        std::vector<PresetColourConversion> _colour_conversions;
        bool _check_for_test_updates;
        /** maximum allowed J2K bandwidth in bits per second */
        int _maximum_j2k_bandwidth;
+       int _log_types;
  
        /** Singleton instance, or 0 */
        static Config* _instance;
diff --combined src/lib/content.h
index 1d3764c20192bd883f5efffe79249e736325f79a,596a0a905c95217d4daaf8ba116b8a6f518c6555..6bbf33b35cd4d4e10377bdda2ad77be6cf673f7c
@@@ -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.h
 + *  @brief Content class.
 + */
 +
  #ifndef DCPOMATIC_CONTENT_H
  #define DCPOMATIC_CONTENT_H
  
@@@ -30,9 -26,7 +30,9 @@@
  #include <boost/thread/mutex.hpp>
  #include <boost/enable_shared_from_this.hpp>
  #include <libxml++/libxml++.h>
 +#include <libcxml/cxml.h>
  #include "types.h"
 +#include "dcpomatic_time.h"
  
  namespace cxml {
        class Node;
@@@ -51,16 -45,13 +51,16 @@@ public
        static int const TRIM_END;
  };
  
 +/** @class Content
 + *  @brief A piece of content represented by one or more files on disk.
 + */
  class Content : public boost::enable_shared_from_this<Content>, public boost::noncopyable
  {
  public:
        Content (boost::shared_ptr<const Film>);
 -      Content (boost::shared_ptr<const Film>, Time);
 +      Content (boost::shared_ptr<const Film>, DCPTime);
        Content (boost::shared_ptr<const Film>, boost::filesystem::path);
 -      Content (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>);
 +      Content (boost::shared_ptr<const Film>, cxml::ConstNodePtr);
        Content (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
        virtual ~Content () {}
        
@@@ -72,7 -63,7 +72,7 @@@
        virtual std::string technical_summary () const;
        virtual std::string information () const = 0;
        virtual void as_xml (xmlpp::Node *) const;
 -      virtual Time full_length () const = 0;
 +      virtual DCPTime full_length () const = 0;
        virtual std::string identifier () const;
  
        boost::shared_ptr<Content> clone () const;
                return _digest;
        }
  
 -      void set_position (Time);
 +      void set_position (DCPTime);
  
 -      /** Time that this content starts; i.e. the time that the first
 +      /** DCPTime that this content starts; i.e. the time that the first
         *  bit of the content (trimmed or not) will happen.
         */
 -      Time position () const {
 +      DCPTime position () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _position;
        }
  
 -      void set_trim_start (Time);
 +      void set_trim_start (DCPTime);
  
 -      Time trim_start () const {
 +      DCPTime trim_start () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _trim_start;
        }
  
 -      void set_trim_end (Time);
 +      void set_trim_end (DCPTime);
        
 -      Time trim_end () const {
 +      DCPTime trim_end () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _trim_end;
        }
        
 -      Time end () const {
 -              return position() + length_after_trim() - 1;
 +      DCPTime end () const {
 +              return position() + length_after_trim();
        }
  
 -      Time length_after_trim () const;
 +      DCPTime length_after_trim () const;
        
        void set_change_signals_frequent (bool f) {
                _change_signals_frequent = f;
        }
  
 -      bool trimmed (Time) const;
++      boost::shared_ptr<const Film> film () const {
++              return _film.lock ();
++      }
        boost::signals2::signal<void (boost::weak_ptr<Content>, int, bool)> Changed;
  
  protected:
        boost::weak_ptr<const Film> _film;
  
        /** _mutex which should be used to protect accesses, as examine
 -          jobs can update content state in threads other than the main one.
 -      */
 +       *  jobs can update content state in threads other than the main one.
 +       */
        mutable boost::mutex _mutex;
  
        /** Paths of our data files */
        
  private:
        std::string _digest;
 -      Time _position;
 -      Time _trim_start;
 -      Time _trim_end;
 +      DCPTime _position;
 +      DCPTime _trim_start;
 +      DCPTime _trim_end;
        bool _change_signals_frequent;
  };
  
diff --combined src/lib/cross.cc
index eadbd75d87d1c203ea7a4a2719079a4da81fbb83,70f643913abfa9cab7e22b20858fc7edc7648602..53ef5723ac50232d5e05fb8664a7b8dc3eba766d
@@@ -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
  #endif
  #include "exceptions.h"
  
+ #include "i18n.h"
+ #define LOG_GENERAL(...) log->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
+ #define LOG_ERROR(...) log->log (String::compose (__VA_ARGS__), Log::TYPE_ERROR);
+ #define LOG_ERROR_NC(...) log->log (__VA_ARGS__, Log::TYPE_ERROR);
  using std::pair;
  using std::list;
  using std::ifstream;
@@@ -157,7 -163,7 +163,7 @@@ run_ffprobe (boost::filesystem::path co
        HANDLE child_stderr_read;
        HANDLE child_stderr_write;
        if (!CreatePipe (&child_stderr_read, &child_stderr_write, &security, 0)) {
-               log->log ("ffprobe call failed (could not CreatePipe)");
+               LOG_ERROR_NC ("ffprobe call failed (could not CreatePipe)");
                return;
        }
  
        PROCESS_INFORMATION process_info;
        ZeroMemory (&process_info, sizeof (process_info));
        if (!CreateProcess (0, command, 0, 0, TRUE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
-               log->log ("ffprobe call failed (could not CreateProcess)");
+               LOG_ERROR_NC (N_("ffprobe call failed (could not CreateProcess)"));
                return;
        }
  
        FILE* o = fopen_boost (out, "w");
        if (!o) {
-               log->log ("ffprobe call failed (could not create output file)");
+               LOG_ERROR_NC (N_("ffprobe call failed (could not create output file)"));
                return;
        }
  
  
  #ifdef DCPOMATIC_LINUX 
        string ffprobe = "ffprobe \"" + content.string() + "\" 2> \"" + out.string() + "\"";
-       log->log (String::compose ("Probing with %1", ffprobe));
+       LOG_GENERAL (N_("Probing with %1"), ffprobe);
          system (ffprobe.c_str ());
  #endif
  
        path /= "ffprobe";
        
        string ffprobe = path.string() + " \"" + content.string() + "\" 2> \"" + out.string() + "\"";
-       log->log (String::compose ("Probing with %1", ffprobe));
+       LOG_GENERAL (N_("Probing with %1"), ffprobe);
        system (ffprobe.c_str ());
  #endif
  }
index d154ba96b074298824da3713de768e892e6e5ed2,6cf987648773031a5cb2ed859f3fe4596446fe04..1aae64ac7aafd98fa40e59316dcb73872fc421b0
  #include <boost/array.hpp>
  #include <boost/asio.hpp>
  #include <boost/filesystem.hpp>
 +#include <boost/lexical_cast.hpp>
  #include <openssl/md5.h>
 -#include <libdcp/rec709_linearised_gamma_lut.h>
 -#include <libdcp/srgb_linearised_gamma_lut.h>
 -#include <libdcp/gamma_lut.h>
 -#include <libdcp/xyz_frame.h>
 -#include <libdcp/rgb_xyz.h>
 -#include <libdcp/colour_matrix.h>
 -#include <libdcp/raw_convert.h>
 +#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_frame.h"
  #include "cross.h"
  #include "player_video_frame.h"
  
+ #define LOG_GENERAL(...) _log->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
  #include "i18n.h"
  
  using std::string;
  using std::stringstream;
  using std::cout;
  using boost::shared_ptr;
 -using libdcp::Size;
 -using libdcp::raw_convert;
 +using boost::lexical_cast;
 +using dcp::Size;
 +using dcp::raw_convert;
  
  #define DCI_COEFFICENT (48.0 / 52.37)
  
@@@ -109,10 -111,13 +111,10 @@@ DCPVideoFrame::DCPVideoFrame (shared_pt
  shared_ptr<EncodedData>
  DCPVideoFrame::encode_locally ()
  {
 -      shared_ptr<libdcp::LUT> in_lut;
 -      if (_frame->colour_conversion().input_gamma_linearised) {
 -              in_lut = libdcp::SRGBLinearisedGammaLUT::cache.get (12, _frame->colour_conversion().input_gamma);
 -      } else {
 -              in_lut = libdcp::GammaLUT::cache.get (12, _frame->colour_conversion().input_gamma);
 -      }
 -
 +      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];
                }
        }
  
 -      shared_ptr<libdcp::XYZFrame> xyz = libdcp::rgb_to_xyz (
 +      shared_ptr<dcp::XYZFrame> xyz = dcp::rgb_to_xyz (
                _frame->image(),
                in_lut,
 -              libdcp::GammaLUT::cache.get (16, 1 / _frame->colour_conversion().output_gamma),
 +              dcp::GammaLUT::cache.get (16, 1 / _frame->colour_conversion().output_gamma, false),
                matrix
                );
  
  
        switch (_frame->eyes()) {
        case EYES_BOTH:
-               _log->log (String::compose (N_("Finished locally-encoded frame %1 for mono"), _index));
+               LOG_GENERAL (N_("Finished locally-encoded frame %1 for mono"), _index);
                break;
        case EYES_LEFT:
-               _log->log (String::compose (N_("Finished locally-encoded frame %1 for L"), _index));
+               LOG_GENERAL (N_("Finished locally-encoded frame %1 for L"), _index);
                break;
        case EYES_RIGHT:
-               _log->log (String::compose (N_("Finished locally-encoded frame %1 for R"), _index));
+               LOG_GENERAL (N_("Finished locally-encoded frame %1 for R"), _index);
                break;
        default:
                break;
@@@ -290,7 -295,7 +292,7 @@@ DCPVideoFrame::encode_remotely (ServerD
        root->add_child("Version")->add_child_text (raw_convert<string> (SERVER_LINK_VERSION));
        add_metadata (root);
  
-       _log->log (String::compose (N_("Sending frame %1 to remote"), _index));
+       LOG_GENERAL (N_("Sending frame %1 to remote"), _index);
        
        /* Send XML metadata */
        stringstream xml;
        shared_ptr<EncodedData> e (new RemotelyEncodedData (socket->read_uint32 ()));
        socket->read (e->data(), e->size());
  
-       _log->log (String::compose (N_("Finished remotely-encoded frame %1"), _index));
+       LOG_GENERAL (N_("Finished remotely-encoded frame %1"), _index);
        
        return e;
  }
@@@ -385,7 -390,7 +387,7 @@@ EncodedData::write (shared_ptr<const Fi
  }
  
  void
 -EncodedData::write_info (shared_ptr<const Film> film, int frame, Eyes eyes, libdcp::FrameInfo fin) const
 +EncodedData::write_info (shared_ptr<const Film> film, int frame, Eyes eyes, dcp::FrameInfo fin) const
  {
        boost::filesystem::path const info = film->info_path (frame, eyes);
        FILE* h = fopen_boost (info, "w");
diff --combined src/lib/decoder.h
index 0ec73ba91f47147af16b057dcef31004a0b75c47,d67592ed812544c644b8766bcb1b1be1c03e84de..18f612e538c5e645470c33cfcca91175a58a0b13
@@@ -18,7 -18,7 +18,7 @@@
  */
  
  /** @file  src/decoder.h
 - *  @brief Parent class for decoders of content.
 + *  @brief Decoder class.
   */
  
  #ifndef DCPOMATIC_DECODER_H
  #include <boost/shared_ptr.hpp>
  #include <boost/weak_ptr.hpp>
  #include <boost/utility.hpp>
 +#include "types.h"
 +#include "dcpomatic_time.h"
  
 -class Film;
 +class Decoded;
- class Film;
  
  /** @class Decoder.
   *  @brief Parent class for decoders of content.
  class Decoder : public boost::noncopyable
  {
  public:
 -      Decoder (boost::shared_ptr<const Film>);
        virtual ~Decoder () {}
  
 -      /** Perform one decode pass of the content, which may or may not
 -       *  cause the object to emit some data.
 +protected:    
 +      /** Seek so that the next pass() will yield the next thing
 +       *  (video/sound frame, subtitle etc.) at or after the requested
 +       *  time.  Pass accurate = true to try harder to get close to
 +       *  the request.  Note that seeking to time t may mean that
 +       *  the next pass() yields, for example, audio at time t and then
 +       *  video before it.
         */
 -      virtual void pass () = 0;
 -      virtual bool done () const = 0;
 -
 -protected:
 -
 -      virtual void flush () {};
 -      
 -      /** The Film that we are decoding in */
 -      boost::weak_ptr<const Film> _film;
 +      virtual void seek (ContentTime time, bool accurate) = 0;
 +      virtual bool pass () = 0;
  };
  
  #endif
diff --combined src/lib/encoder.cc
index 2364b67a7ceccb5a1ced874efad51b83228fdcaa,e83ac70f52a379cef6688f00e0c72a284544abf5..5dc9e47c772cb7ec0e7c13a91b332c54bce63307
  
  #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_TIMING(...) _film->log()->microsecond_log (String::compose (__VA_ARGS__), Log::TYPE_TIMING);
  using std::pair;
  using std::string;
  using std::stringstream;
@@@ -61,7 -65,9 +65,7 @@@ Encoder::Encoder (shared_ptr<const Film
        , _video_frames_out (0)
        , _terminate (false)
  {
 -      _have_a_real_frame[EYES_BOTH] = false;
 -      _have_a_real_frame[EYES_LEFT] = false;
 -      _have_a_real_frame[EYES_RIGHT] = false;
 +
  }
  
  Encoder::~Encoder ()
@@@ -76,7 -82,7 +80,7 @@@
  void
  Encoder::add_worker_threads (ServerDescription d)
  {
-       _film->log()->log (String::compose (N_("Adding %1 worker threads for remote %2"), d.host_name ()));
+       LOG_GENERAL (N_("Adding %1 worker threads for remote %2"), d.host_name ());
        for (int i = 0; i < d.threads(); ++i) {
                _threads.push_back (new boost::thread (boost::bind (&Encoder::encoder_thread, this, d)));
        }
@@@ -98,11 -104,10 +102,10 @@@ Encoder::process_end (
  {
        boost::mutex::scoped_lock lock (_mutex);
  
-       _film->log()->log (String::compose (N_("Clearing queue of %1"), _queue.size ()));
+       LOG_GENERAL (N_("Clearing queue of %1"), _queue.size ());
  
        /* Keep waking workers until the queue is empty */
        while (!_queue.empty ()) {
-               _film->log()->log (String::compose (N_("Waking with %1"), _queue.size ()), Log::VERBOSE);
                _condition.notify_all ();
                _condition.wait (lock);
        }
        
        terminate_threads ();
  
-       _film->log()->log (String::compose (N_("Mopping up %1"), _queue.size()));
+       LOG_GENERAL (N_("Mopping up %1"), _queue.size());
  
        /* The following sequence of events can occur in the above code:
             1. a remote worker takes the last image off the queue
        */
  
        for (list<shared_ptr<DCPVideoFrame> >::iterator i = _queue.begin(); i != _queue.end(); ++i) {
-               _film->log()->log (String::compose (N_("Encode left-over frame %1"), (*i)->index ()));
+               LOG_GENERAL (N_("Encode left-over frame %1"), (*i)->index ());
                try {
                        _writer->write ((*i)->encode_locally(), (*i)->index (), (*i)->eyes ());
                        frame_done ();
                } catch (std::exception& e) {
-                       _film->log()->log (String::compose (N_("Local encode failed (%1)"), e.what ()));
+                       LOG_ERROR (N_("Local encode failed (%1)"), e.what ());
                }
        }
                
@@@ -178,7 -183,7 +181,7 @@@ Encoder::frame_done (
  }
  
  void
 -Encoder::process_video (shared_ptr<PlayerVideoFrame> pvf, bool same)
 +Encoder::process_video (shared_ptr<PlayerVideoFrame> pvf)
  {
        _waker.nudge ();
        
  
        /* Wait until the queue has gone down a bit */
        while (_queue.size() >= _threads.size() * 2 && !_terminate) {
-               TIMING ("decoder sleeps with queue of %1", _queue.size());
+               LOG_TIMING ("decoder sleeps with queue of %1", _queue.size());
                _condition.wait (lock);
-               TIMING ("decoder wakes with queue of %1", _queue.size());
+               LOG_TIMING ("decoder wakes with queue of %1", _queue.size());
        }
  
        if (_terminate) {
  
        if (_writer->can_fake_write (_video_frames_out)) {
                _writer->fake_write (_video_frames_out, pvf->eyes ());
 -              _have_a_real_frame[pvf->eyes()] = false;
 -              frame_done ();
 -      } else if (same && _have_a_real_frame[pvf->eyes()]) {
 -              /* Use the last frame that we encoded. */
 -              _writer->repeat (_video_frames_out, pvf->eyes());
                frame_done ();
        } else {
                /* Queue this new frame for encoding */
-               TIMING ("adding to queue of %1", _queue.size ());
+               LOG_TIMING ("adding to queue of %1", _queue.size ());
                _queue.push_back (shared_ptr<DCPVideoFrame> (
                                          new DCPVideoFrame (
 -                                                pvf, _video_frames_out, _film->video_frame_rate(),
 -                                                _film->j2k_bandwidth(), _film->resolution(), _film->log()
 +                                                pvf,
 +                                                _video_frames_out,
 +                                                _film->video_frame_rate(),
 +                                                _film->j2k_bandwidth(),
 +                                                _film->resolution(),
 +                                                _film->log()
                                                  )
                                          ));
                
                _condition.notify_all ();
 -              _have_a_real_frame[pvf->eyes()] = true;
        }
  
        if (pvf->eyes() != EYES_LEFT) {
@@@ -266,7 -273,7 +269,7 @@@ tr
        
        while (1) {
  
-               TIMING ("encoder thread %1 sleeps", boost::this_thread::get_id());
+               LOG_TIMING ("[%1] encoder thread sleeps", boost::this_thread::get_id());
                boost::mutex::scoped_lock lock (_mutex);
                while (_queue.empty () && !_terminate) {
                        _condition.wait (lock);
                        return;
                }
  
-               TIMING ("encoder thread %1 wakes with queue of %2", boost::this_thread::get_id(), _queue.size());
+               LOG_TIMING ("[%1] encoder thread wakes with queue of %2", boost::this_thread::get_id(), _queue.size());
                shared_ptr<DCPVideoFrame> vf = _queue.front ();
-               TIMING ("encoder thread %1 pops frame %2 (%3) from queue", boost::this_thread::get_id(), vf->index(), vf->eyes ());
+               LOG_TIMING ("[%1] encoder thread pops frame %2 (%3) from queue", boost::this_thread::get_id(), vf->index(), vf->eyes ());
                _queue.pop_front ();
                
                lock.unlock ();
                                encoded = vf->encode_remotely (server.get ());
  
                                if (remote_backoff > 0) {
-                                       _film->log()->log (String::compose (N_("%1 was lost, but now she is found; removing backoff"), server->host_name ()));
+                                       LOG_GENERAL ("%1 was lost, but now she is found; removing backoff", server->host_name ());
                                }
                                
                                /* This job succeeded, so remove any backoff */
                                        /* back off more */
                                        remote_backoff += 10;
                                }
-                               _film->log()->log (
-                                       String::compose (
-                                               N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"),
-                                               vf->index(), server->host_name(), e.what(), remote_backoff)
+                               LOG_ERROR (
+                                       N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"),
+                                       vf->index(), server->host_name(), e.what(), remote_backoff
                                        );
                        }
                                
                } else {
                        try {
-                               TIMING ("encoder thread %1 begins local encode of %2", boost::this_thread::get_id(), vf->index());
+                               LOG_TIMING ("[%1] encoder thread begins local encode of %2", boost::this_thread::get_id(), vf->index());
                                encoded = vf->encode_locally ();
-                               TIMING ("encoder thread %1 finishes local encode of %2", boost::this_thread::get_id(), vf->index());
+                               LOG_TIMING ("[%1] encoder thread finishes local encode of %2", boost::this_thread::get_id(), vf->index());
                        } catch (std::exception& e) {
-                               _film->log()->log (String::compose (N_("Local encode failed (%1)"), e.what ()));
+                               LOG_ERROR (N_("Local encode failed (%1)"), e.what ());
                        }
                }
  
                        frame_done ();
                } else {
                        lock.lock ();
-                       _film->log()->log (
-                               String::compose (N_("Encoder thread %1 pushes frame %2 back onto queue after failure"), boost::this_thread::get_id(), vf->index())
-                               );
+                       LOG_GENERAL (N_("[%1] Encoder thread pushes frame %2 back onto queue after failure"), boost::this_thread::get_id(), vf->index());
                        _queue.push_front (vf);
                        lock.unlock ();
                }
index 2b507ab371adf3187c08d5b1241ff80c7f359edc,41a3724a29cfee6b19523a776fd0edcb773542ea..9889d511cd45016c97b08746bce15856a953e265
@@@ -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"
@@@ -36,6 -34,8 +36,8 @@@
  
  #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;
@@@ -44,7 -44,7 +46,7 @@@ 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;
@@@ -61,7 -61,7 +63,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)
@@@ -155,7 -155,7 +157,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()));
        }
  }
  
@@@ -166,14 -166,14 +168,14 @@@ FFmpegContent::examine (shared_ptr<Job
  
        Content::examine (job);
  
 -      shared_ptr<const Film> film = _film.lock ();
 -      assert (film);
 -
        shared_ptr<FFmpegExaminer> examiner (new FFmpegExaminer (shared_from_this ()));
 +      take_from_video_examiner (examiner);
 +
 +      ContentTime video_length = examiner->video_length ();
  
 -      VideoContent::Frame video_length = 0;
 -      video_length = examiner->video_length ();
 -      LOG_GENERAL ("Video length obtained from header as %1 frames", video_length);
 +      shared_ptr<const Film> film = _film.lock ();
 +      assert (film);
-       film->log()->log (String::compose ("Video length obtained from header as %1 frames", video_length.frames (video_frame_rate ())));
++      LOG_GENERAL ("Video length obtained from header as %1 frames", video_length.frames (video_frame_rate ()));
  
        {
                boost::mutex::scoped_lock lm (_mutex);
                _first_video = examiner->first_video ();
        }
  
 -      take_from_video_examiner (examiner);
 -
        signal_changed (ContentProperty::LENGTH);
        signal_changed (FFmpegContentProperty::SUBTITLE_STREAMS);
        signal_changed (FFmpegContentProperty::SUBTITLE_STREAM);
@@@ -234,13 -236,13 +236,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;
        
 -      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 ();
@@@ -268,14 -270,19 +270,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
@@@ -291,7 -298,7 +293,7 @@@ FFmpegContent::audio_channels () cons
  }
  
  int
 -FFmpegContent::content_audio_frame_rate () const
 +FFmpegContent::audio_frame_rate () const
  {
        boost::mutex::scoped_lock lm (_mutex);
  
        return _audio_stream->frame_rate;
  }
  
 -int
 -FFmpegContent::output_audio_frame_rate () const
 -{
 -      shared_ptr<const Film> film = _film.lock ();
 -      assert (film);
 -      
 -      /* Resample to a DCI-approved sample rate */
 -      double t = dcp_audio_frame_rate (content_audio_frame_rate ());
 -
 -      FrameRateConversion frc (video_frame_rate(), film->video_frame_rate());
 -
 -      /* Compensate if the DCP is being run at a different frame rate
 -         to the source; that is, if the video is run such that it will
 -         look different in the DCP compared to the source (slower or faster).
 -         skip/repeat doesn't come into effect here.
 -      */
 -
 -      if (frc.change_speed) {
 -              t *= video_frame_rate() * frc.factor() / film->video_frame_rate();
 -      }
 -
 -      return rint (t);
 -}
 -
  bool
  operator== (FFmpegStream const & a, FFmpegStream const & b)
  {
@@@ -314,12 -345,94 +316,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);
 -      
 -      FrameRateConversion 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
@@@ -349,7 -462,7 +351,7 @@@ voi
  FFmpegContent::set_audio_mapping (AudioMapping m)
  {
        audio_stream()->mapping = m;
 -      signal_changed (AudioContentProperty::AUDIO_MAPPING);
 +      AudioContent::set_audio_mapping (m);
  }
  
  string
@@@ -392,18 -505,3 +394,18 @@@ FFmpegContent::audio_analysis_path () c
        p /= name;
        return p;
  }
 +
 +bool
 +FFmpegContent::has_subtitle_during (ContentTimePeriod period) const
 +{
 +      shared_ptr<FFmpegSubtitleStream> stream = subtitle_stream ();
 +
 +      /* XXX: inefficient */
 +      for (vector<ContentTimePeriod>::const_iterator i = stream->periods.begin(); i != stream->periods.end(); ++i) {
 +              if (i->from <= period.to && i->to >= period.from) {
 +                      return true;
 +              }
 +      }
 +
 +      return false;
 +}
index d668deb6f6bbeee617128acbe05489e30874f9a4,5fe34ce14fb3430749f522b93de4aa2971621ff3..0dae2a4edc41be11d70855d94e93b9594dc27c39
@@@ -32,21 -32,24 +32,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 "film.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;
  using std::vector;
@@@ -57,17 -60,23 +62,17 @@@ using std::pair
  using boost::shared_ptr;
  using boost::optional;
  using boost::dynamic_pointer_cast;
 -using libdcp::Size;
 +using dcp::Size;
  
 -FFmpegDecoder::FFmpegDecoder (shared_ptr<const Film> f, shared_ptr<const FFmpegContent> c, bool video, bool audio)
 -      : Decoder (f)
 -      , VideoDecoder (f, c)
 -      , AudioDecoder (f, c)
 -      , SubtitleDecoder (f)
 +FFmpegDecoder::FFmpegDecoder (shared_ptr<const FFmpegContent> c, shared_ptr<Log> log)
 +      : VideoDecoder (c)
 +      , AudioDecoder (c)
 +      , SubtitleDecoder (c)
        , FFmpeg (c)
 +      , _log (log)
        , _subtitle_codec_context (0)
        , _subtitle_codec (0)
 -      , _decode_video (video)
 -      , _decode_audio (audio)
 -      , _pts_offset (0)
 -      , _just_sought (false)
  {
 -      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;
 +              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;
        }
  }
  
@@@ -120,15 -137,20 +125,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));
-                       _log->log (String::compose (N_("error on av_read_frame (%1) (%2)"), buf, r));
 -                      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.
@@@ -288,131 -314,77 +293,131 @@@ FFmpegDecoder::bytes_per_audio_sample (
        return av_get_bytes_per_sample (audio_sample_format ());
  }
  
 -void
 -FFmpegDecoder::seek (VideoContent::Frame frame, bool accurate)
 +int
 +FFmpegDecoder::minimal_run (boost::function<bool (optional<ContentTime>, optional<ContentTime>, int)> finished)
  {
 -      double const time_base = av_q2d (_format_context->streams[_video_stream]->time_base);
 +      int frames_read = 0;
 +      optional<ContentTime> last_video;
 +      optional<ContentTime> last_audio;
  
 -      /* If we are doing an accurate seek, our initial shot will be 5 frames (5 being
 -         a number plucked from the air) earlier than we want to end up.  The loop below
 -         will hopefully then step through to where we want to be.
 -      */
 -      int initial = frame;
 +      while (!finished (last_video, last_audio, frames_read)) {
 +              int r = av_read_frame (_format_context, &_packet);
 +              if (r < 0) {
 +                      /* We should flush our decoders here, possibly yielding a few more frames,
 +                         but the consequence of having to do that is too hideous to contemplate.
 +                         Instead we give up and say that you can't seek too close to the end
 +                         of a file.
 +                      */
 +                      return frames_read;
 +              }
 +
 +              ++frames_read;
 +
 +              double const time_base = av_q2d (_format_context->streams[_packet.stream_index]->time_base);
  
 -      if (accurate) {
 -              initial -= 5;
 +              if (_packet.stream_index == _video_stream) {
 +
 +                      avcodec_get_frame_defaults (_frame);
 +                      
 +                      int got_picture = 0;
 +                      r = avcodec_decode_video2 (video_codec_context(), _frame, &got_picture, &_packet);
 +                      if (r >= 0 && got_picture) {
 +                              last_video = ContentTime::from_seconds (av_frame_get_best_effort_timestamp (_frame) * time_base) + _pts_offset;
 +                      }
 +
 +              } else if (_ffmpeg_content->audio_stream() && _ffmpeg_content->audio_stream()->uses_index (_format_context, _packet.stream_index)) {
 +                      AVPacket copy_packet = _packet;
 +                      while (copy_packet.size > 0) {
 +
 +                              int got_frame;
 +                              r = avcodec_decode_audio4 (audio_codec_context(), _frame, &got_frame, &_packet);
 +                              if (r >= 0 && got_frame) {
 +                                      last_audio = ContentTime::from_seconds (av_frame_get_best_effort_timestamp (_frame) * time_base) + _pts_offset;
 +                              }
 +                                      
 +                              copy_packet.data += r;
 +                              copy_packet.size -= r;
 +                      }
 +              }
 +              
 +              av_free_packet (&_packet);
        }
  
 -      if (initial < 0) {
 -              initial = 0;
 +      return frames_read;
 +}
 +
 +bool
 +FFmpegDecoder::seek_overrun_finished (ContentTime seek, optional<ContentTime> last_video, optional<ContentTime> last_audio) const
 +{
 +      return (last_video && last_video.get() >= seek) || (last_audio && last_audio.get() >= seek);
 +}
 +
 +bool
 +FFmpegDecoder::seek_final_finished (int n, int done) const
 +{
 +      return n == done;
 +}
 +
 +void
 +FFmpegDecoder::seek_and_flush (ContentTime t)
 +{
 +      ContentTime const u = t - _pts_offset;
 +      int64_t s = u.seconds() / av_q2d (_format_context->streams[_video_stream]->time_base);
 +
 +      if (_ffmpeg_content->audio_stream ()) {
 +              s = min (
 +                      s, int64_t (u.seconds() / av_q2d (_ffmpeg_content->audio_stream()->stream(_format_context)->time_base))
 +                      );
        }
  
 -      /* Initial seek time in the stream's timebase */
 -      int64_t const initial_vt = ((initial / _ffmpeg_content->video_frame_rate()) - _pts_offset) / time_base;
 +      /* Ridiculous empirical hack */
 +      s--;
 +      if (s < 0) {
 +              s = 0;
 +      }
  
 -      av_seek_frame (_format_context, _video_stream, initial_vt, AVSEEK_FLAG_BACKWARD);
 +      av_seek_frame (_format_context, _video_stream, s, 0);
  
        avcodec_flush_buffers (video_codec_context());
 +      if (audio_codec_context ()) {
 +              avcodec_flush_buffers (audio_codec_context ());
 +      }
        if (_subtitle_codec_context) {
                avcodec_flush_buffers (_subtitle_codec_context);
        }
 +}
  
 -      /* This !accurate is piling hack upon hack; setting _just_sought to true
 -         even with accurate == true defeats our attempt to align the start
 -         of the video and audio.  Here we disable that defeat when accurate == true
 -         i.e. when we are making a DCP rather than just previewing one.
 -         Ewww.  This should be gone in 2.0.
 +void
 +FFmpegDecoder::seek (ContentTime time, bool accurate)
 +{
 +      VideoDecoder::seek (time, accurate);
 +      AudioDecoder::seek (time, accurate);
 +      
 +      /* If we are doing an accurate seek, our initial shot will be 2s (2 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.
        */
 -      if (!accurate) {
 -              _just_sought = true;
 +
 +      ContentTime pre_roll = accurate ? ContentTime::from_seconds (2) : ContentTime (0);
 +      ContentTime initial_seek = time - pre_roll;
 +      if (initial_seek < ContentTime (0)) {
 +              initial_seek = ContentTime (0);
        }
 -      
 -      _video_position = frame;
 -      
 -      if (frame == 0 || !accurate) {
 -              /* We're already there, or we're as close as we need to be */
 +
 +      /* Initial seek time in the video stream's timebase */
 +
 +      seek_and_flush (initial_seek);
 +
 +      if (!accurate) {
 +              /* That'll do */
                return;
        }
  
 -      while (1) {
 -              int r = av_read_frame (_format_context, &_packet);
 -              if (r < 0) {
 -                      return;
 -              }
 +      int const N = minimal_run (boost::bind (&FFmpegDecoder::seek_overrun_finished, this, time, _1, _2));
  
 -              if (_packet.stream_index != _video_stream) {
 -                      av_free_packet (&_packet);
 -                      continue;
 -              }
 -              
 -              int finished = 0;
 -              r = avcodec_decode_video2 (video_codec_context(), _frame, &finished, &_packet);
 -              if (r >= 0 && finished) {
 -                      _video_position = rint (
 -                              (av_frame_get_best_effort_timestamp (_frame) * time_base + _pts_offset) * _ffmpeg_content->video_frame_rate()
 -                              );
 -
 -                      if (_video_position >= (frame - 1)) {
 -                              av_free_packet (&_packet);
 -                              break;
 -                      }
 -              }
 -              
 -              av_free_packet (&_packet);
 +      seek_and_flush (initial_seek);
 +      if (N > 0) {
 +              minimal_run (boost::bind (&FFmpegDecoder::seek_final_finished, this, N - 1, _3));
        }
  }
  
@@@ -429,23 -401,39 +434,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) {
-                       _log->log (String::compose ("avcodec_decode_audio4 failed (%1)", decode_result));
 -                      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 */
 -                                      shared_ptr<AudioBuffers> silence (
 -                                              new AudioBuffers (
 -                                                      _ffmpeg_content->audio_channels(),
 -                                                      pts * _ffmpeg_content->content_audio_frame_rate()
 -                                                      )
 -                                              );
 -                                      
 -                                      silence->make_silent ();
 -                                      audio (silence, _audio_position);
 -                              }
 -                      }
 +                      ContentTime const ct = ContentTime::from_seconds (
 +                              av_frame_get_best_effort_timestamp (_frame) *
 +                              av_q2d (_ffmpeg_content->audio_stream()->stream (_format_context)->time_base))
 +                              + _pts_offset;
                        
                        int const data_size = av_samples_get_buffer_size (
                                0, audio_codec_context()->channels, _frame->nb_samples, audio_sample_format (), 1
                                );
 -                      
 -                      audio (deinterleave_audio (_frame->data, data_size), _audio_position);
 +
 +                      audio (deinterleave_audio (_frame->data, data_size), ct);
                }
                        
                copy_packet.data += decode_result;
@@@ -466,34 -454,115 +471,37 @@@ 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->log (String::compose (N_("New graph for %1x%2, pixel format %3"), _frame->width, _frame->height, _frame->format));
 -
+               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->video_frame_rate ());
 -                              _just_sought = false;
 -                      }
 -
 -                      double const next = _video_position / _ffmpeg_content->video_frame_rate();
 -                      double const one_frame = 1 / _ffmpeg_content->video_frame_rate ();
 -                      double delta = pts - next;
 -
 -                      while (delta > one_frame) {
 -                              /* This PTS is more than one frame forward in time of where we think we should be; emit
 -                                 a black frame.
 -                              */
 -
 -                              /* XXX: I think this should be a copy of the last frame... */
 -                              boost::shared_ptr<Image> black (
 -                                      new Image (
 -                                              static_cast<AVPixelFormat> (_frame->format),
 -                                              libdcp::Size (video_codec_context()->width, video_codec_context()->height),
 -                                              true
 -                                              )
 -                                      );
 -                              
 -                              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)), rint (pts * _ffmpeg_content->video_frame_rate ()));
++                      video (
++                              shared_ptr<ImageProxy> (new RawImageProxy (image, _video_content->film()->log())),
++                              rint (pts * _ffmpeg_content->video_frame_rate ())
++                              );
                } else {
-                       _log->log ("Dropping frame without PTS");
+                       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 ()
           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);
  }
index df12830f819eb182c2c04db9838e71f550e236ff,d73063083f894fa82a39e5dd316796ac97f79589..d9bcedfc528f196ba9a85b064d1f3900f32fdbbc
@@@ -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,9 -23,6 +23,9 @@@ extern "C" 
  }
  #include "ffmpeg_examiner.h"
  #include "ffmpeg_content.h"
 +#include "ffmpeg_audio_stream.h"
 +#include "ffmpeg_subtitle_stream.h"
 +#include "util.h"
  
  #include "i18n.h"
  
@@@ -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 (1) {
                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) / AV_TIME_BASE);
 -      VideoContent::Frame const length = (double (_format_context->duration - _format_context->start_time) / AV_TIME_BASE) * video_frame_rate();
 -      return max (1, length);
++      ContentTime const length = ContentTime::from_seconds (double (_format_context->duration - _format_context->start_time) / AV_TIME_BASE);
 +      return ContentTime (max (int64_t (1), length.get ()));
  }
  
  string
diff --combined src/lib/film.cc
index 50b1b4fad309e18c7bd848f829fce4e159b8fad6,1b5b2b36601e64eb519b4dfa20f1d13abd298053..25730ae1cbe8bff7e07da16c6eced81e099c7c79
  #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/signer_chain.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"
@@@ -77,21 -77,21 +77,24 @@@ using boost::to_upper_copy
  using boost::ends_with;
  using boost::starts_with;
  using boost::optional;
 -using libdcp::Size;
 -using libdcp::Signer;
 -using libdcp::raw_convert;
 +using dcp::Size;
 +using dcp::Signer;
 +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);
  /* 5 -> 6
   * AudioMapping XML changed.
   * 6 -> 7
   * Subtitle offset changed to subtitle y offset, and subtitle x offset added.
   * 7 -> 8
   * Use <Scale> tag in <VideoContent> rather than <Ratio>.
 + *
 + * Bumped to 32 for 2.0 branch; some times are expressed in Times rather
 + * than frames now.
   */
 -int const Film::current_state_version = 8;
 +int const Film::current_state_version = 32;
  
  /** Construct a Film object in a given directory.
   *
@@@ -256,43 -256,43 +259,43 @@@ Film::make_dcp (
         */
        write_metadata ();
  
-       log()->log (String::compose ("DCP-o-matic %1 git %2 using %3", dcpomatic_version, dcpomatic_git_commit, dependency_version_summary()));
+       LOG_GENERAL ("DCP-o-matic %1 git %2 using %3", dcpomatic_version, dcpomatic_git_commit, dependency_version_summary());
  
        {
                char buffer[128];
                gethostname (buffer, sizeof (buffer));
-               log()->log (String::compose ("Starting to make DCP on %1", buffer));
+               LOG_GENERAL ("Starting to make DCP on %1", buffer);
        }
  
        ContentList cl = content ();
        for (ContentList::const_iterator i = cl.begin(); i != cl.end(); ++i) {
-               log()->log (String::compose ("Content: %1", (*i)->technical_summary()));
+               LOG_GENERAL ("Content: %1", (*i)->technical_summary());
        }
-       log()->log (String::compose ("DCP video rate %1 fps", video_frame_rate()));
-       log()->log (String::compose ("%1 threads", Config::instance()->num_local_encoding_threads()));
-       log()->log (String::compose ("J2K bandwidth %1", j2k_bandwidth()));
+       LOG_GENERAL ("DCP video rate %1 fps", video_frame_rate());
+       LOG_GENERAL ("%1 threads", Config::instance()->num_local_encoding_threads());
+       LOG_GENERAL ("J2K bandwidth %1", j2k_bandwidth());
  #ifdef DCPOMATIC_DEBUG
-       log()->log ("DCP-o-matic built in debug mode.");
+       LOG_GENERAL_NC ("DCP-o-matic built in debug mode.");
  #else
-       log()->log ("DCP-o-matic built in optimised mode.");
+       LOG_GENERAL_NC ("DCP-o-matic built in optimised mode.");
  #endif
  #ifdef LIBDCP_DEBUG
-       log()->log ("libdcp built in debug mode.");
+       LOG_GENERAL_NC ("libdcp built in debug mode.");
  #else
-       log()->log ("libdcp built in optimised mode.");
+       LOG_GENERAL_NC ("libdcp built in optimised mode.");
  #endif
  
  #ifdef DCPOMATIC_WINDOWS
        OSVERSIONINFO info;
        info.dwOSVersionInfoSize = sizeof (info);
        GetVersionEx (&info);
-       log()->log (String::compose ("Windows version %1.%2.%3 SP %4", info.dwMajorVersion, info.dwMinorVersion, info.dwBuildNumber, info.szCSDVersion));
+       LOG_GENERAL ("Windows version %1.%2.%3 SP %4", info.dwMajorVersion, info.dwMinorVersion, info.dwBuildNumber, info.szCSDVersion);
  #endif        
        
-       log()->log (String::compose ("CPU: %1, %2 processors", cpu_info(), boost::thread::hardware_concurrency ()));
+       LOG_GENERAL ("CPU: %1, %2 processors", cpu_info(), boost::thread::hardware_concurrency ());
        list<pair<string, string> > const m = mount_info ();
        for (list<pair<string, string> >::const_iterator i = m.begin(); i != m.end(); ++i) {
-               log()->log (String::compose ("Mount: %1 %2", i->first, i->second));
+               LOG_GENERAL ("Mount: %1 %2", i->first, i->second);
        }
        
        if (container() == 0) {
@@@ -436,7 -436,7 +439,7 @@@ Film::read_metadata (
        _sequence_video = f.bool_child ("SequenceVideo");
        _three_d = f.bool_child ("ThreeD");
        _interop = f.bool_child ("Interop");
 -      _key = libdcp::Key (f.string_child ("Key"));
 +      _key = dcp::Key (f.string_child ("Key"));
  
        list<string> notes;
        /* This method is the only one that can return notes (so far) */
@@@ -766,11 -766,11 +769,11 @@@ Film::j2c_path (int f, Eyes e, bool t) 
        return file (p);
  }
  
- /** @return List of subdirectories (not full paths) containing DCPs that can be successfully dcp::DCP::read() */
list<boost::filesystem::path>
- Film::dcps () const
+ /** Find all the DCPs in our directory that can be libdcp::DCP::read() and return details of their CPLs */
vector<CPLSummary>
+ Film::cpls () const
  {
-       list<boost::filesystem::path> out;
+       vector<CPLSummary> out;
        
        boost::filesystem::path const dir = directory ();
        for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator(dir); i != boost::filesystem::directory_iterator(); ++i) {
                        ) {
  
                        try {
 -                              libdcp::DCP dcp (*i);
 +                              dcp::DCP dcp (*i);
                                dcp.read ();
-                               out.push_back (i->path().leaf ());
+                               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 (...) {
  
                        }
@@@ -879,7 -883,7 +889,7 @@@ Film::move_content_later (shared_ptr<Co
        _playlist->move_later (c);
  }
  
 -Time
 +DCPTime
  Film::length () const
  {
        return _playlist->length ();
@@@ -891,18 -895,12 +901,18 @@@ Film::has_subtitles () cons
        return _playlist->has_subtitles ();
  }
  
 -OutputVideoFrame
 +int
  Film::best_video_frame_rate () const
  {
        return _playlist->best_dcp_frame_rate ();
  }
  
 +FrameRateChange
 +Film::active_frame_rate_change (DCPTime t) const
 +{
 +      return _playlist->active_frame_rate_change (t, video_frame_rate ());
 +}
 +
  void
  Film::playlist_content_changed (boost::weak_ptr<Content> c, int p)
  {
@@@ -921,7 -919,31 +931,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 */
@@@ -937,61 -959,56 +947,50 @@@ 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 ());
  }
  
 -/** @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,
 +      shared_ptr<dcp::Certificate> target,
-       boost::filesystem::path dcp_dir,
+       boost::filesystem::path cpl_file,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime until
 +      dcp::LocalTime from,
 +      dcp::LocalTime until
        ) const
  {
--      shared_ptr<const Signer> signer = make_signer ();
--
-       dcp::DCP dcp (dir (dcp_dir.string ()));
-       
-       try {
-               dcp.read ();
-       } catch (...) {
-               throw KDMError (_("Could not read DCP to make KDM for"));
-       }
-       
-       dcp.cpls().front()->set_mxf_keys (key ());
 -      time_t now = time (0);
 -      struct tm* tm = localtime (&now);
 -      string const issue_date = libdcp::tm_to_string (tm);
--      
 -      return libdcp::KDM (cpl_file, signer, target, key (), from, until, "DCP-o-matic", issue_date);
++      shared_ptr<const dcp::CPL> cpl (new dcp::CPL (cpl_file));
 +      return dcp::DecryptedKDM (
-               dcp.cpls().front(), from, until, "DCP-o-matic", dcp.cpls().front()->content_title_text(), dcp::LocalTime().as_string()
-               ).encrypt (signer, target);
++              cpl, from, until, "DCP-o-matic", cpl->content_title_text(), dcp::LocalTime().as_string()
++              ).encrypt (make_signer(), target);
  }
  
 -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
 +      dcp::LocalTime from,
 +      dcp::LocalTime until
        ) 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));
  uint64_t
  Film::required_disk_space () const
  {
 -      return uint64_t (j2k_bandwidth() / 8) * length() / TIME_HZ;
 +      return uint64_t (j2k_bandwidth() / 8) * length().seconds();
  }
  
  /** This method checks the disk that the Film is on and tries to decide whether or not
diff --combined src/lib/film.h
index ee8756b3dfdcf860d837c0dd90a11188427bd350,06c770efadc2860ced9e4dcce4b98d2e2aa643db..d9d7e82fd2140a4cbf9fae20c69ea9e6badd4db6
@@@ -31,9 -31,8 +31,9 @@@
  #include <boost/signals2.hpp>
  #include <boost/enable_shared_from_this.hpp>
  #include <boost/filesystem.hpp>
 -#include <libdcp/key.h>
 -#include <libdcp/kdm.h>
 +#include <dcp/key.h>
 +#include <dcp/decrypted_kdm.h>
 +#include <dcp/encrypted_kdm.h>
  #include "util.h"
  #include "types.h"
  #include "dci_metadata.h"
@@@ -96,15 -95,20 +96,15 @@@ public
                return _dirty;
        }
  
 -      libdcp::Size full_frame () const;
 -      libdcp::Size frame_size () const;
 +      dcp::Size full_frame () const;
 +      dcp::Size frame_size () const;
  
-       std::list<boost::filesystem::path> dcps () const;
+       std::vector<CPLSummary> cpls () const;
  
        boost::shared_ptr<Player> make_player () const;
        boost::shared_ptr<Playlist> playlist () const;
  
 -      OutputAudioFrame audio_frame_rate () const;
 -
 -      OutputAudioFrame time_to_audio_frames (Time) const;
 -      OutputVideoFrame time_to_video_frames (Time) const;
 -      Time video_frames_to_time (OutputVideoFrame) const;
 -      Time audio_frames_to_time (OutputAudioFrame) const;
 +      int audio_frame_rate () const;
  
        uint64_t required_disk_space () const;
        bool should_be_enough_disk_space (double &, double &) const;
        /* Proxies for some Playlist methods */
  
        ContentList content () const;
 -      Time length () const;
 +      DCPTime length () const;
        bool has_subtitles () const;
 -      OutputVideoFrame best_video_frame_rate () const;
 +      int best_video_frame_rate () const;
 +      FrameRateChange active_frame_rate_change (DCPTime) const;
  
 -      libdcp::KDM
 +      dcp::EncryptedKDM
        make_kdm (
 -              boost::shared_ptr<libdcp::Certificate> target,
 +              boost::shared_ptr<dcp::Certificate> target,
-               boost::filesystem::path dcp,
+               boost::filesystem::path cpl_file,
 -              boost::posix_time::ptime from,
 -              boost::posix_time::ptime until
 +              dcp::LocalTime from,
 +              dcp::LocalTime until
                ) const;
        
 -      std::list<libdcp::KDM> make_kdms (
 +      std::list<dcp::EncryptedKDM> make_kdms (
                std::list<boost::shared_ptr<Screen> >,
-               boost::filesystem::path dcp,
+               boost::filesystem::path cpl_file,
 -              boost::posix_time::ptime from,
 -              boost::posix_time::ptime until
 +              dcp::LocalTime from,
 +              dcp::LocalTime until
                ) const;
  
 -      libdcp::Key key () const {
 +      dcp::Key key () const {
                return _key;
        }
  
@@@ -325,7 -328,7 +325,7 @@@ private
        bool _three_d;
        bool _sequence_video;
        bool _interop;
 -      libdcp::Key _key;
 +      dcp::Key _key;
  
        int _state_version;
  
diff --combined src/lib/image_decoder.cc
index 9f83d1d896d1444579bcd157b214b389468b9f30,7a9acd9e4960e611bda76e912e4837738e94dc59..d3cdbd6f1ee753b994919cd677828662c1b96a8e
  
  using std::cout;
  using boost::shared_ptr;
 -using libdcp::Size;
 +using dcp::Size;
  
 -ImageDecoder::ImageDecoder (shared_ptr<const Film> f, shared_ptr<const ImageContent> c)
 -      : Decoder (f)
 -      , VideoDecoder (f, c)
 +ImageDecoder::ImageDecoder (shared_ptr<const ImageContent> c)
 +      : VideoDecoder (c)
        , _image_content (c)
  {
  
  }
  
 -void
 +bool
  ImageDecoder::pass ()
  {
 -      if (_video_position >= _image_content->video_length ()) {
 -              return;
 +      if (_video_position >= _image_content->video_length().frames (_image_content->video_frame_rate ())) {
 +              return true;
        }
  
 -      if (_image && _image_content->still ()) {
 -              video (_image, true, _video_position);
 -              return;
 +      if (!_image_content->still() || !_image) {
 +              /* Either we need an image or we are using moving images, so load one */
-               _image.reset (new MagickImageProxy (_image_content->path (_image_content->still() ? 0 : _video_position)));
++              _image.reset (new MagickImageProxy (_image_content->path (_image_content->still() ? 0 : _video_position), _image_content->film()->log ()));
        }
 -
 -      shared_ptr<const Film> film = _film.lock ();
 -      assert (film);
 -
 -      _image.reset (new MagickImageProxy (_image_content->path (_image_content->still() ? 0 : _video_position), film->log ()));
 -      video (_image, false, _video_position);
 +              
 +      video (_image, _video_position);
 +      ++_video_position;
 +      return false;
  }
  
  void
 -ImageDecoder::seek (VideoContent::Frame frame, bool)
 -{
 -      _video_position = frame;
 -}
 -
 -bool
 -ImageDecoder::done () const
 +ImageDecoder::seek (ContentTime time, bool accurate)
  {
 -      return _video_position >= _image_content->video_length ();
 +      VideoDecoder::seek (time, accurate);
 +      _video_position = time.frames (_image_content->video_frame_rate ());
  }
diff --combined src/lib/image_proxy.cc
index c74e846c99680b1e0e3f4ab0319ad17c109f1c9a,230bfacadc51cfba2dcf6eeb76b281da95e895cd..16bd92f6ece1ea0f933cc0b09ec8a168533c0ece
  */
  
  #include <Magick++.h>
 -#include <libdcp/util.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/util.h>
 +#include <dcp/raw_convert.h>
  #include "image_proxy.h"
  #include "image.h"
  #include "exceptions.h"
  #include "cross.h"
+ #include "log.h"
  
  #include "i18n.h"
  
+ #define LOG_TIMING(...) _log->microsecond_log (String::compose (__VA_ARGS__), Log::TYPE_TIMING);
  using std::cout;
  using std::string;
  using std::stringstream;
  using boost::shared_ptr;
  
RawImageProxy::RawImageProxy (shared_ptr<Image> image)
-       : _image (image)
ImageProxy::ImageProxy (shared_ptr<Log> log)
+       : _log (log)
  {
  
  }
  
- RawImageProxy::RawImageProxy (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket)
+ RawImageProxy::RawImageProxy (shared_ptr<Image> image, shared_ptr<Log> log)
+       : ImageProxy (log)
+       , _image (image)
+ {
+ }
+ RawImageProxy::RawImageProxy (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket, shared_ptr<Log> log)
+       : ImageProxy (log)
  {
 -      libdcp::Size size (
 +      dcp::Size size (
                xml->number_child<int> ("Width"), xml->number_child<int> ("Height")
                );
  
@@@ -58,8 -69,8 +69,8 @@@ voi
  RawImageProxy::add_metadata (xmlpp::Node* node) const
  {
        node->add_child("Type")->add_child_text (N_("Raw"));
 -      node->add_child("Width")->add_child_text (libdcp::raw_convert<string> (_image->size().width));
 -      node->add_child("Height")->add_child_text (libdcp::raw_convert<string> (_image->size().height));
 +      node->add_child("Width")->add_child_text (dcp::raw_convert<string> (_image->size().width));
 +      node->add_child("Height")->add_child_text (dcp::raw_convert<string> (_image->size().height));
  }
  
  void
@@@ -68,7 -79,8 +79,8 @@@ RawImageProxy::send_binary (shared_ptr<
        _image->write_to_socket (socket);
  }
  
- MagickImageProxy::MagickImageProxy (boost::filesystem::path path)
+ MagickImageProxy::MagickImageProxy (boost::filesystem::path path, shared_ptr<Log> log)
+       : ImageProxy (log)
  {
        /* Read the file into a Blob */
        
        delete[] data;
  }
  
- MagickImageProxy::MagickImageProxy (shared_ptr<cxml::Node>, shared_ptr<Socket> socket)
+ MagickImageProxy::MagickImageProxy (shared_ptr<cxml::Node>, shared_ptr<Socket> socket, shared_ptr<Log> log)
+       : ImageProxy (log)
  {
        uint32_t const size = socket->read_uint32 ();
        uint8_t* data = new uint8_t[size];
@@@ -105,6 -118,8 +118,8 @@@ MagickImageProxy::image () cons
                return _image;
        }
  
+       LOG_TIMING ("[%1] MagickImageProxy begins decode and convert of %2 bytes", boost::this_thread::get_id(), _blob.length());
        Magick::Image* magick_image = 0;
        try {
                magick_image = new Magick::Image (_blob);
                throw DecodeError (_("Could not decode image file"));
        }
  
 +      dcp::Size size (magick_image->columns(), magick_image->rows());
+       LOG_TIMING ("[%1] MagickImageProxy decode finished", boost::this_thread::get_id ());
  
 -      libdcp::Size size (magick_image->columns(), magick_image->rows());
 -
        _image.reset (new Image (PIX_FMT_RGB24, size, true));
  
        using namespace MagickCore;
  
        delete magick_image;
  
+       LOG_TIMING ("[%1] MagickImageProxy completes decode and convert of %2 bytes", boost::this_thread::get_id(), _blob.length());
        return _image;
  }
  
@@@ -149,12 -168,12 +167,12 @@@ MagickImageProxy::send_binary (shared_p
  }
  
  shared_ptr<ImageProxy>
- image_proxy_factory (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket)
+ image_proxy_factory (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket, shared_ptr<Log> log)
  {
        if (xml->string_child("Type") == N_("Raw")) {
-               return shared_ptr<ImageProxy> (new RawImageProxy (xml, socket));
+               return shared_ptr<ImageProxy> (new RawImageProxy (xml, socket, log));
        } else if (xml->string_child("Type") == N_("Magick")) {
-               return shared_ptr<MagickImageProxy> (new MagickImageProxy (xml, socket));
+               return shared_ptr<MagickImageProxy> (new MagickImageProxy (xml, socket, log));
        }
  
        throw NetworkError (_("Unexpected image type received by server"));
diff --combined src/lib/job.cc
index c6a6b90a808fcdd234c053ce1c21a31d3128d3eb,96aedac65bf4fb8718d8749712db3ae28d9ae627..594c0da34f1b5cdc7a55177e61bee698b5ac21d5
@@@ -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 "film.h"
 +#include "log.h"
  
  #include "i18n.h"
  
@@@ -68,8 -66,8 +68,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 +206,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 +281,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));
++      _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;
diff --combined src/lib/kdm.cc
index 902f0d33322a48617e71e193801780704a46ca39,d5d5ec0a0b16d5e503070d09c158b4ff61733ee6..c08750961e2c7c77245191e8e99f2b97daf5d875
@@@ -21,7 -21,7 +21,7 @@@
  #include <boost/shared_ptr.hpp>
  #include <quickmail.h>
  #include <zip.h>
 -#include <libdcp/kdm.h>
 +#include <dcp/encrypted_kdm.h>
  #include "kdm.h"
  #include "cinema.h"
  #include "exceptions.h"
@@@ -36,13 -36,13 +36,13 @@@ 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
@@@ -102,17 -102,17 +102,17 @@@ static list<ScreenKDM
  make_screen_kdms (
        shared_ptr<const Film> film,
        list<shared_ptr<Screen> > screens,
-       boost::filesystem::path dcp,
+       boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to
 +      dcp::LocalTime from,
 +      dcp::LocalTime to
        )
  {
-       list<dcp::EncryptedKDM> kdms = film->make_kdms (screens, dcp, from, to);
 -      list<libdcp::KDM> kdms = film->make_kdms (screens, cpl, from, to);
++      list<dcp::EncryptedKDM> kdms = film->make_kdms (screens, cpl, from, to);
           
        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;
@@@ -126,12 -126,12 +126,12 @@@ static list<CinemaKDMs
  make_cinema_kdms (
        shared_ptr<const Film> film,
        list<shared_ptr<Screen> > screens,
-       boost::filesystem::path dcp,
+       boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to
 +      dcp::LocalTime from,
 +      dcp::LocalTime to
        )
  {
-       list<ScreenKDM> screen_kdms = make_screen_kdms (film, screens, dcp, from, to);
+       list<ScreenKDM> screen_kdms = make_screen_kdms (film, screens, cpl, from, to);
        list<CinemaKDMs> cinema_kdms;
  
        while (!screen_kdms.empty ()) {
@@@ -171,13 -171,13 +171,13 @@@ voi
  write_kdm_files (
        shared_ptr<const Film> film,
        list<shared_ptr<Screen> > screens,
-       boost::filesystem::path dcp,
+       boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to,
 +      dcp::LocalTime from,
 +      dcp::LocalTime to,
        boost::filesystem::path directory
        )
  {
-       list<ScreenKDM> screen_kdms = make_screen_kdms (film, screens, dcp, from, to);
+       list<ScreenKDM> screen_kdms = make_screen_kdms (film, screens, cpl, from, to);
  
        /* Write KDMs to the specified directory */
        for (list<ScreenKDM>::iterator i = screen_kdms.begin(); i != screen_kdms.end(); ++i) {
@@@ -191,13 -191,13 +191,13 @@@ voi
  write_kdm_zip_files (
        shared_ptr<const Film> film,
        list<shared_ptr<Screen> > screens,
-       boost::filesystem::path dcp,
+       boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to,
 +      dcp::LocalTime from,
 +      dcp::LocalTime to,
        boost::filesystem::path directory
        )
  {
-       list<CinemaKDMs> cinema_kdms = make_cinema_kdms (film, screens, dcp, from, to);
+       list<CinemaKDMs> cinema_kdms = make_cinema_kdms (film, screens, cpl, from, to);
  
        for (list<CinemaKDMs>::const_iterator i = cinema_kdms.begin(); i != cinema_kdms.end(); ++i) {
                boost::filesystem::path path = directory;
@@@ -210,12 -210,12 +210,12 @@@ voi
  email_kdms (
        shared_ptr<const Film> film,
        list<shared_ptr<Screen> > screens,
-       boost::filesystem::path dcp,
+       boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to
 +      dcp::LocalTime from,
 +      dcp::LocalTime to
        )
  {
-       list<CinemaKDMs> cinema_kdms = make_cinema_kdms (film, screens, dcp, from, to);
+       list<CinemaKDMs> cinema_kdms = make_cinema_kdms (film, screens, cpl, from, to);
  
        for (list<CinemaKDMs>::const_iterator i = cinema_kdms.begin(); i != cinema_kdms.end(); ++i) {
                
diff --combined src/lib/kdm.h
index 5df161b2a03404beafc39d88fa2d7dddb0b83e8e,8aacd7b720fe8bfce57c7f75b493d8fb28f17a39..023107a826875f169c4e1c780ee9666ba366eefc
@@@ -26,26 -26,26 +26,26 @@@ class Film
  extern void write_kdm_files (
        boost::shared_ptr<const Film> film,
        std::list<boost::shared_ptr<Screen> > screens,
-       boost::filesystem::path dcp,
+       boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to,
 +      dcp::LocalTime from,
 +      dcp::LocalTime to,
        boost::filesystem::path directory
        );
  
  extern void write_kdm_zip_files (
        boost::shared_ptr<const Film> film,
        std::list<boost::shared_ptr<Screen> > screens,
-       boost::filesystem::path dcp,
+       boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to,
 +      dcp::LocalTime from,
 +      dcp::LocalTime to,
        boost::filesystem::path directory
        );
  
  extern void email_kdms (
        boost::shared_ptr<const Film> film,
        std::list<boost::shared_ptr<Screen> > screens,
-       boost::filesystem::path dcp,
+       boost::filesystem::path cpl,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime to
 +      dcp::LocalTime from,
 +      dcp::LocalTime to
        );
  
diff --combined src/lib/player.cc
index b60dd846786056a2918b3a1338a8382183e81cea,b112760ef412a024fa581c9779ca5be4b1c65138..77def1e60d7eec92d83bbe66168fcb562107b827
  */
  
  #include <stdint.h>
 +#include <algorithm>
  #include "player.h"
  #include "film.h"
  #include "ffmpeg_decoder.h"
 +#include "audio_buffers.h"
  #include "ffmpeg_content.h"
  #include "image_decoder.h"
  #include "image_content.h"
  #include "sndfile_decoder.h"
  #include "sndfile_content.h"
  #include "subtitle_content.h"
 +#include "subrip_decoder.h"
 +#include "subrip_content.h"
  #include "playlist.h"
  #include "job.h"
  #include "image.h"
  #include "image_proxy.h"
  #include "ratio.h"
 -#include "resampler.h"
  #include "log.h"
  #include "scaler.h"
 +#include "render_subtitles.h"
 +#include "config.h"
 +#include "content_video.h"
  #include "player_video_frame.h"
  
+ #define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
  using std::list;
  using std::cout;
  using std::min;
  using std::max;
 +using std::min;
  using std::vector;
  using std::pair;
  using std::map;
 +using std::make_pair;
  using boost::shared_ptr;
  using boost::weak_ptr;
  using boost::dynamic_pointer_cast;
 +using boost::optional;
  
  Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p)
        : _film (f)
        , _playlist (p)
 -      , _video (true)
 -      , _audio (true)
        , _have_valid_pieces (false)
 -      , _video_position (0)
 -      , _audio_position (0)
 -      , _audio_merger (f->audio_channels(), bind (&Film::time_to_audio_frames, f.get(), _1), bind (&Film::audio_frames_to_time, f.get(), _1))
 -      , _last_emit_was_black (false)
 +      , _approximate_size (false)
 +      , _burn_subtitles (false)
  {
        _playlist_changed_connection = _playlist->Changed.connect (bind (&Player::playlist_changed, this));
        _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Player::content_changed, this, _1, _2, _3));
  }
  
  void
 -Player::disable_video ()
 -{
 -      _video = false;
 -}
 -
 -void
 -Player::disable_audio ()
 +Player::setup_pieces ()
  {
 -      _audio = false;
 -}
 +      list<shared_ptr<Piece> > old_pieces = _pieces;
 +      _pieces.clear ();
  
 -bool
 -Player::pass ()
 -{
 -      if (!_have_valid_pieces) {
 -              setup_pieces ();
 -      }
 +      ContentList content = _playlist->content ();
  
 -      Time earliest_t = TIME_MAX;
 -      shared_ptr<Piece> earliest;
 -      enum {
 -              VIDEO,
 -              AUDIO
 -      } type = VIDEO;
 +      for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
  
 -      for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -              if ((*i)->decoder->done ()) {
 +              if (!(*i)->paths_valid ()) {
                        continue;
                }
 -
 -              shared_ptr<VideoDecoder> vd = dynamic_pointer_cast<VideoDecoder> ((*i)->decoder);
 -              shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 -
 -              if (_video && vd) {
 -                      if ((*i)->video_position < earliest_t) {
 -                              earliest_t = (*i)->video_position;
 -                              earliest = *i;
 -                              type = VIDEO;
 +              
 +              shared_ptr<Decoder> decoder;
 +              optional<FrameRateChange> frc;
 +
 +              /* Work out a FrameRateChange for the best overlap video for this content, in case we need it below */
 +              DCPTime best_overlap_t;
 +              shared_ptr<VideoContent> best_overlap;
 +              for (ContentList::iterator j = content.begin(); j != content.end(); ++j) {
 +                      shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*j);
 +                      if (!vc) {
 +                              continue;
                        }
 -              }
 -
 -              if (_audio && ad && ad->has_audio ()) {
 -                      if ((*i)->audio_position < earliest_t) {
 -                              earliest_t = (*i)->audio_position;
 -                              earliest = *i;
 -                              type = AUDIO;
 +                      
 +                      DCPTime const overlap = max (vc->position(), (*i)->position()) - min (vc->end(), (*i)->end());
 +                      if (overlap > best_overlap_t) {
 +                              best_overlap = vc;
 +                              best_overlap_t = overlap;
                        }
                }
 -      }
  
 -      if (!earliest) {
 -              flush ();
 -              return true;
 -      }
 -
 -      switch (type) {
 -      case VIDEO:
 -              if (earliest_t > _video_position) {
 -                      emit_black ();
 +              optional<FrameRateChange> best_overlap_frc;
 +              if (best_overlap) {
 +                      best_overlap_frc = FrameRateChange (best_overlap->video_frame_rate(), _film->video_frame_rate ());
                } else {
 -                      if (earliest->repeating ()) {
 -                              earliest->repeat (this);
 -                      } else {
 -                              earliest->decoder->pass ();
 -                      }
 +                      /* No video overlap; e.g. if the DCP is just audio */
 +                      best_overlap_frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ());
                }
 -              break;
  
 -      case AUDIO:
 -              if (earliest_t > _audio_position) {
 -                      emit_silence (_film->time_to_audio_frames (earliest_t - _audio_position));
 -              } else {
 -                      earliest->decoder->pass ();
 -
 -                      if (earliest->decoder->done()) {
 -                              shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (earliest->content);
 -                              assert (ac);
 -                              shared_ptr<Resampler> re = resampler (ac, false);
 -                              if (re) {
 -                                      shared_ptr<const AudioBuffers> b = re->flush ();
 -                                      if (b->frames ()) {
 -                                              process_audio (earliest, b, ac->audio_length ());
 -                                      }
 -                              }
 -                      }
 +              /* FFmpeg */
 +              shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i);
 +              if (fc) {
 +                      decoder.reset (new FFmpegDecoder (fc, _film->log()));
 +                      frc = FrameRateChange (fc->video_frame_rate(), _film->video_frame_rate());
                }
 -              break;
 -      }
  
 -      if (_audio) {
 -              boost::optional<Time> audio_done_up_to;
 -              for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -                      if ((*i)->decoder->done ()) {
 -                              continue;
 +              /* ImageContent */
 +              shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i);
 +              if (ic) {
 +                      /* See if we can re-use an old ImageDecoder */
 +                      for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) {
 +                              shared_ptr<ImageDecoder> imd = dynamic_pointer_cast<ImageDecoder> ((*j)->decoder);
 +                              if (imd && imd->content() == ic) {
 +                                      decoder = imd;
 +                              }
                        }
  
 -                      shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 -                      if (ad && ad->has_audio ()) {
 -                              audio_done_up_to = min (audio_done_up_to.get_value_or (TIME_MAX), (*i)->audio_position);
 +                      if (!decoder) {
 +                              decoder.reset (new ImageDecoder (ic));
                        }
 +
 +                      frc = FrameRateChange (ic->video_frame_rate(), _film->video_frame_rate());
                }
  
 -              if (audio_done_up_to) {
 -                      TimedAudioBuffers<Time> tb = _audio_merger.pull (audio_done_up_to.get ());
 -                      Audio (tb.audio, tb.time);
 -                      _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
 +              /* SndfileContent */
 +              shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i);
 +              if (sc) {
 +                      decoder.reset (new SndfileDecoder (sc));
 +                      frc = best_overlap_frc;
                }
 +
 +              /* SubRipContent */
 +              shared_ptr<const SubRipContent> rc = dynamic_pointer_cast<const SubRipContent> (*i);
 +              if (rc) {
 +                      decoder.reset (new SubRipDecoder (rc));
 +                      frc = best_overlap_frc;
 +              }
 +
 +              _pieces.push_back (shared_ptr<Piece> (new Piece (*i, decoder, frc.get ())));
        }
 -              
 -      return false;
 +
 +      _have_valid_pieces = true;
  }
  
 -/** @param extra Amount of extra time to add to the content frame's time (for repeat) */
  void
 -Player::process_video (weak_ptr<Piece> weak_piece, shared_ptr<const ImageProxy> image, Eyes eyes, Part part, bool same, VideoContent::Frame frame, Time extra)
 +Player::content_changed (weak_ptr<Content> w, int property, bool frequent)
  {
 -      /* Keep a note of what came in so that we can repeat it if required */
 -      _last_incoming_video.weak_piece = weak_piece;
 -      _last_incoming_video.image = image;
 -      _last_incoming_video.eyes = eyes;
 -      _last_incoming_video.part = part;
 -      _last_incoming_video.same = same;
 -      _last_incoming_video.frame = frame;
 -      _last_incoming_video.extra = extra;
 -      
 -      shared_ptr<Piece> piece = weak_piece.lock ();
 -      if (!piece) {
 -              return;
 -      }
 -
 -      shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
 -      assert (content);
 -
 -      FrameRateConversion frc (content->video_frame_rate(), _film->video_frame_rate());
 -      if (frc.skip && (frame % 2) == 1) {
 -              return;
 -      }
 -
 -      Time const relative_time = (frame * frc.factor() * TIME_HZ / _film->video_frame_rate());
 -      if (content->trimmed (relative_time)) {
 +      shared_ptr<Content> c = w.lock ();
 +      if (!c) {
                return;
        }
  
 -      Time const time = content->position() + relative_time + extra - content->trim_start ();
 -      libdcp::Size const image_size = content->scale().size (content, _video_container_size, _film->frame_size ());
 -
 -      shared_ptr<PlayerVideoFrame> pi (
 -              new PlayerVideoFrame (
 -                      image,
 -                      content->crop(),
 -                      image_size,
 -                      _video_container_size,
 -                      _film->scaler(),
 -                      eyes,
 -                      part,
 -                      content->colour_conversion()
 -                      )
 -              );
 -      
 -      if (_film->with_subtitles ()) {
 -              for (list<Subtitle>::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
 -                      if (i->covers (time)) {
 -                              /* This may be true for more than one of _subtitles, but the last (latest-starting)
 -                                 one is the one we want to use, so that's ok.
 -                              */
 -                              Position<int> const container_offset (
 -                                      (_video_container_size.width - image_size.width) / 2,
 -                                      (_video_container_size.height - image_size.width) / 2
 -                                      );
 -                              
 -                              pi->set_subtitle (i->out_image(), i->out_position() + container_offset);
 -                      }
 -              }
 -      }
 -
 -      /* Clear out old subtitles */
 -      for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ) {
 -              list<Subtitle>::iterator j = i;
 -              ++j;
 +      if (
 +              property == ContentProperty::POSITION ||
 +              property == ContentProperty::LENGTH ||
 +              property == ContentProperty::TRIM_START ||
 +              property == ContentProperty::TRIM_END ||
 +              property == ContentProperty::PATH ||
 +              property == VideoContentProperty::VIDEO_FRAME_TYPE
 +              ) {
                
 -              if (i->ends_before (time)) {
 -                      _subtitles.erase (i);
 -              }
 -
 -              i = j;
 -      }
 -
 -#ifdef DCPOMATIC_DEBUG
 -      _last_video = piece->content;
 -#endif
 -
 -      Video (pi, same, time);
 -
 -      _last_emit_was_black = false;
 -      _video_position = piece->video_position = (time + TIME_HZ / _film->video_frame_rate());
 +              _have_valid_pieces = false;
 +              Changed (frequent);
  
 -      if (frc.repeat > 1 && !piece->repeating ()) {
 -              piece->set_repeat (_last_incoming_video, frc.repeat - 1);
 +      } else if (
 +              property == SubtitleContentProperty::SUBTITLE_X_OFFSET ||
 +              property == SubtitleContentProperty::SUBTITLE_Y_OFFSET ||
 +              property == SubtitleContentProperty::SUBTITLE_SCALE ||
 +              property == VideoContentProperty::VIDEO_CROP ||
 +              property == VideoContentProperty::VIDEO_SCALE ||
 +              property == VideoContentProperty::VIDEO_FRAME_RATE
 +              ) {
 +              
 +              Changed (frequent);
        }
  }
  
  void
 -Player::process_audio (weak_ptr<Piece> weak_piece, shared_ptr<const AudioBuffers> audio, AudioContent::Frame frame)
 +Player::playlist_changed ()
  {
 -      shared_ptr<Piece> piece = weak_piece.lock ();
 -      if (!piece) {
 -              return;
 -      }
 +      _have_valid_pieces = false;
 +      Changed (false);
 +}
  
 -      shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> (piece->content);
 -      assert (content);
 +void
 +Player::set_video_container_size (dcp::Size s)
 +{
 +      _video_container_size = s;
  
 -      /* Gain */
 -      if (content->audio_gain() != 0) {
 -              shared_ptr<AudioBuffers> gain (new AudioBuffers (audio));
 -              gain->apply_gain (content->audio_gain ());
 -              audio = gain;
 -      }
 +      _black_image.reset (new Image (PIX_FMT_RGB24, _video_container_size, true));
 +      _black_image->make_black ();
 +}
  
 -      /* Resample */
 -      if (content->content_audio_frame_rate() != content->output_audio_frame_rate()) {
 -              shared_ptr<Resampler> r = resampler (content, true);
 -              pair<shared_ptr<const AudioBuffers>, AudioContent::Frame> ro = r->run (audio, frame);
 -              audio = ro.first;
 -              frame = ro.second;
 -      }
 -      
 -      Time const relative_time = _film->audio_frames_to_time (frame);
 +void
 +Player::film_changed (Film::Property p)
 +{
 +      /* Here we should notice Film properties that affect our output, and
 +         alert listeners that our output now would be different to how it was
 +         last time we were run.
 +      */
  
 -      if (content->trimmed (relative_time)) {
 -              return;
 +      if (p == Film::SCALER || p == Film::WITH_SUBTITLES || p == Film::CONTAINER || p == Film::VIDEO_FRAME_RATE) {
 +              Changed (false);
        }
 +}
  
 -      Time time = content->position() + (content->audio_delay() * TIME_HZ / 1000) + relative_time - content->trim_start ();
 +list<PositionImage>
 +Player::process_content_image_subtitles (shared_ptr<SubtitleContent> content, list<shared_ptr<ContentImageSubtitle> > subs) const
 +{
 +      list<PositionImage> all;
        
 -      /* Remap channels */
 -      shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->frames()));
 -      dcp_mapped->make_silent ();
 -
 -      AudioMapping map = content->audio_mapping ();
 -      for (int i = 0; i < map.content_channels(); ++i) {
 -              for (int j = 0; j < _film->audio_channels(); ++j) {
 -                      if (map.get (i, static_cast<libdcp::Channel> (j)) > 0) {
 -                              dcp_mapped->accumulate_channel (
 -                                      audio.get(),
 -                                      i,
 -                                      static_cast<libdcp::Channel> (j),
 -                                      map.get (i, static_cast<libdcp::Channel> (j))
 -                                      );
 -                      }
 +      for (list<shared_ptr<ContentImageSubtitle> >::const_iterator i = subs.begin(); i != subs.end(); ++i) {
 +              if (!(*i)->image) {
 +                      continue;
                }
 +
 +              dcpomatic::Rect<double> in_rect = (*i)->rectangle;
 +              dcp::Size scaled_size;
 +              
 +              in_rect.x += content->subtitle_x_offset ();
 +              in_rect.y += content->subtitle_y_offset ();
 +              
 +              /* We will scale the subtitle up to fit _video_container_size, and also by the additional subtitle_scale */
 +              scaled_size.width = in_rect.width * _video_container_size.width * content->subtitle_scale ();
 +              scaled_size.height = in_rect.height * _video_container_size.height * content->subtitle_scale ();
 +              
 +              /* Then we need a corrective translation, consisting of two parts:
 +               *
 +               * 1.  that which is the result of the scaling of the subtitle by _video_container_size; this will be
 +               *     rect.x * _video_container_size.width and rect.y * _video_container_size.height.
 +               *
 +               * 2.  that to shift the origin of the scale by subtitle_scale to the centre of the subtitle; this will be
 +               *     (width_before_subtitle_scale * (1 - subtitle_scale) / 2) and
 +               *     (height_before_subtitle_scale * (1 - subtitle_scale) / 2).
 +               *
 +               * Combining these two translations gives these expressions.
 +               */
 +
 +              all.push_back (
 +                      PositionImage (
 +                              (*i)->image->scale (
 +                                      scaled_size,
 +                                      Scaler::from_id ("bicubic"),
 +                                      (*i)->image->pixel_format (),
 +                                      true
 +                                      ),
 +                              Position<int> (
 +                                      rint (_video_container_size.width * (in_rect.x + (in_rect.width * (1 - content->subtitle_scale ()) / 2))),
 +                                      rint (_video_container_size.height * (in_rect.y + (in_rect.height * (1 - content->subtitle_scale ()) / 2)))
 +                                      )
 +                              )
 +                      );
        }
  
 -      audio = dcp_mapped;
 +      return all;
 +}
  
 -      /* We must cut off anything that comes before the start of all time */
 -      if (time < 0) {
 -              int const frames = - time * _film->audio_frame_rate() / TIME_HZ;
 -              if (frames >= audio->frames ()) {
 -                      return;
 +list<PositionImage>
 +Player::process_content_text_subtitles (list<shared_ptr<ContentTextSubtitle> > sub) const
 +{
 +      list<PositionImage> all;
 +      for (list<shared_ptr<ContentTextSubtitle> >::const_iterator i = sub.begin(); i != sub.end(); ++i) {
 +              if (!(*i)->subs.empty ()) {
 +                      all.push_back (render_subtitles ((*i)->subs, _video_container_size));
                }
 -
 -              shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->channels(), audio->frames() - frames));
 -              trimmed->copy_from (audio.get(), audio->frames() - frames, frames, 0);
 -
 -              audio = trimmed;
 -              time = 0;
        }
  
 -      _audio_merger.push (audio, time);
 -      piece->audio_position += _film->audio_frames_to_time (audio->frames ());
 +      return all;
  }
  
  void
 -Player::flush ()
 +Player::set_approximate_size ()
  {
 -      TimedAudioBuffers<Time> tb = _audio_merger.flush ();
 -      if (_audio && tb.audio) {
 -              Audio (tb.audio, tb.time);
 -              _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
 -      }
 +      _approximate_size = true;
 +}
  
 -      while (_video && _video_position < _audio_position) {
 -              emit_black ();
 -      }
 +shared_ptr<PlayerVideoFrame>
 +Player::black_player_video_frame () const
 +{
 +      return shared_ptr<PlayerVideoFrame> (
 +              new PlayerVideoFrame (
-                       shared_ptr<const ImageProxy> (new RawImageProxy (_black_image)),
++                      shared_ptr<const ImageProxy> (new RawImageProxy (_black_image, _film->log ())),
 +                      Crop (),
 +                      _video_container_size,
 +                      _video_container_size,
 +                      Scaler::from_id ("bicubic"),
 +                      EYES_BOTH,
 +                      PART_WHOLE,
 +                      Config::instance()->colour_conversions().front().conversion
 +              )
 +      );
 +}
  
 -      while (_audio && _audio_position < _video_position) {
 -              emit_silence (_film->time_to_audio_frames (_video_position - _audio_position));
 +shared_ptr<PlayerVideoFrame>
 +Player::content_to_player_video_frame (
 +      shared_ptr<VideoContent> content,
 +      ContentVideo content_video,
 +      list<shared_ptr<Piece> > subs,
 +      DCPTime time,
 +      dcp::Size image_size) const
 +{
 +      shared_ptr<PlayerVideoFrame> pvf (
 +              new PlayerVideoFrame (
 +                      content_video.image,
 +                      content->crop (),
 +                      image_size,
 +                      _video_container_size,
 +                      _film->scaler(),
 +                      content_video.eyes,
 +                      content_video.part,
 +                      content->colour_conversion ()
 +                      )
 +              );
 +      
 +      
 +      /* Add subtitles */
 +      
 +      list<PositionImage> sub_images;
 +      
 +      for (list<shared_ptr<Piece> >::const_iterator i = subs.begin(); i != subs.end(); ++i) {
 +              shared_ptr<SubtitleDecoder> subtitle_decoder = dynamic_pointer_cast<SubtitleDecoder> ((*i)->decoder);
 +              shared_ptr<SubtitleContent> subtitle_content = dynamic_pointer_cast<SubtitleContent> ((*i)->content);
 +              ContentTime const from = dcp_to_content_subtitle (*i, time);
 +              ContentTime const to = from + ContentTime::from_frames (1, content->video_frame_rate ());
 +              
 +              list<shared_ptr<ContentImageSubtitle> > image_subtitles = subtitle_decoder->get_image_subtitles (ContentTimePeriod (from, to));
 +              if (!image_subtitles.empty ()) {
 +                      list<PositionImage> im = process_content_image_subtitles (
 +                              subtitle_content,
 +                              image_subtitles
 +                              );
 +                      
 +                      copy (im.begin(), im.end(), back_inserter (sub_images));
 +              }
 +              
 +              if (_burn_subtitles) {
 +                      list<shared_ptr<ContentTextSubtitle> > text_subtitles = subtitle_decoder->get_text_subtitles (ContentTimePeriod (from, to));
 +                      if (!text_subtitles.empty ()) {
 +                              list<PositionImage> im = process_content_text_subtitles (text_subtitles);
 +                              copy (im.begin(), im.end(), back_inserter (sub_images));
 +                      }
 +              }
        }
        
 +      if (!sub_images.empty ()) {
 +              pvf->set_subtitle (merge (sub_images));
 +      }
 +
 +      return pvf;
  }
  
 -/** Seek so that the next pass() will yield (approximately) the requested frame.
 - *  Pass accurate = true to try harder to get close to the request.
 - *  @return true on error
 - */
 -void
 -Player::seek (Time t, bool accurate)
 +/** @return All PlayerVideoFrames at the given time (there may be two frames for 3D) */
 +list<shared_ptr<PlayerVideoFrame> >
 +Player::get_video (DCPTime time, bool accurate)
  {
        if (!_have_valid_pieces) {
                setup_pieces ();
        }
 +      
 +      list<shared_ptr<Piece> > ov = overlaps<VideoContent> (
 +              time,
 +              time + DCPTime::from_frames (1, _film->video_frame_rate ())
 +              );
  
 -      if (_pieces.empty ()) {
 -              return;
 +      list<shared_ptr<PlayerVideoFrame> > pvf;
 +              
 +      if (ov.empty ()) {
 +              /* No video content at this time */
 +              pvf.push_back (black_player_video_frame ());
 +              return pvf;
        }
  
 -      for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -              shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> ((*i)->content);
 -              if (!vc) {
 -                      continue;
 -              }
 -
 -              /* s is the offset of t from the start position of this content */
 -              Time s = t - vc->position ();
 -              s = max (static_cast<Time> (0), s);
 -              s = min (vc->length_after_trim(), s);
 +      /* Create a PlayerVideoFrame from the content's video at this time */
  
 -              /* Hence set the piece positions to the `global' time */
 -              (*i)->video_position = (*i)->audio_position = vc->position() + s;
 -
 -              /* And seek the decoder */
 -              dynamic_pointer_cast<VideoDecoder>((*i)->decoder)->seek (
 -                      vc->time_to_content_video_frames (s + vc->trim_start ()), accurate
 -                      );
 +      shared_ptr<Piece> piece = ov.back ();
 +      shared_ptr<VideoDecoder> decoder = dynamic_pointer_cast<VideoDecoder> (piece->decoder);
 +      assert (decoder);
 +      shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
 +      assert (content);
  
 -              (*i)->reset_repeat ();
 +      list<ContentVideo> content_video = decoder->get_video (dcp_to_content_video (piece, time), accurate);
 +      if (content_video.empty ()) {
 +              pvf.push_back (black_player_video_frame ());
 +              return pvf;
        }
  
 -      _video_position = _audio_position = t;
 +      dcp::Size image_size = content->scale().size (content, _video_container_size, _film->frame_size ());
 +      if (_approximate_size) {
 +              image_size.width &= ~3;
 +              image_size.height &= ~3;
 +      }
  
 -      /* XXX: don't seek audio because we don't need to... */
 +      for (list<ContentVideo>::const_iterator i = content_video.begin(); i != content_video.end(); ++i) {
 +              list<shared_ptr<Piece> > subs = overlaps<SubtitleContent> (
 +                      time,
 +                      time + DCPTime::from_frames (1, _film->video_frame_rate ())
 +                      );
 +              
 +              pvf.push_back (content_to_player_video_frame (content, *i, subs, time, image_size));
 +      }
 +              
 +      return pvf;
  }
  
 -void
 -Player::setup_pieces ()
 +shared_ptr<AudioBuffers>
 +Player::get_audio (DCPTime time, DCPTime length, bool accurate)
  {
 -      list<shared_ptr<Piece> > old_pieces = _pieces;
 +      if (!_have_valid_pieces) {
 +              setup_pieces ();
 +      }
  
 -      _pieces.clear ();
 +      AudioFrame const length_frames = length.frames (_film->audio_frame_rate ());
  
 -      ContentList content = _playlist->content ();
 -      sort (content.begin(), content.end(), ContentSorter ());
 +      shared_ptr<AudioBuffers> audio (new AudioBuffers (_film->audio_channels(), length_frames));
 +      audio->make_silent ();
 +      
 +      list<shared_ptr<Piece> > ov = overlaps<AudioContent> (time, time + length);
 +      if (ov.empty ()) {
 +              return audio;
 +      }
  
 -      for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
 +      for (list<shared_ptr<Piece> >::iterator i = ov.begin(); i != ov.end(); ++i) {
  
 -              if (!(*i)->paths_valid ()) {
 +              shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> ((*i)->content);
 +              assert (content);
 +              shared_ptr<AudioDecoder> decoder = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 +              assert (decoder);
 +
 +              if (content->audio_frame_rate() == 0) {
 +                      /* This AudioContent has no audio (e.g. if it is an FFmpegContent with no
 +                       * audio stream).
 +                       */
                        continue;
                }
  
 -              shared_ptr<Piece> piece (new Piece (*i));
 -
 -              /* XXX: into content? */
 -
 -              shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i);
 -              if (fc) {
 -                      shared_ptr<FFmpegDecoder> fd (new FFmpegDecoder (_film, fc, _video, _audio));
 -                      
 -                      fd->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, _5, 0));
 -                      fd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2));
 -                      fd->Subtitle.connect (bind (&Player::process_subtitle, this, weak_ptr<Piece> (piece), _1, _2, _3, _4));
 -
 -                      fd->seek (fc->time_to_content_video_frames (fc->trim_start ()), true);
 -                      piece->decoder = fd;
 +              /* The time that we should request from the content */
 +              DCPTime request = time - DCPTime::from_seconds (content->audio_delay() / 1000.0);
 +              DCPTime offset;
 +              if (request < DCPTime ()) {
 +                      /* We went off the start of the content, so we will need to offset
 +                         the stuff we get back.
 +                      */
 +                      offset = -request;
 +                      request = DCPTime ();
                }
 -              
 -              shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i);
 -              if (ic) {
 -                      bool reusing = false;
 -                      
 -                      /* See if we can re-use an old ImageDecoder */
 -                      for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) {
 -                              shared_ptr<ImageDecoder> imd = dynamic_pointer_cast<ImageDecoder> ((*j)->decoder);
 -                              if (imd && imd->content() == ic) {
 -                                      piece = *j;
 -                                      reusing = true;
 -                              }
 -                      }
  
 -                      if (!reusing) {
 -                              shared_ptr<ImageDecoder> id (new ImageDecoder (_film, ic));
 -                              id->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, _5, 0));
 -                              piece->decoder = id;
 -                      }
 -              }
 +              AudioFrame const content_frame = dcp_to_content_audio (*i, request);
  
 -              shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i);
 -              if (sc) {
 -                      shared_ptr<AudioDecoder> sd (new SndfileDecoder (_film, sc));
 -                      sd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2));
 +              /* Audio from this piece's decoder (which might be more or less than what we asked for) */
 +              shared_ptr<ContentAudio> all = decoder->get_audio (content_frame, length_frames, accurate);
  
 -                      piece->decoder = sd;
 +              /* Gain */
 +              if (content->audio_gain() != 0) {
 +                      shared_ptr<AudioBuffers> gain (new AudioBuffers (all->audio));
 +                      gain->apply_gain (content->audio_gain ());
 +                      all->audio = gain;
                }
  
 -              _pieces.push_back (piece);
 -      }
 -
 -      _have_valid_pieces = true;
 -}
 -
 -void
 -Player::content_changed (weak_ptr<Content> w, int property, bool frequent)
 -{
 -      shared_ptr<Content> c = w.lock ();
 -      if (!c) {
 -              return;
 -      }
 -
 -      if (
 -              property == ContentProperty::POSITION || property == ContentProperty::LENGTH ||
 -              property == ContentProperty::TRIM_START || property == ContentProperty::TRIM_END ||
 -              property == VideoContentProperty::VIDEO_FRAME_TYPE 
 -              ) {
 -              
 -              _have_valid_pieces = false;
 -              Changed (frequent);
 -
 -      } else if (
 -              property == SubtitleContentProperty::SUBTITLE_X_OFFSET ||
 -              property == SubtitleContentProperty::SUBTITLE_Y_OFFSET ||
 -              property == SubtitleContentProperty::SUBTITLE_SCALE
 -              ) {
 -
 -              for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
 -                      i->update (_film, _video_container_size);
 +              /* Remap channels */
 +              shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), all->audio->frames()));
 +              dcp_mapped->make_silent ();
 +              AudioMapping map = content->audio_mapping ();
 +              for (int i = 0; i < map.content_channels(); ++i) {
 +                      for (int j = 0; j < _film->audio_channels(); ++j) {
 +                              if (map.get (i, static_cast<dcp::Channel> (j)) > 0) {
 +                                      dcp_mapped->accumulate_channel (
 +                                              all->audio.get(),
 +                                              i,
 +                                              j,
 +                                              map.get (i, static_cast<dcp::Channel> (j))
 +                                              );
 +                              }
 +                      }
                }
                
 -              Changed (frequent);
 +              all->audio = dcp_mapped;
  
 -      } else if (
 -              property == VideoContentProperty::VIDEO_CROP || property == VideoContentProperty::VIDEO_SCALE ||
 -              property == VideoContentProperty::VIDEO_FRAME_RATE
 -              ) {
 -              
 -              Changed (frequent);
 -
 -      } else if (property == ContentProperty::PATH) {
 -
 -              _have_valid_pieces = false;
 -              Changed (frequent);
 +              audio->accumulate_frames (
 +                      all->audio.get(),
 +                      content_frame - all->frame,
 +                      offset.frames (_film->audio_frame_rate()),
 +                      min (AudioFrame (all->audio->frames()), length_frames) - offset.frames (_film->audio_frame_rate ())
 +                      );
        }
-       return audio;
  }
  
 -void
 -Player::playlist_changed ()
 -{
 -      _have_valid_pieces = false;
 -      Changed (false);
 -}
 -
 -void
 -Player::set_video_container_size (libdcp::Size s)
 -{
 -      _video_container_size = s;
 -
 -      shared_ptr<Image> im (new Image (PIX_FMT_RGB24, _video_container_size, true));
 -      im->make_black ();
 -      
 -      _black_frame.reset (
 -              new PlayerVideoFrame (
 -                      shared_ptr<ImageProxy> (new RawImageProxy (im, _film->log ())),
 -                      Crop(),
 -                      _video_container_size,
 -                      _video_container_size,
 -                      Scaler::from_id ("bicubic"),
 -                      EYES_BOTH,
 -                      PART_WHOLE,
 -                      ColourConversion ()
 -                      )
 -              );
 -}
 -
 -shared_ptr<Resampler>
 -Player::resampler (shared_ptr<AudioContent> c, bool create)
 +VideoFrame
 +Player::dcp_to_content_video (shared_ptr<const Piece> piece, DCPTime t) const
  {
 -      map<shared_ptr<AudioContent>, shared_ptr<Resampler> >::iterator i = _resamplers.find (c);
 -      if (i != _resamplers.end ()) {
 -              return i->second;
 -      }
 +      /* s is the offset of t from the start position of this content */
 +      DCPTime s = t - piece->content->position ();
 +      s = DCPTime (max (int64_t (0), s.get ()));
 +      s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
  
 -      if (!create) {
 -              return shared_ptr<Resampler> ();
 -      }
 -
 -      LOG_GENERAL (
 -              "Creating new resampler for %1 to %2 with %3 channels", c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()
 -              );
 -      
 -      shared_ptr<Resampler> r (new Resampler (c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()));
 -      _resamplers[c] = r;
 -      return r;
 +      /* Convert this to the content frame */
 +      return DCPTime (s + piece->content->trim_start()).frames (_film->video_frame_rate()) * piece->frc.factor ();
  }
  
 -void
 -Player::emit_black ()
 +AudioFrame
 +Player::dcp_to_content_audio (shared_ptr<const Piece> piece, DCPTime t) const
  {
 -#ifdef DCPOMATIC_DEBUG
 -      _last_video.reset ();
 -#endif
 +      /* s is the offset of t from the start position of this content */
 +      DCPTime s = t - piece->content->position ();
 +      s = DCPTime (max (int64_t (0), s.get ()));
 +      s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
  
 -      Video (_black_frame, _last_emit_was_black, _video_position);
 -      _video_position += _film->video_frames_to_time (1);
 -      _last_emit_was_black = true;
 +      /* Convert this to the content frame */
 +      return DCPTime (s + piece->content->trim_start()).frames (_film->audio_frame_rate());
  }
  
 -void
 -Player::emit_silence (OutputAudioFrame most)
 +ContentTime
 +Player::dcp_to_content_subtitle (shared_ptr<const Piece> piece, DCPTime t) const
  {
 -      if (most == 0) {
 -              return;
 -      }
 -      
 -      OutputAudioFrame N = min (most, _film->audio_frame_rate() / 2);
 -      shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), N));
 -      silence->make_silent ();
 -      Audio (silence, _audio_position);
 -      _audio_position += _film->audio_frames_to_time (N);
 -}
 -
 -void
 -Player::film_changed (Film::Property p)
 -{
 -      /* Here we should notice Film properties that affect our output, and
 -         alert listeners that our output now would be different to how it was
 -         last time we were run.
 -      */
 +      /* s is the offset of t from the start position of this content */
 +      DCPTime s = t - piece->content->position ();
 +      s = DCPTime (max (int64_t (0), s.get ()));
 +      s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
  
 -      if (p == Film::SCALER || p == Film::WITH_SUBTITLES || p == Film::CONTAINER || p == Film::VIDEO_FRAME_RATE) {
 -              Changed (false);
 -      }
 +      return ContentTime (s + piece->content->trim_start(), piece->frc);
  }
  
  void
 -Player::process_subtitle (weak_ptr<Piece> weak_piece, shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to)
 +PlayerStatistics::dump (shared_ptr<Log> log) const
  {
-       log->log (String::compose ("Video: %1 good %2 skipped %3 black %4 repeat", video.good, video.skip, video.black, video.repeat));
-       log->log (String::compose ("Audio: %1 good %2 skipped %3 silence", audio.good, audio.skip, audio.silence.seconds()));
 -      if (!image) {
 -              /* A null image means that we should stop any current subtitles at `from' */
 -              for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
 -                      i->set_stop (from);
 -              }
 -      } else {
 -              _subtitles.push_back (Subtitle (_film, _video_container_size, weak_piece, image, rect, from, to));
 -      }
++      log->log (String::compose ("Video: %1 good %2 skipped %3 black %4 repeat", video.good, video.skip, video.black, video.repeat), Log::TYPE_GENERAL);
++      log->log (String::compose ("Audio: %1 good %2 skipped %3 silence", audio.good, audio.skip, audio.silence.seconds()), Log::TYPE_GENERAL);
  }
  
 -/** Re-emit the last frame that was emitted, using current settings for crop, ratio, scaler and subtitles.
 - *  @return false if this could not be done.
 - */
 -bool
 -Player::repeat_last_video ()
 +PlayerStatistics const &
 +Player::statistics () const
  {
 -      if (!_last_incoming_video.image || !_have_valid_pieces) {
 -              return false;
 -      }
 -
 -      process_video (
 -              _last_incoming_video.weak_piece,
 -              _last_incoming_video.image,
 -              _last_incoming_video.eyes,
 -              _last_incoming_video.part,
 -              _last_incoming_video.same,
 -              _last_incoming_video.frame,
 -              _last_incoming_video.extra
 -              );
 -
 -      return true;
 +      return _statistics;
  }
index 2544f8bbcb0f3c7f330d7c6cc1073bd01a427ba8,94760e495ce95b5ce0c284b5c5d2f8a4492e24c5..5463925bf89e015bf31b58ebbccccad54ac19b5b
@@@ -17,7 -17,7 +17,7 @@@
  
  */
  
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "player_video_frame.h"
  #include "image.h"
  #include "image_proxy.h"
  using std::string;
  using std::cout;
  using boost::shared_ptr;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  PlayerVideoFrame::PlayerVideoFrame (
        shared_ptr<const ImageProxy> in,
        Crop crop,
 -      libdcp::Size inter_size,
 -      libdcp::Size out_size,
 +      dcp::Size inter_size,
 +      dcp::Size out_size,
        Scaler const * scaler,
        Eyes eyes,
        Part part,
  
  }
  
- PlayerVideoFrame::PlayerVideoFrame (shared_ptr<cxml::Node> node, shared_ptr<Socket> socket)
+ PlayerVideoFrame::PlayerVideoFrame (shared_ptr<cxml::Node> node, shared_ptr<Socket> socket, shared_ptr<Log> log)
  {
        _crop = Crop (node);
  
 -      _inter_size = libdcp::Size (node->number_child<int> ("InterWidth"), node->number_child<int> ("InterHeight"));
 -      _out_size = libdcp::Size (node->number_child<int> ("OutWidth"), node->number_child<int> ("OutHeight"));
 +      _inter_size = dcp::Size (node->number_child<int> ("InterWidth"), node->number_child<int> ("InterHeight"));
 +      _out_size = dcp::Size (node->number_child<int> ("OutWidth"), node->number_child<int> ("OutHeight"));
        _scaler = Scaler::from_id (node->string_child ("Scaler"));
        _eyes = (Eyes) node->number_child<int> ("Eyes");
        _part = (Part) node->number_child<int> ("Part");
        _colour_conversion = ColourConversion (node);
  
-       _in = image_proxy_factory (node->node_child ("In"), socket);
+       _in = image_proxy_factory (node->node_child ("In"), socket, log);
  
        if (node->optional_number_child<int> ("SubtitleX")) {
                
 -              _subtitle_position = Position<int> (node->number_child<int> ("SubtitleX"), node->number_child<int> ("SubtitleY"));
 +              _subtitle.position = Position<int> (node->number_child<int> ("SubtitleX"), node->number_child<int> ("SubtitleY"));
  
 -              shared_ptr<Image> image (
 -                      new Image (PIX_FMT_RGBA, libdcp::Size (node->number_child<int> ("SubtitleWidth"), node->number_child<int> ("SubtitleHeight")), true)
 +              _subtitle.image.reset (
 +                      new Image (PIX_FMT_RGBA, dcp::Size (node->number_child<int> ("SubtitleWidth"), node->number_child<int> ("SubtitleHeight")), true)
                        );
                
 -              image->read_from_socket (socket);
 -              _subtitle_image = image;
 +              _subtitle.image->read_from_socket (socket);
        }
  }
  
  void
 -PlayerVideoFrame::set_subtitle (shared_ptr<const Image> image, Position<int> pos)
 +PlayerVideoFrame::set_subtitle (PositionImage image)
  {
 -      _subtitle_image = image;
 -      _subtitle_position = pos;
 +      _subtitle = image;
  }
  
  shared_ptr<Image>
@@@ -104,12 -106,12 +104,12 @@@ PlayerVideoFrame::image () cons
                break;
        }
                
 -      shared_ptr<Image> out = im->crop_scale_window (total_crop, _inter_size, _out_size, _scaler, PIX_FMT_RGB24, false);
 +      shared_ptr<Image> out = im->crop_scale_window (total_crop, _inter_size, _out_size, _scaler, PIX_FMT_RGB24, true);
  
        Position<int> const container_offset ((_out_size.width - _inter_size.width) / 2, (_out_size.height - _inter_size.width) / 2);
  
 -      if (_subtitle_image) {
 -              out->alpha_blend (_subtitle_image, _subtitle_position);
 +      if (_subtitle.image) {
 +              out->alpha_blend (_subtitle.image, _subtitle.position);
        }
  
        return out;
@@@ -128,11 -130,11 +128,11 @@@ PlayerVideoFrame::add_metadata (xmlpp::
        node->add_child("Eyes")->add_child_text (raw_convert<string> (_eyes));
        node->add_child("Part")->add_child_text (raw_convert<string> (_part));
        _colour_conversion.as_xml (node);
 -      if (_subtitle_image) {
 -              node->add_child ("SubtitleWidth")->add_child_text (raw_convert<string> (_subtitle_image->size().width));
 -              node->add_child ("SubtitleHeight")->add_child_text (raw_convert<string> (_subtitle_image->size().height));
 -              node->add_child ("SubtitleX")->add_child_text (raw_convert<string> (_subtitle_position.x));
 -              node->add_child ("SubtitleY")->add_child_text (raw_convert<string> (_subtitle_position.y));
 +      if (_subtitle.image) {
 +              node->add_child ("SubtitleWidth")->add_child_text (raw_convert<string> (_subtitle.image->size().width));
 +              node->add_child ("SubtitleHeight")->add_child_text (raw_convert<string> (_subtitle.image->size().height));
 +              node->add_child ("SubtitleX")->add_child_text (raw_convert<string> (_subtitle.position.x));
 +              node->add_child ("SubtitleY")->add_child_text (raw_convert<string> (_subtitle.position.y));
        }
  }
  
@@@ -140,7 -142,7 +140,7 @@@ voi
  PlayerVideoFrame::send_binary (shared_ptr<Socket> socket) const
  {
        _in->send_binary (socket);
 -      if (_subtitle_image) {
 -              _subtitle_image->write_to_socket (socket);
 +      if (_subtitle.image) {
 +              _subtitle.image->write_to_socket (socket);
        }
  }
index 225b0a4bafe43c8dee4be008b8e5bdb5b519fb1b,b085cb609e659f59b55f6a709c88b046c2a6a078..4c6a9c63008cd6b30f2898b27e8b9e8974dddd8a
  #include "types.h"
  #include "position.h"
  #include "colour_conversion.h"
 +#include "position_image.h"
  
  class Image;
  class ImageProxy;
  class Scaler;
  class Socket;
+ class Log;
  
  /** Everything needed to describe a video frame coming out of the player, but with the
   *  bits still their raw form.  We may want to combine the bits on a remote machine,
  class PlayerVideoFrame
  {
  public:
 -      PlayerVideoFrame (boost::shared_ptr<const ImageProxy>, Crop, libdcp::Size, libdcp::Size, Scaler const *, Eyes, Part, ColourConversion);
 +      PlayerVideoFrame (boost::shared_ptr<const ImageProxy>, Crop, dcp::Size, dcp::Size, Scaler const *, Eyes, Part, ColourConversion);
-       PlayerVideoFrame (boost::shared_ptr<cxml::Node>, boost::shared_ptr<Socket>);
+       PlayerVideoFrame (boost::shared_ptr<cxml::Node>, boost::shared_ptr<Socket>, boost::shared_ptr<Log>);
  
 -      void set_subtitle (boost::shared_ptr<const Image>, Position<int>);
 +      void set_subtitle (PositionImage);
        
        boost::shared_ptr<Image> image () const;
  
  private:
        boost::shared_ptr<const ImageProxy> _in;
        Crop _crop;
 -      libdcp::Size _inter_size;
 -      libdcp::Size _out_size;
 +      dcp::Size _inter_size;
 +      dcp::Size _out_size;
        Scaler const * _scaler;
        Eyes _eyes;
        Part _part;
        ColourConversion _colour_conversion;
 -      boost::shared_ptr<const Image> _subtitle_image;
 -      Position<int> _subtitle_position;
 +      PositionImage _subtitle;
  };
diff --combined src/lib/server.cc
index d72b7e5025b84e57513a97864014137d5658eacd,6b4064cd7ebae77fdca24040228c7a5169cf9505..507ff2ae593ae2bfa5c15e818d16d561281e83de
@@@ -29,7 -29,7 +29,7 @@@
  #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 "i18n.h"
  
+ #define LOG_GENERAL(...)    _log->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
+ #define LOG_GENERAL_NC(...) _log->log (__VA_ARGS__, Log::TYPE_GENERAL);
+ #define LOG_ERROR(...)      _log->log (String::compose (__VA_ARGS__), Log::TYPE_ERROR);
+ #define LOG_ERROR_NC(...)   _log->log (__VA_ARGS__, Log::TYPE_ERROR);
  using std::string;
  using std::stringstream;
  using std::multimap;
@@@ -57,8 -62,8 +62,8 @@@ 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)
@@@ -82,11 -87,11 +87,11 @@@ Server::process (shared_ptr<Socket> soc
        xml->read_stream (s);
        if (xml->number_child<int> ("Version") != SERVER_LINK_VERSION) {
                cerr << "Mismatched server/client versions\n";
-               _log->log ("Mismatched server/client versions");
+               LOG_ERROR_NC ("Mismatched server/client versions");
                return -1;
        }
  
-       shared_ptr<PlayerVideoFrame> pvf (new PlayerVideoFrame (xml, socket));
+       shared_ptr<PlayerVideoFrame> pvf (new PlayerVideoFrame (xml, socket, _log));
  
        DCPVideoFrame dcp_video_frame (pvf, xml, _log);
  
        try {
                encoded->send (socket);
        } catch (std::exception& e) {
-               _log->log (String::compose ("Send failed; frame %1", dcp_video_frame.index()));
+               LOG_ERROR ("Send failed; frame %1", dcp_video_frame.index());
                throw;
        }
  
@@@ -134,7 -139,7 +139,7 @@@ Server::worker_thread (
                        frame = process (socket, after_read, after_encode);
                        ip = socket->socket().remote_endpoint().address().to_string();
                } catch (std::exception& e) {
-                       _log->log (String::compose ("Error: %1", e.what()));
+                       LOG_ERROR ("Error: %1", e.what());
                }
  
                gettimeofday (&end, 0);
                                cout << message.str() << "\n";
                        }
  
-                       _log->log (message.str ());
+                       LOG_GENERAL_NC (message.str ());
                }
                
                _worker_condition.notify_all ();
  void
  Server::run (int num_threads)
  {
-       _log->log (String::compose ("Server starting with %1 threads", num_threads));
+       LOG_GENERAL ("Server starting with %1 threads", num_threads);
        if (_verbose) {
                cout << "DCP-o-matic server starting with " << num_threads << " threads.\n";
        }
diff --combined src/lib/transcode_job.cc
index 97e8bd416a0432697b14dcb2503d6f82d62ba2e0,ef15f9f5e9865968ecee6f6b87e0437e672d1e66..4a85fa18aa8935d0a2c0ff5670f504de012b439e
  
  #include "i18n.h"
  
+ #define LOG_GENERAL_NC(...) _film->log()->log (__VA_ARGS__, Log::TYPE_GENERAL);
+ #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.
@@@ -62,17 -64,20 +65,20 @@@ TranscodeJob::run (
  {
        try {
  
-               _film->log()->log (N_("Transcode job starting"));
+               LOG_GENERAL_NC (N_("Transcode job starting"));
  
                _transcoder.reset (new Transcoder (_film, shared_from_this ()));
                _transcoder->go ();
                set_progress (1);
                set_state (FINISHED_OK);
  
-               _film->log()->log (N_("Transcode job completed successfully"));
+               LOG_GENERAL_NC (N_("Transcode job completed successfully"));
                _transcoder.reset ();
  
        } catch (...) {
+               set_progress (1);
+               set_state (FINISHED_ERROR);
+               LOG_ERROR_NC (N_("Transcode job failed or cancelled"));
                _transcoder.reset ();
                throw;
        }
@@@ -101,7 -106,6 +107,7 @@@ TranscodeJob::status () cons
        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/types.h
index e858d1e1feb6b7827ce09d79822e5817b4fa3df0,4eb3d927e5090c8af08ef0c3cdc1167e9fe88929..9a6a30b861d45344325507186f5c08b2b200ea42
@@@ -23,9 -23,7 +23,9 @@@
  #include <vector>
  #include <stdint.h>
  #include <boost/shared_ptr.hpp>
 -#include <libdcp/util.h>
 +#include <dcp/util.h>
 +#include "dcpomatic_time.h"
 +#include "position.h"
  
  class Content;
  class VideoContent;
@@@ -48,29 -46,31 +48,29 @@@ namespace xmlpp 
   */
  #define SERVER_LINK_VERSION 2
  
 -typedef int64_t Time;
 -#define TIME_MAX INT64_MAX
 -#define TIME_HZ        ((Time) 96000)
 -typedef int64_t OutputAudioFrame;
 -typedef int   OutputVideoFrame;
  typedef std::vector<boost::shared_ptr<Content> > ContentList;
  typedef std::vector<boost::shared_ptr<VideoContent> > VideoContentList;
  typedef std::vector<boost::shared_ptr<AudioContent> > AudioContentList;
  typedef std::vector<boost::shared_ptr<SubtitleContent> > SubtitleContentList;
  typedef std::vector<boost::shared_ptr<FFmpegContent> > FFmpegContentList;
  
 -template<class T>
 +typedef int64_t VideoFrame;
 +typedef int64_t AudioFrame;
 +
 +/* XXX -> DCPAudio */
  struct TimedAudioBuffers
  {
        TimedAudioBuffers ()
                : time (0)
        {}
        
 -      TimedAudioBuffers (boost::shared_ptr<AudioBuffers> a, T t)
 +      TimedAudioBuffers (boost::shared_ptr<AudioBuffers> a, DCPTime t)
                : audio (a)
                , time (t)
        {}
        
        boost::shared_ptr<AudioBuffers> audio;
 -      T time;
 +      DCPTime time;
  };
  
  enum VideoFrameType
@@@ -120,7 -120,7 +120,7 @@@ struct Cro
        /** Number of pixels to remove from the bottom */
        int bottom;
  
 -      libdcp::Size apply (libdcp::Size s, int minimum = 4) const {
 +      dcp::Size apply (dcp::Size s, int minimum = 4) const {
                s.width -= left + right;
                s.height -= top + bottom;
  
        void as_xml (xmlpp::Node *) const;
  };
  
+ struct CPLSummary
+ {
+       CPLSummary (std::string d, std::string i, std::string a, boost::filesystem::path f)
+               : dcp_directory (d)
+               , cpl_id (i)
+               , cpl_annotation_text (a)
+               , cpl_file (f)
+       {}
+       
+       std::string dcp_directory;
+       std::string cpl_id;
+       std::string cpl_annotation_text;
+       boost::filesystem::path cpl_file;
+ };
  extern bool operator== (Crop const & a, Crop const & b);
  extern bool operator!= (Crop const & a, Crop const & b);
  
diff --combined src/lib/util.h
index 196a6e8f93b13e3314fca48423f1e70e5702f108,5ca9f74c88d68a878a54f5e5b75a7af0628caa22..28af8ef2f88c41bcfbdb8bad30806248ff98af35
@@@ -31,8 -31,7 +31,8 @@@
  #include <boost/asio.hpp>
  #include <boost/optional.hpp>
  #include <boost/filesystem.hpp>
 -#include <libdcp/util.h>
 +#include <dcp/util.h>
 +#include <dcp/signer.h>
  extern "C" {
  #include <libavcodec/avcodec.h>
  #include <libavfilter/avfilter.h>
  #include "types.h"
  #include "video_content.h"
  
- #ifdef DCPOMATIC_DEBUG
- #define TIMING(...) _film->log()->microsecond_log (String::compose (__VA_ARGS__), Log::TIMING);
- #else
- #define TIMING(...)
- #endif
  #undef check
  
  /** The maximum number of audio channels that we can have in a DCP */
@@@ -59,7 -52,6 +53,7 @@@ namespace libdcp 
  }
  
  class Job;
 +struct AVSubtitle;
  
  extern std::string seconds_to_hms (int);
  extern std::string seconds_to_approximate_hms (int);
@@@ -78,10 -70,44 +72,10 @@@ extern bool valid_image_file (boost::fi
  extern boost::filesystem::path mo_path ();
  #endif
  extern std::string tidy_for_filename (std::string);
 -extern boost::shared_ptr<const libdcp::Signer> make_signer ();
 -extern libdcp::Size fit_ratio_within (float ratio, libdcp::Size);
 +extern boost::shared_ptr<const dcp::Signer> make_signer ();
 +extern dcp::Size fit_ratio_within (float ratio, dcp::Size);
  extern std::string entities_to_text (std::string e);
  extern std::map<std::string, std::string> split_get_request (std::string url);
 -
 -struct FrameRateConversion
 -{
 -      FrameRateConversion (float, int);
 -
 -      /** @return factor by which to multiply a source frame rate
 -          to get the effective rate after any skip or repeat has happened.
 -      */
 -      float factor () const {
 -              if (skip) {
 -                      return 0.5;
 -              }
 -
 -              return repeat;
 -      }
 -
 -      /** true to skip every other frame */
 -      bool skip;
 -      /** number of times to use each frame (e.g. 1 is normal, 2 means repeat each frame once, and so on) */
 -      int repeat;
 -      /** true if this DCP will run its video faster or slower than the source
 -       *  without taking into account `repeat' nor `skip'.
 -       *  (e.g. change_speed will be true if
 -       *          source is 29.97fps, DCP is 30fps
 -       *          source is 14.50fps, DCP is 30fps
 -       *  but not if
 -       *          source is 15.00fps, DCP is 30fps
 -       *          source is 12.50fps, DCP is 25fps)
 -       */
 -      bool change_speed;
 -
 -      std::string description;
 -};
 -
  extern int dcp_audio_frame_rate (int);
  extern int stride_round_up (int, int const *, int);
  extern std::multimap<std::string, std::string> read_key_value (std::istream& s);
@@@ -92,7 -118,6 +86,7 @@@ extern int get_optional_int (std::multi
  extern std::string get_optional_string (std::multimap<std::string, std::string> const & kv, std::string k);
  extern void* wrapped_av_malloc (size_t);
  extern int64_t divide_with_round (int64_t a, int64_t b);
 +extern ContentTimePeriod subtitle_period (AVSubtitle const &);
  
  /** @class Socket
   *  @brief A class to wrap a boost::asio::ip::tcp::socket with some things
@@@ -135,20 -160,16 +129,20 @@@ private
  
  extern int64_t video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second);
  
 +/** @class ScopedTemporary
 + *  @brief A temporary file which is deleted when the ScopedTemporary object goes out of scope.
 + */
  class ScopedTemporary
  {
  public:
        ScopedTemporary ();
        ~ScopedTemporary ();
  
 +      /** @return temporary filename */
        boost::filesystem::path file () const {
                return _file;
        }
 -      
 +
        char const * c_str () const;
        FILE* open (char const *);
        void close ();
diff --combined src/lib/writer.cc
index 306f6d7f4968c387965014baf084e43668bee5f6,b175843ed62bb0c975543b980c1bd4dc66f83fc9..9410dd565b4a9ba963cda63af72aea763c489511
  
  #include <fstream>
  #include <cerrno>
 -#include <libdcp/mono_picture_asset.h>
 -#include <libdcp/stereo_picture_asset.h>
 -#include <libdcp/sound_asset.h>
 -#include <libdcp/reel.h>
 -#include <libdcp/dcp.h>
 -#include <libdcp/cpl.h>
 +#include <dcp/mono_picture_mxf.h>
 +#include <dcp/stereo_picture_mxf.h>
 +#include <dcp/sound_mxf.h>
 +#include <dcp/sound_mxf_writer.h>
 +#include <dcp/reel.h>
 +#include <dcp/reel_mono_picture_asset.h>
 +#include <dcp/reel_stereo_picture_asset.h>
 +#include <dcp/reel_sound_asset.h>
 +#include <dcp/dcp.h>
 +#include <dcp/cpl.h>
  #include "writer.h"
  #include "compose.hpp"
  #include "film.h"
  #include "config.h"
  #include "job.h"
  #include "cross.h"
 +#include "audio_buffers.h"
  
  #include "i18n.h"
  
+ #define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
+ #define LOG_TIMING(...) _film->log()->microsecond_log (String::compose (__VA_ARGS__), Log::TYPE_TIMING);
+ #define LOG_WARNING_NC(...) _film->log()->log (__VA_ARGS__, Log::TYPE_WARNING);
  /* OS X strikes again */
  #undef set_key
  
@@@ -56,7 -55,6 +60,7 @@@ using std::cout
  using std::stringstream;
  using boost::shared_ptr;
  using boost::weak_ptr;
 +using boost::dynamic_pointer_cast;
  
  int const Writer::_maximum_frames_in_memory = Config::instance()->num_local_encoding_threads() + 4;
  
@@@ -71,6 -69,7 +75,6 @@@ Writer::Writer (shared_ptr<const Film> 
        , _last_written_eyes (EYES_RIGHT)
        , _full_written (0)
        , _fake_written (0)
 -      , _repeat_written (0)
        , _pushed_to_disk (0)
  {
        /* Remove any old DCP */
        */
  
        if (_film->three_d ()) {
 -              _picture_asset.reset (new libdcp::StereoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ()));
 +              _picture_mxf.reset (new dcp::StereoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1)));
        } else {
 -              _picture_asset.reset (new libdcp::MonoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ()));
 +              _picture_mxf.reset (new dcp::MonoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1)));
        }
  
 -      _picture_asset->set_edit_rate (_film->video_frame_rate ());
 -      _picture_asset->set_size (_film->frame_size ());
 -      _picture_asset->set_interop (_film->interop ());
 +      _picture_mxf->set_size (_film->frame_size ());
  
        if (_film->encrypted ()) {
 -              _picture_asset->set_key (_film->key ());
 +              _picture_mxf->set_key (_film->key ());
        }
        
 -      _picture_asset_writer = _picture_asset->start_write (_first_nonexistant_frame > 0);
 +      _picture_mxf_writer = _picture_mxf->start_write (
 +              _film->internal_video_mxf_dir() / _film->internal_video_mxf_filename(),
 +              _film->interop() ? dcp::INTEROP : dcp::SMPTE,
 +              _first_nonexistant_frame > 0
 +              );
  
        if (_film->audio_channels ()) {
 -              _sound_asset.reset (new libdcp::SoundAsset (_film->directory (), _film->audio_mxf_filename ()));
 -              _sound_asset->set_edit_rate (_film->video_frame_rate ());
 -              _sound_asset->set_channels (_film->audio_channels ());
 -              _sound_asset->set_sampling_rate (_film->audio_frame_rate ());
 -              _sound_asset->set_interop (_film->interop ());
 +              _sound_mxf.reset (new dcp::SoundMXF (dcp::Fraction (_film->video_frame_rate(), 1), _film->audio_frame_rate (), _film->audio_channels ()));
  
                if (_film->encrypted ()) {
 -                      _sound_asset->set_key (_film->key ());
 +                      _sound_mxf->set_key (_film->key ());
                }
 -              
 -              /* Write the sound asset into the film directory so that we leave the creation
 +      
 +              /* Write the sound MXF into the film directory so that we leave the creation
                   of the DCP directory until the last minute.
                */
 -              _sound_asset_writer = _sound_asset->start_write ();
 +              _sound_mxf_writer = _sound_mxf->start_write (_film->directory() / _film->audio_mxf_filename(), _film->interop() ? dcp::INTEROP : dcp::SMPTE);
        }
  
        _thread = new boost::thread (boost::bind (&Writer::thread, this));
@@@ -169,7 -170,7 +173,7 @@@ Writer::fake_write (int frame, Eyes eye
        }
        
        FILE* ifi = fopen_boost (_film->info_path (frame, eyes), "r");
 -      libdcp::FrameInfo info (ifi);
 +      dcp::FrameInfo info (ifi);
        fclose (ifi);
        
        QueueItem qi;
  void
  Writer::write (shared_ptr<const AudioBuffers> audio)
  {
 -      if (_sound_asset) {
 -              _sound_asset_writer->write (audio->data(), audio->frames());
 +      if (_sound_mxf_writer) {
 +              _sound_mxf_writer->write (audio->data(), audio->frames());
        }
  }
  
@@@ -242,9 -243,9 +246,9 @@@ tr
                                break;
                        }
  
-                       TIMING (N_("writer sleeps with a queue of %1"), _queue.size());
+                       LOG_TIMING (N_("writer sleeps with a queue of %1"), _queue.size());
                        _empty_condition.wait (lock);
-                       TIMING (N_("writer wakes with a queue of %1"), _queue.size());
+                       LOG_TIMING (N_("writer wakes with a queue of %1"), _queue.size());
                }
  
                if (_finish && _queue.empty()) {
                        switch (qi.type) {
                        case QueueItem::FULL:
                        {
-                               _film->log()->log (String::compose (N_("Writer FULL-writes %1 to MXF"), qi.frame));
+                               LOG_GENERAL (N_("Writer FULL-writes %1 to MXF"), qi.frame);
                                if (!qi.encoded) {
                                        qi.encoded.reset (new EncodedData (_film->j2c_path (qi.frame, qi.eyes, false)));
                                }
  
 -                              libdcp::FrameInfo fin = _picture_asset_writer->write (qi.encoded->data(), qi.encoded->size());
 +                              dcp::FrameInfo fin = _picture_mxf_writer->write (qi.encoded->data(), qi.encoded->size());
                                qi.encoded->write_info (_film, qi.frame, qi.eyes, fin);
                                _last_written[qi.eyes] = qi.encoded;
                                ++_full_written;
                                break;
                        }
                        case QueueItem::FAKE:
-                               _film->log()->log (String::compose (N_("Writer FAKE-writes %1 to MXF"), qi.frame));
+                               LOG_GENERAL (N_("Writer FAKE-writes %1 to MXF"), qi.frame);
 -                              _picture_asset_writer->fake_write (qi.size);
 +                              _picture_mxf_writer->fake_write (qi.size);
                                _last_written[qi.eyes].reset ();
                                ++_fake_written;
                                break;
 -                      case QueueItem::REPEAT:
 -                      {
 -                              LOG_GENERAL (N_("Writer REPEAT-writes %1 to MXF"), qi.frame);
 -                              libdcp::FrameInfo fin = _picture_asset_writer->write (
 -                                      _last_written[qi.eyes]->data(),
 -                                      _last_written[qi.eyes]->size()
 -                                      );
 -                              
 -                              _last_written[qi.eyes]->write_info (_film, qi.frame, qi.eyes, fin);
 -                              ++_repeat_written;
 -                              break;
 -                      }
                        }
                        lock.lock ();
  
                        _last_written_frame = qi.frame;
                        _last_written_eyes = qi.eyes;
                        
 -                      if (_film->length()) {
 -                              shared_ptr<Job> job = _job.lock ();
 -                              assert (job);
 -                              int total = _film->time_to_video_frames (_film->length ());
 -                              if (_film->three_d ()) {
 -                                      /* _full_written and so on are incremented for each eye, so we need to double the total
 -                                         frames to get the correct progress.
 -                                      */
 -                                      total *= 2;
 -                              }
 -                              job->set_progress (float (_full_written + _fake_written + _repeat_written) / total);
 +                      shared_ptr<Job> job = _job.lock ();
 +                      assert (job);
 +                      int64_t total = _film->length().frames (_film->video_frame_rate ());
 +                      if (_film->three_d ()) {
 +                              /* _full_written and so on are incremented for each eye, so we need to double the total
 +                                 frames to get the correct progress.
 +                              */
 +                              total *= 2;
 +                      }
 +                      if (total) {
 +                              job->set_progress (float (_full_written + _fake_written) / total);
                        }
                }
  
                        
                        lock.unlock ();
  
-                       _film->log()->log (
-                               String::compose (
-                                       "Writer full (awaiting %1 [last eye was %2]); pushes %3 to disk",
-                                       _last_written_frame + 1,
-                                       _last_written_eyes, qi.frame)
+                       LOG_GENERAL (
+                               "Writer full (awaiting %1 [last eye was %2]); pushes %3 to disk",
+                               _last_written_frame + 1,
+                               _last_written_eyes, qi.frame
                                );
                        
                        qi.encoded->write (_film, qi.frame, qi.eyes);
@@@ -371,11 -383,15 +374,11 @@@ Writer::finish (
        
        terminate_thread (true);
  
 -      _picture_asset_writer->finalize ();
 -      if (_sound_asset_writer) {
 -              _sound_asset_writer->finalize ();
 +      _picture_mxf_writer->finalize ();
 +      if (_sound_mxf_writer) {
 +              _sound_mxf_writer->finalize ();
        }
        
 -      int const frames = _last_written_frame + 1;
 -
 -      _picture_asset->set_duration (frames);
 -
        /* Hard-link the video MXF into the DCP */
        boost::filesystem::path video_from;
        video_from /= _film->internal_video_mxf_dir();
        if (ec) {
                /* hard link failed; copy instead */
                boost::filesystem::copy_file (video_from, video_to);
-               _film->log()->log ("Hard-link failed; fell back to copying");
+               LOG_WARNING_NC ("Hard-link failed; fell back to copying");
        }
  
 -      /* And update the asset */
 -
 -      _picture_asset->set_directory (_film->dir (_film->dcp_name ()));
 -      _picture_asset->set_file_name (_film->video_mxf_filename ());
 +      _picture_mxf->set_file (video_to);
  
        /* Move the audio MXF into the DCP */
  
 -      if (_sound_asset) {
 +      if (_sound_mxf) {
                boost::filesystem::path audio_to;
                audio_to /= _film->dir (_film->dcp_name ());
                audio_to /= _film->audio_mxf_filename ();
                                String::compose (_("could not move audio MXF into the DCP (%1)"), ec.value ()), _film->file (_film->audio_mxf_filename ())
                                );
                }
 -              
 -              _sound_asset->set_directory (_film->dir (_film->dcp_name ()));
 -              _sound_asset->set_duration (frames);
 +
 +              _sound_mxf->set_file (audio_to);
        }
 -      
 -      libdcp::DCP dcp (_film->dir (_film->dcp_name()));
  
 -      shared_ptr<libdcp::CPL> cpl (
 -              new libdcp::CPL (
 -                      _film->dir (_film->dcp_name()),
 +      dcp::DCP dcp (_film->dir (_film->dcp_name()));
 +
 +      shared_ptr<dcp::CPL> cpl (
 +              new dcp::CPL (
                        _film->dcp_name(),
 -                      _film->dcp_content_type()->libdcp_kind (),
 -                      frames,
 -                      _film->video_frame_rate ()
 +                      _film->dcp_content_type()->libdcp_kind ()
                        )
                );
        
 -      dcp.add_cpl (cpl);
 +      dcp.add (cpl);
 +
 +      shared_ptr<dcp::Reel> reel (new dcp::Reel ());
  
 -      cpl->add_reel (shared_ptr<libdcp::Reel> (new libdcp::Reel (
 -                                                       _picture_asset,
 -                                                       _sound_asset,
 -                                                       shared_ptr<libdcp::SubtitleAsset> ()
 -                                                       )
 -                             ));
 +      shared_ptr<dcp::MonoPictureMXF> mono = dynamic_pointer_cast<dcp::MonoPictureMXF> (_picture_mxf);
 +      if (mono) {
 +              reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelMonoPictureAsset (mono, 0)));
 +              dcp.add (mono);
 +      }
 +
 +      shared_ptr<dcp::StereoPictureMXF> stereo = dynamic_pointer_cast<dcp::StereoPictureMXF> (_picture_mxf);
 +      if (stereo) {
 +              reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelStereoPictureAsset (stereo, 0)));
 +              dcp.add (stereo);
 +      }
 +
 +      if (_sound_mxf) {
 +              reel->add (shared_ptr<dcp::ReelSoundAsset> (new dcp::ReelSoundAsset (_sound_mxf, 0)));
 +              dcp.add (_sound_mxf);
 +      }
 +      
 +      cpl->add (reel);
  
        shared_ptr<Job> job = _job.lock ();
        assert (job);
  
        job->sub (_("Computing image digest"));
 -      _picture_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false));
 +      _picture_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false));
  
 -      if (_sound_asset) {
 +      if (_sound_mxf) {
                job->sub (_("Computing audio digest"));
 -              _sound_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false));
 +              _sound_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false));
        }
  
 -      libdcp::XMLMetadata meta = Config::instance()->dcp_metadata ();
 +      dcp::XMLMetadata meta = Config::instance()->dcp_metadata ();
        meta.set_issue_date_now ();
 -      dcp.write_xml (_film->interop (), meta, _film->is_signed() ? make_signer () : shared_ptr<const libdcp::Signer> ());
 +
 +      dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, _film->is_signed() ? make_signer () : shared_ptr<const dcp::Signer> ());
  
-       _film->log()->log (
-               String::compose (N_("Wrote %1 FULL, %2 FAKE, %3 pushed to disk"), _full_written, _fake_written, _pushed_to_disk)
+       LOG_GENERAL (
 -              N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT; %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
++              N_("Wrote %1 FULL, %2 FAKE, %3 pushed to disk"), _full_written, _fake_written, _pushed_to_disk
                );
  }
  
 -/** Tell the writer that frame `f' should be a repeat of the frame before it */
 -void
 -Writer::repeat (int f, Eyes e)
 -{
 -      boost::mutex::scoped_lock lock (_mutex);
 -
 -      while (_queued_full_in_memory > _maximum_frames_in_memory) {
 -              _full_condition.wait (lock);
 -      }
 -      
 -      QueueItem qi;
 -      qi.type = QueueItem::REPEAT;
 -      qi.frame = f;
 -      if (_film->three_d() && e == EYES_BOTH) {
 -              qi.eyes = EYES_LEFT;
 -              _queue.push_back (qi);
 -              qi.eyes = EYES_RIGHT;
 -              _queue.push_back (qi);
 -      } else {
 -              qi.eyes = e;
 -              _queue.push_back (qi);
 -      }
 -
 -      _empty_condition.notify_all ();
 -}
 -
  bool
  Writer::check_existing_picture_mxf_frame (FILE* mxf, int f, Eyes eyes)
  {
        /* Read the frame info as written */
        FILE* ifi = fopen_boost (_film->info_path (f, eyes), "r");
        if (!ifi) {
-               _film->log()->log (String::compose ("Existing frame %1 has no info file", f));
+               LOG_GENERAL ("Existing frame %1 has no info file", f);
                return false;
        }
        
 -      libdcp::FrameInfo info (ifi);
 +      dcp::FrameInfo info (ifi);
        fclose (ifi);
        if (info.size == 0) {
-               _film->log()->log (String::compose ("Existing frame %1 has no info file", f));
+               LOG_GENERAL ("Existing frame %1 has no info file", f);
                return false;
        }
        
        EncodedData data (info.size);
        size_t const read = fread (data.data(), 1, data.size(), mxf);
        if (read != static_cast<size_t> (data.size ())) {
-               _film->log()->log (String::compose ("Existing frame %1 is incomplete", f));
+               LOG_GENERAL ("Existing frame %1 is incomplete", f);
                return false;
        }
        
        string const existing_hash = md5_digest (data.data(), data.size());
        if (existing_hash != info.hash) {
-               _film->log()->log (String::compose ("Existing frame %1 failed hash check", f));
+               LOG_GENERAL ("Existing frame %1 failed hash check", f);
                return false;
        }
  
@@@ -509,7 -543,7 +512,7 @@@ Writer::check_existing_picture_mxf (
        p /= _film->internal_video_mxf_filename ();
        FILE* mxf = fopen_boost (p, "rb");
        if (!mxf) {
-               _film->log()->log (String::compose ("Could not open existing MXF at %1 (errno=%2)", p.string(), errno));
+               LOG_GENERAL ("Could not open existing MXF at %1 (errno=%2)", p.string(), errno);
                return;
        }
  
                        }
                }
  
-               _film->log()->log (String::compose ("Have existing frame %1", _first_nonexistant_frame));
+               LOG_GENERAL ("Have existing frame %1", _first_nonexistant_frame);
                ++_first_nonexistant_frame;
        }
  
diff --combined src/tools/dcpomatic.cc
index f2aff9359a35412684f4262bbfe7afdd9c6aa110,0ec2a81a44ba1b5c5dd452fc51e0021dd8f0ecd7..23c1e500512a750f17468445a2cfcfd1cd78f7de
@@@ -30,6 -30,7 +30,7 @@@
  #include <wx/stdpaths.h>
  #include <wx/cmdline.h>
  #include <wx/preferences.h>
+ #include <libdcp/exceptions.h>
  #include "wx/film_viewer.h"
  #include "wx/film_editor.h"
  #include "wx/job_manager_view.h"
@@@ -74,7 -75,6 +75,6 @@@ using boost::dynamic_pointer_cast
  static FilmEditor* film_editor = 0;
  static FilmViewer* film_viewer = 0;
  static shared_ptr<Film> film;
- static std::string log_level;
  static std::string film_to_load;
  static std::string film_to_create;
  static std::string content_to_add;
@@@ -157,13 -157,12 +157,12 @@@ load_film (boost::filesystem::path file
        for (list<string>::const_iterator i = notes.begin(); i != notes.end(); ++i) {
                error_dialog (0, std_to_wx (*i));
        }
-       film->log()->set_level (log_level);
  }
  
  #define ALWAYS                  0x0
  #define NEEDS_FILM              0x1
  #define NOT_DURING_DCP_CREATION 0x2
- #define NEEDS_DCP               0x4
+ #define NEEDS_CPL               0x4
  
  map<wxMenuItem*, int> menu_items;
        
@@@ -183,7 -182,7 +182,7 @@@ set_menu_sensitivity (
                ++i;
        }
        bool const dcp_creation = (i != jobs.end ()) && !(*i)->finished ();
-       bool const have_dcp = film && !film->dcps().empty ();
+       bool const have_cpl = film && !film->cpls().empty ();
  
        for (map<wxMenuItem*, int>::iterator j = menu_items.begin(); j != menu_items.end(); ++j) {
  
                        enabled = false;
                }
  
-               if ((j->second & NEEDS_DCP) && !have_dcp) {
+               if ((j->second & NEEDS_CPL) && !have_cpl) {
                        enabled = false;
                }
                
@@@ -249,9 -248,9 +248,9 @@@ setup_menu (wxMenuBar* m
  
        jobs_menu = new wxMenu;
        add_item (jobs_menu, _("&Make DCP"), ID_jobs_make_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION);
-       add_item (jobs_menu, _("Make &KDMs..."), ID_jobs_make_kdms, NEEDS_FILM | NEEDS_DCP);
-       add_item (jobs_menu, _("&Send DCP to TMS"), ID_jobs_send_dcp_to_tms, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_DCP);
-       add_item (jobs_menu, _("S&how DCP"), ID_jobs_show_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_DCP);
+       add_item (jobs_menu, _("Make &KDMs..."), ID_jobs_make_kdms, NEEDS_FILM);
+       add_item (jobs_menu, _("&Send DCP to TMS"), ID_jobs_send_dcp_to_tms, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
+       add_item (jobs_menu, _("S&how DCP"), ID_jobs_show_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION | NEEDS_CPL);
  
        wxMenu* tools = new wxMenu;
        add_item (tools, _("Hints..."), ID_tools_hints, 0);
@@@ -401,7 -400,6 +400,6 @@@ private
                        maybe_save_then_delete_film ();
                        film.reset (new Film (d->get_path ()));
                        film->write_metadata ();
-                       film->log()->set_level (log_level);
                        film->set_name (boost::filesystem::path (d->get_path()).filename().generic_string());
                        set_film ();
                }
  
                try {
                        if (d->write_to ()) {
-                               write_kdm_files (film, d->screens (), d->dcp (), d->from (), d->until (), d->directory ());
+                               write_kdm_files (film, d->screens (), d->cpl (), d->from (), d->until (), d->directory ());
                        } else {
                                JobManager::instance()->add (
-                                       shared_ptr<Job> (new SendKDMEmailJob (film, d->screens (), d->dcp (), d->from (), d->until ()))
+                                       shared_ptr<Job> (new SendKDMEmailJob (film, d->screens (), d->cpl (), d->from (), d->until ()))
                                        );
                        }
+               } catch (libdcp::NotEncryptedError& e) {
+                       error_dialog (this, _("CPL's content is not encrypted."));
                } catch (exception& e) {
                        error_dialog (this, e.what ());
                } catch (...) {
  };
  
  static const wxCmdLineEntryDesc command_line_description[] = {
-       { wxCMD_LINE_OPTION, "l", "log", "set log level (silent, verbose or timing)", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
        { wxCMD_LINE_SWITCH, "n", "new", "create new film", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
        { wxCMD_LINE_OPTION, "c", "content", "add content file", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
        { wxCMD_LINE_PARAM, 0, 0, "film to load or create", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
        { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
  };
  
 +/** @class App
 + *  @brief The magic App class for wxWidgets.
 + */
  class App : public wxApp
  {
        bool OnInit ()
                if (!film_to_create.empty ()) {
                        film.reset (new Film (film_to_create));
                        film->write_metadata ();
-                       film->log()->set_level (log_level);
                        film->set_name (boost::filesystem::path (film_to_create).filename().generic_string ());
                }
  
                        content_to_add = wx_to_std (content);
                }
  
-               wxString log;
-               if (parser.Found (wxT ("log"), &log)) {
-                       log_level = wx_to_std (log);
-               }
                return true;
        }
  
index d95b2ed99f63fac0a2c0e923c4272bedf1fa69ae,8fed28f18039217e82aa618e36e985f0d98f695e..acd1335e62d5e5ef57c469232e429db29c22e6c6
@@@ -20,7 -20,7 +20,7 @@@
  #include <iostream>
  #include <iomanip>
  #include <getopt.h>
 -#include <libdcp/version.h>
 +#include <dcp/version.h>
  #include "lib/film.h"
  #include "lib/filter.h"
  #include "lib/transcode_job.h"
@@@ -65,7 -65,6 +65,6 @@@ main (int argc, char* argv[]
        string film_dir;
        bool progress = true;
        bool no_remote = false;
-       int log_level = 0;
        int json_port = 0;
        bool keep_going = false;
  
                        { "flags", no_argument, 0, 'f'},
                        { "no-progress", no_argument, 0, 'n'},
                        { "no-remote", no_argument, 0, 'r'},
-                       { "log-level", required_argument, 0, 'l' },
                        { "json", required_argument, 0, 'j' },
                        { "keep-going", no_argument, 0, 'k' },
                        { 0, 0, 0, 0 }
                };
  
-               int c = getopt_long (argc, argv, "vhdfnrl:j:k", long_options, &option_index);
+               int c = getopt_long (argc, argv, "vhdfnrj:k", long_options, &option_index);
  
                if (c == -1) {
                        break;
                case 'r':
                        no_remote = true;
                        break;
-               case 'l':
-                       log_level = atoi (optarg);
-                       break;
                case 'j':
                        json_port = atoi (optarg);
                        break;
                exit (EXIT_FAILURE);
        }
  
-       film->log()->set_level ((Log::Level) log_level);
        cout << "\nMaking DCP for " << film->name() << "\n";
  //    cout << "Content: " << film->content() << "\n";
  //    pair<string, string> const f = Filter::ffmpeg_strings (film->filters ());
index 2f6916df2be684ae5569e30a73af3922eaba3c72,041f6c7ef295d1a9100454fa7d25cfb0a6e87991..0f2d5b8a31ad5e6c757a27d8702a1fe0d396726e
@@@ -18,7 -18,7 +18,7 @@@
  */
  
  #include <getopt.h>
 -#include <libdcp/certificates.h>
 +#include <dcp/certificates.h>
  #include "lib/film.h"
  #include "lib/cinema.h"
  #include "lib/kdm.h"
@@@ -30,6 -30,7 +30,7 @@@ using std::stringstream
  using std::cout;
  using std::cerr;
  using std::list;
+ using std::vector;
  using boost::shared_ptr;
  
  static string program_name;
@@@ -40,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"
                "  -z, --zip              ZIP each cinema's KDMs into its own file\n"
                "  -v, --verbose          be verbose\n"
@@@ -219,14 -220,14 +220,14 @@@ int main (int argc, char* argv[]
        }
  
        /* XXX: allow specification of this */
-       list<boost::filesystem::path> dcps = film->dcps ();
-       if (dcps.empty ()) {
-               error ("no DCPs found in film");
-       } else if (dcps.size() > 1) {
-               error ("more than one DCP found in film");
+       vector<CPLSummary> cpls = film->cpls ();
+       if (cpls.empty ()) {
+               error ("no CPLs found in film");
+       } else if (cpls.size() > 1) {
+               error ("more than one CPL found in film");
        }
  
-       boost::filesystem::path dcp = dcps.front ();
+       boost::filesystem::path cpl = cpls.front().cpl_file;
  
        if (cinema_name.empty ()) {
  
                        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());
 +              shared_ptr<dcp::Certificate> certificate (new dcp::Certificate (boost::filesystem::path (certificate_file)));
-               dcp::EncryptedKDM kdm = film->make_kdm (certificate, dcp, valid_from.get(), valid_to.get());
++              dcp::EncryptedKDM kdm = film->make_kdm (certificate, cpl, valid_from.get(), valid_to.get());
                kdm.as_xml (output);
                if (verbose) {
                        cout << "Generated KDM " << output << " for certificate.\n";
  
                try {
                        if (zip) {
-                               write_kdm_zip_files (film, (*i)->screens(), dcp, dcp::LocalTime (valid_from.get()), dcp::LocalTime (valid_to.get()), output);
 -                              write_kdm_zip_files (film, (*i)->screens(), cpl, valid_from.get(), valid_to.get(), output);
++                              write_kdm_zip_files (film, (*i)->screens(), cpl, dcp::LocalTime (valid_from.get()), dcp::LocalTime (valid_to.get()), output);
++
                                if (verbose) {
                                        cout << "Wrote ZIP files to " << output << "\n";
                                }
                        } else {
-                               write_kdm_files (film, (*i)->screens(), dcp, dcp::LocalTime (valid_from.get()), dcp::LocalTime (valid_to.get()), output);
 -                              write_kdm_files (film, (*i)->screens(), cpl, valid_from.get(), valid_to.get(), output);
++                              write_kdm_files (film, (*i)->screens(), cpl, dcp::LocalTime (valid_from.get()), dcp::LocalTime (valid_to.get()), output);
                                if (verbose) {
                                        cout << "Wrote KDM files to " << output << "\n";
                                }
diff --combined src/wx/about_dialog.cc
index 276f5507ae9b9c521abd1dbb4320946a5f74c37c,e0296d60cfeb60589610d2d79617756fe6e24e35..105831014eccc81354e7910b3fd0b76fce86c717
@@@ -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"
@@@ -162,8 -158,10 +162,10 @@@ AboutDialog::AboutDialog (wxWindow* par
        tested_by.Add (wxT ("Andreas Eli"));
        tested_by.Add (wxT ("Maurizio Giampà"));
        tested_by.Add (wxT ("Luke Granger-Brown"));
+       tested_by.Add (wxT ("Sumit Guha"));
        tested_by.Add (wxT ("Steve Guttag"));
        tested_by.Add (wxT ("Patrick Haderer"));
+       tested_by.Add (wxT ("Bill Hamell"));
        tested_by.Add (wxT ("Jonathan Jensen"));
        tested_by.Add (wxT ("Thierry Journet"));
        tested_by.Add (wxT ("Ada de Kamper"));
        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/config_dialog.cc
index f06670dfb222c7d2e613b48ae7055cfd41e335c3,684d2496e67feb3a53e7903efd6d03e048c955a7..631628e1d1d0a13163a0800107d75fe9f2a55b9c
@@@ -28,7 -28,7 +28,7 @@@
  #include <wx/preferences.h>
  #include <wx/filepicker.h>
  #include <wx/spinctrl.h>
 -#include <libdcp/colour_matrix.h>
 +#include <dcp/colour_matrix.h>
  #include "lib/config.h"
  #include "lib/ratio.h"
  #include "lib/scaler.h"
@@@ -106,14 -106,6 +106,6 @@@ public
                _num_local_encoding_threads = new wxSpinCtrl (panel);
                table->Add (_num_local_encoding_threads, 1);
  
-               add_label_to_sizer (table, panel, _("Maximum JPEG2000 bandwidth"), true);
-               _maximum_j2k_bandwidth = new wxSpinCtrl (panel);
-               table->Add (_maximum_j2k_bandwidth, 1);
-               _allow_any_dcp_frame_rate = new wxCheckBox (panel, wxID_ANY, _("Allow any DCP frame rate"));
-               table->Add (_allow_any_dcp_frame_rate, 1, wxEXPAND | wxALL);
-               table->AddSpacer (0);
-               
                add_label_to_sizer (table, panel, _("Outgoing mail server"), true);
                _mail_server = new wxTextCtrl (panel, wxID_ANY);
                table->Add (_mail_server, 1, wxEXPAND | wxALL);
                _num_local_encoding_threads->SetValue (config->num_local_encoding_threads ());
                _num_local_encoding_threads->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&GeneralPage::num_local_encoding_threads_changed, this));
  
-               _maximum_j2k_bandwidth->SetRange (1, 500);
-               _maximum_j2k_bandwidth->SetValue (config->maximum_j2k_bandwidth() / 1000000);
-               _maximum_j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&GeneralPage::maximum_j2k_bandwidth_changed, this));
-               
                _mail_server->SetValue (std_to_wx (config->mail_server ()));
                _mail_server->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&GeneralPage::mail_server_changed, this));
                _mail_user->SetValue (std_to_wx (config->mail_user ()));
                _check_for_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::check_for_updates_changed, this));
                _check_for_test_updates->SetValue (config->check_for_test_updates ());
                _check_for_test_updates->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::check_for_test_updates_changed, this));
-               _allow_any_dcp_frame_rate->SetValue (config->allow_any_dcp_frame_rate ());
-               _allow_any_dcp_frame_rate->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&GeneralPage::allow_any_dcp_frame_rate_changed, this));
                
                return panel;
        }
@@@ -271,21 -257,9 +257,9 @@@ private
                Config::instance()->set_num_local_encoding_threads (_num_local_encoding_threads->GetValue ());
        }
  
-       void maximum_j2k_bandwidth_changed ()
-       {
-               Config::instance()->set_maximum_j2k_bandwidth (_maximum_j2k_bandwidth->GetValue() * 1000000);
-       }
-       void allow_any_dcp_frame_rate_changed ()
-       {
-               Config::instance()->set_allow_any_dcp_frame_rate (_allow_any_dcp_frame_rate->GetValue ());
-       }
-       
        wxCheckBox* _set_language;
        wxChoice* _language;
        wxSpinCtrl* _num_local_encoding_threads;
-       wxSpinCtrl* _maximum_j2k_bandwidth;
-       wxCheckBox* _allow_any_dcp_frame_rate;
        wxTextCtrl* _mail_server;
        wxTextCtrl* _mail_user;
        wxTextCtrl* _mail_password;
@@@ -472,14 -446,14 +446,14 @@@ private
  
        void issuer_changed ()
        {
 -              libdcp::XMLMetadata m = Config::instance()->dcp_metadata ();
 +              dcp::XMLMetadata m = Config::instance()->dcp_metadata ();
                m.issuer = wx_to_std (_issuer->GetValue ());
                Config::instance()->set_dcp_metadata (m);
        }
        
        void creator_changed ()
        {
 -              libdcp::XMLMetadata m = Config::instance()->dcp_metadata ();
 +              dcp::XMLMetadata m = Config::instance()->dcp_metadata ();
                m.creator = wx_to_std (_creator->GetValue ());
                Config::instance()->set_dcp_metadata (m);
        }
@@@ -742,6 -716,104 +716,104 @@@ private
        wxTextCtrl* _kdm_email;
  };
  
+ class AdvancedPage : public wxStockPreferencesPage, public Page
+ {
+ public:
+       AdvancedPage (wxSize panel_size, int border)
+               : wxStockPreferencesPage (Kind_Advanced)
+               , Page (panel_size, border)
+       {}
+       
+       wxWindow* CreateWindow (wxWindow* parent)
+       {
+               wxPanel* panel = new wxPanel (parent);
+               wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
+               panel->SetSizer (s);
+               wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+               table->AddGrowableCol (1, 1);
+               s->Add (table, 1, wxALL | wxEXPAND, _border);
+               
+               add_label_to_sizer (table, panel, _("Maximum JPEG2000 bandwidth"), true);
+               _maximum_j2k_bandwidth = new wxSpinCtrl (panel);
+               table->Add (_maximum_j2k_bandwidth, 1);
+               _allow_any_dcp_frame_rate = new wxCheckBox (panel, wxID_ANY, _("Allow any DCP frame rate"));
+               table->Add (_allow_any_dcp_frame_rate, 1, wxEXPAND | wxALL);
+               table->AddSpacer (0);
+               add_label_to_sizer (table, panel, _("Log"), true);
+               _log_general = new wxCheckBox (panel, wxID_ANY, _("General"));
+               table->Add (_log_general, 1, wxEXPAND | wxALL);
+               _log_warning = new wxCheckBox (panel, wxID_ANY, _("Warnings"));
+               table->AddSpacer (0);
+               table->Add (_log_warning, 1, wxEXPAND | wxALL);
+               _log_error = new wxCheckBox (panel, wxID_ANY, _("Errors"));
+               table->AddSpacer (0);
+               table->Add (_log_error, 1, wxEXPAND | wxALL);
+               _log_timing = new wxCheckBox (panel, wxID_ANY, _("Timing"));
+               table->AddSpacer (0);
+               table->Add (_log_timing, 1, wxEXPAND | wxALL);
+               Config* config = Config::instance ();
+               
+               _maximum_j2k_bandwidth->SetRange (1, 500);
+               _maximum_j2k_bandwidth->SetValue (config->maximum_j2k_bandwidth() / 1000000);
+               _maximum_j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&AdvancedPage::maximum_j2k_bandwidth_changed, this));
+               _allow_any_dcp_frame_rate->SetValue (config->allow_any_dcp_frame_rate ());
+               _allow_any_dcp_frame_rate->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::allow_any_dcp_frame_rate_changed, this));
+               _log_general->SetValue (config->log_types() & Log::TYPE_GENERAL);
+               _log_general->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
+               _log_warning->SetValue (config->log_types() & Log::TYPE_WARNING);
+               _log_warning->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
+               _log_error->SetValue (config->log_types() & Log::TYPE_ERROR);
+               _log_error->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
+               _log_timing->SetValue (config->log_types() & Log::TYPE_TIMING);
+               _log_timing->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this));
+               
+               return panel;
+       }
+ private:
+       void maximum_j2k_bandwidth_changed ()
+       {
+               Config::instance()->set_maximum_j2k_bandwidth (_maximum_j2k_bandwidth->GetValue() * 1000000);
+       }
+       void allow_any_dcp_frame_rate_changed ()
+       {
+               Config::instance()->set_allow_any_dcp_frame_rate (_allow_any_dcp_frame_rate->GetValue ());
+       }
+       void log_changed ()
+       {
+               int types = 0;
+               if (_log_general->GetValue ()) {
+                       types |= Log::TYPE_GENERAL;
+               }
+               if (_log_warning->GetValue ()) {
+                       types |= Log::TYPE_WARNING;
+               }
+               if (_log_error->GetValue ())  {
+                       types |= Log::TYPE_ERROR;
+               }
+               if (_log_timing->GetValue ()) {
+                       types |= Log::TYPE_TIMING;
+               }
+               Config::instance()->set_log_types (types);
+       }
+       
+       wxSpinCtrl* _maximum_j2k_bandwidth;
+       wxCheckBox* _allow_any_dcp_frame_rate;
+       wxCheckBox* _log_general;
+       wxCheckBox* _log_warning;
+       wxCheckBox* _log_error;
+       wxCheckBox* _log_timing;
+ };
+       
  wxPreferencesEditor*
  create_config_dialog ()
  {
        e->AddPage (new ColourConversionsPage (ps, border));
        e->AddPage (new TMSPage (ps, border));
        e->AddPage (new KDMEmailPage (ps, border));
+       e->AddPage (new AdvancedPage (ps, border));
        return e;
  }
diff --combined src/wx/film_editor.cc
index c20a1232402f1b4f5bc337aa9b724219d40f3fb7,17db76ef60e6afc20e3dc9b0f5921f331e5c2039..a6cb77f8561cf36de0aed3594e62a5a8b058acc6
@@@ -275,25 -275,19 +275,19 @@@ FilmEditor::make_content_panel (
                _content->InsertColumn (0, wxT(""));
                _content->SetColumnWidth (0, 512);
  
- #ifdef DCPOMATIC_OSX
-               int const pad = 2;
- #else
-               int const pad = 0;
- #endif                
                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, pad);
+               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, pad);
+               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, pad);
+               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, pad);
+               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, pad);
+               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, pad);
+               b->Add (_content_timeline, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP);
  
                s->Add (b, 0, wxALL, 4);
  
@@@ -873,7 -867,7 +867,7 @@@ FilmEditor::setup_content_sensitivity (
  
        _video_panel->Enable    (video_selection.size() > 0 && _generally_sensitive);
        _audio_panel->Enable    (audio_selection.size() > 0 && _generally_sensitive);
 -      _subtitle_panel->Enable (selection.size() == 1 && dynamic_pointer_cast<FFmpegContent> (selection.front()) && _generally_sensitive);
 +      _subtitle_panel->Enable (selection.size() == 1 && dynamic_pointer_cast<SubtitleContent> (selection.front()) && _generally_sensitive);
        _timing_panel->Enable   (selection.size() == 1 && _generally_sensitive);
  }
  
index 8e4fb0e1867e845e644864012eae8812b2468183,98993cc5bd67c5148870e38245ff56db6da5864e..c0eddd8f6a7f8eee16dcbbef3d27119a2cf45898
  
  */
  
 +/** @file  test/client_server_test.cc
 + *  @brief Test the server class.
 + *
 + *  Create a test image and then encode it using the standard mechanism
 + *  and also using a Server object running on localhost.  Compare the resulting
 + *  encoded data to check that they are the same.
 + */
 +
  #include <boost/test/unit_test.hpp>
  #include <boost/thread.hpp>
  #include "lib/server.h"
@@@ -47,12 -39,12 +47,12 @@@ do_remote_encode (shared_ptr<DCPVideoFr
        BOOST_CHECK (remotely_encoded);
        
        BOOST_CHECK_EQUAL (locally_encoded->size(), remotely_encoded->size());
 -      BOOST_CHECK (memcmp (locally_encoded->data(), remotely_encoded->data(), locally_encoded->size()) == 0);
 +      BOOST_CHECK_EQUAL (memcmp (locally_encoded->data(), remotely_encoded->data(), locally_encoded->size()), 0);
  }
  
  BOOST_AUTO_TEST_CASE (client_server_test)
  {
 -      shared_ptr<Image> image (new Image (PIX_FMT_RGB24, libdcp::Size (1998, 1080), true));
 +      shared_ptr<Image> image (new Image (PIX_FMT_RGB24, dcp::Size (1998, 1080), true));
        uint8_t* p = image->data()[0];
        
        for (int y = 0; y < 1080; ++y) {
@@@ -65,7 -57,7 +65,7 @@@
                p += image->stride()[0];
        }
  
 -      shared_ptr<Image> sub_image (new Image (PIX_FMT_RGBA, libdcp::Size (100, 200), true));
 +      shared_ptr<Image> sub_image (new Image (PIX_FMT_RGBA, dcp::Size (100, 200), true));
        p = sub_image->data()[0];
        for (int y = 0; y < 200; ++y) {
                uint8_t* q = p;
                p += sub_image->stride()[0];
        }
  
+       shared_ptr<FileLog> log (new FileLog ("build/test/client_server_test.log"));
        shared_ptr<PlayerVideoFrame> pvf (
                new PlayerVideoFrame (
-                       shared_ptr<ImageProxy> (new RawImageProxy (image)),
+                       shared_ptr<ImageProxy> (new RawImageProxy (image, log)),
                        Crop (),
 -                      libdcp::Size (1998, 1080),
 -                      libdcp::Size (1998, 1080),
 +                      dcp::Size (1998, 1080),
 +                      dcp::Size (1998, 1080),
                        Scaler::from_id ("bicubic"),
                        EYES_BOTH,
                        PART_WHOLE,
                        )
                );
  
 -      pvf->set_subtitle (sub_image, Position<int> (50, 60));
 +      pvf->set_subtitle (PositionImage (sub_image, Position<int> (50, 60)));
  
-       shared_ptr<FileLog> log (new FileLog ("build/test/client_server_test.log"));
        shared_ptr<DCPVideoFrame> frame (
                new DCPVideoFrame (
                        pvf,
diff --combined test/ffmpeg_dcp_test.cc
index 1c897bcd93a90bc158fcbbb9232debe7e72a1529,4922ec4d4d99f4c6d523ffc9175bf32ece02caee..234bf2c79b10bd8dafc5e74f71d2a36c98c22d3e
@@@ -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
  
  */
  
 +/** @file test/ffmpeg_dcp_test.cc
 + *  @brief Test creation of a very simple DCP from some FFmpegContent (data/test.mp4).
 + *
 + *  Also a quick test of Film::have_dcp ().
 + */
 +
  #include <boost/test/unit_test.hpp>
  #include <boost/filesystem.hpp>
  #include "lib/film.h"
  
  using boost::shared_ptr;
  
 -/** @file test/ffmpeg_dcp_test.cc
 - *  @brief Test scaling and black-padding of images from a still-image source.
 - */
 -
  BOOST_AUTO_TEST_CASE (ffmpeg_dcp_test)
  {
        shared_ptr<Film> film = new_test_film ("ffmpeg_dcp_test");
        wait_for_jobs ();
  }
  
- /** Test Film::have_dcp().  Requires the output from ffmpeg_dcp_test above */
+ /** Briefly test Film::cpls().  Requires the output from ffmpeg_dcp_test above */
  BOOST_AUTO_TEST_CASE (ffmpeg_have_dcp_test)
  {
        boost::filesystem::path p = test_film_dir ("ffmpeg_dcp_test");
        shared_ptr<Film> f (new Film (p.string ()));
        f->read_metadata ();
-       BOOST_CHECK (!f->dcps().empty());
+       BOOST_CHECK (!f->cpls().empty());
  
        p /= f->dcp_name();
        p /= f->video_mxf_filename();
        boost::filesystem::remove (p);
-       BOOST_CHECK (f->dcps().empty());
+       BOOST_CHECK (f->cpls().empty());
  }