Merge master; specify libdcp-1.0.
authorCarl Hetherington <cth@carlh.net>
Mon, 24 Feb 2014 12:19:50 +0000 (12:19 +0000)
committerCarl Hetherington <cth@carlh.net>
Mon, 24 Feb 2014 12:19:50 +0000 (12:19 +0000)
20 files changed:
1  2 
cscript
src/lib/analyse_audio_job.cc
src/lib/analyse_audio_job.h
src/lib/content.cc
src/lib/content.h
src/lib/ffmpeg_content.cc
src/lib/ffmpeg_content.h
src/lib/ffmpeg_decoder.cc
src/lib/ffmpeg_examiner.cc
src/lib/ffmpeg_examiner.h
src/lib/film.cc
src/lib/film.h
src/lib/job.cc
src/lib/transcode_job.cc
src/lib/util.cc
src/lib/util.h
src/lib/wscript
src/wx/audio_mapping_view.cc
test/stream_test.cc
wscript

diff --combined cscript
index d6a04aba6e2ae9797a04d75346ee1d9d94d01d19,14e3347a19703ddf1a125a39a8b85ca2164fb91a..b8b0580063ee0490652cd2c8e0b0132360bcf349
+++ b/cscript
@@@ -129,8 -129,8 +129,8 @@@ def make_control(debian_version, bits, 
          print >>f,''
  
  def dependencies(target):
-     return (('ffmpeg-cdist', '5ac3a6af077c10f07c31954c372a8f29e4e18e2a'),
+     return (('ffmpeg-cdist', '08827fa4e1d483511e6135c424d2ca9c56a9ed50'),
 -            ('libdcp', '5839998'))
 +            ('libdcp', '1.0'))
  
  def build(target, options):
      cmd = './waf configure --prefix=%s' % target.work_dir_cscript()
index 872947b55d3d56cac4a957ca561bc5944a81d34c,bfe0ed61f281816a71e20b66f17c18e429d46ac9..a6926bc14a02a4a465cd01b37496eb5e97bdd6ea
@@@ -48,6 -48,12 +48,12 @@@ AnalyseAudioJob::name () cons
        return _("Analyse audio");
  }
  
+ string
+ AnalyseAudioJob::json_name () const
+ {
+       return N_("analyse_audio");
+ }
  void
  AnalyseAudioJob::run ()
  {
@@@ -69,7 -75,7 +75,7 @@@
        _analysis.reset (new AudioAnalysis (_film->audio_channels ()));
  
        _done = 0;
 -      OutputAudioFrame const len = _film->time_to_audio_frames (_film->length ());
 +      AudioFrame const len = _film->time_to_audio_frames (_film->length ());
        while (!player->pass ()) {
                set_progress (double (_done) / len);
        }
@@@ -81,7 -87,7 +87,7 @@@
  }
  
  void
 -AnalyseAudioJob::audio (shared_ptr<const AudioBuffers> b, Time)
 +AnalyseAudioJob::audio (shared_ptr<const AudioBuffers> b, DCPTime)
  {
        for (int i = 0; i < b->frames(); ++i) {
                for (int j = 0; j < b->channels(); ++j) {
index 6ed236d85ca57854a7a4f4691f4082bf4c1c059e,3e376634cd0d9be375fc825ae60c6285db16588a..3d3900a5112b5f621e01a81a81b80c55a132271a
@@@ -30,13 -30,14 +30,14 @@@ public
        AnalyseAudioJob (boost::shared_ptr<const Film>, boost::shared_ptr<AudioContent>);
  
        std::string name () const;
+       std::string json_name () const;
        void run ();
  
  private:
 -      void audio (boost::shared_ptr<const AudioBuffers>, Time);
 +      void audio (boost::shared_ptr<const AudioBuffers>, DCPTime);
  
        boost::weak_ptr<AudioContent> _content;
 -      OutputAudioFrame _done;
 +      AudioFrame _done;
        int64_t _samples_per_point;
        std::vector<AudioPoint> _current;
  
diff --combined src/lib/content.cc
index ea1c19acdc1cc620cffeee582e85a35bd4d10acc,1883dfb4a9276d7773a8fe8b3ba34f32ace08662..8e3b99da89b43027ede972e2b3200d2f63754475
@@@ -54,7 -54,7 +54,7 @@@ Content::Content (shared_ptr<const Film
  
  }
  
 -Content::Content (shared_ptr<const Film> f, Time p)
 +Content::Content (shared_ptr<const Film> f, DCPTime p)
        : _film (f)
        , _position (p)
        , _trim_start (0)
@@@ -83,9 -83,9 +83,9 @@@ Content::Content (shared_ptr<const Film
                _paths.push_back ((*i)->content ());
        }
        _digest = node->string_child ("Digest");
 -      _position = node->number_child<Time> ("Position");
 -      _trim_start = node->number_child<Time> ("TrimStart");
 -      _trim_end = node->number_child<Time> ("TrimEnd");
 +      _position = node->number_child<DCPTime> ("Position");
 +      _trim_start = node->number_child<DCPTime> ("TrimStart");
 +      _trim_end = node->number_child<DCPTime> ("TrimEnd");
  }
  
  Content::Content (shared_ptr<const Film> f, vector<shared_ptr<Content> > c)
@@@ -146,7 -146,7 +146,7 @@@ Content::signal_changed (int p
  }
  
  void
 -Content::set_position (Time p)
 +Content::set_position (DCPTime p)
  {
        {
                boost::mutex::scoped_lock lm (_mutex);
  }
  
  void
 -Content::set_trim_start (Time t)
 +Content::set_trim_start (DCPTime t)
  {
        {
                boost::mutex::scoped_lock lm (_mutex);
  }
  
  void
 -Content::set_trim_end (Time t)
 +Content::set_trim_end (DCPTime t)
  {
        {
                boost::mutex::scoped_lock lm (_mutex);
@@@ -195,7 -195,7 +195,7 @@@ Content::clone () cons
        xmlpp::Document doc;
        xmlpp::Node* node = doc.create_root_node ("Content");
        as_xml (node);
-       return content_factory (film, cxml::NodePtr (new cxml::Node (node)), Film::state_version);
+       return content_factory (film, cxml::NodePtr (new cxml::Node (node)), Film::current_state_version);
  }
  
  string
@@@ -204,12 -204,21 +204,12 @@@ Content::technical_summary () cons
        return String::compose ("%1 %2 %3", path_summary(), digest(), position());
  }
  
 -Time
 +DCPTime
  Content::length_after_trim () const
  {
        return full_length() - trim_start() - trim_end();
  }
  
 -/** @param t A time relative to the start of this content (not the position).
 - *  @return true if this time is trimmed by our trim settings.
 - */
 -bool
 -Content::trimmed (Time t) const
 -{
 -      return (t < trim_start() || t > (full_length() - trim_end ()));
 -}
 -
  /** @return string which includes everything about how this content affects
   *  its playlist.
   */
diff --combined src/lib/content.h
index 3172b9c8d1c77b3ad19a3d4b034b88f744181311,596a0a905c95217d4daaf8ba116b8a6f518c6555..78a41e306010cf38828e0090151b81065ef263e7
@@@ -49,7 -49,7 +49,7 @@@ class Content : public boost::enable_sh
  {
  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>, std::vector<boost::shared_ptr<Content> >);
        
        virtual void examine (boost::shared_ptr<Job>);
        virtual std::string summary () const = 0;
+       /** @return Technical details of this content; these are written to logs to
+        *  help with debugging.
+        */
        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 {
 +      DCPTime end () const {
                return position() + length_after_trim() - 1;
        }
  
 -      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::signals2::signal<void (boost::weak_ptr<Content>, int, bool)> Changed;
  
  protected:
        
  private:
        std::string _digest;
 -      Time _position;
 -      Time _trim_start;
 -      Time _trim_end;
 +      DCPTime _position;
 +      DCPTime _trim_start;
 +      DCPTime _trim_end;
        bool _change_signals_frequent;
  };
  
index 3bee49146a5304024d083c6138b00e8332012390,5524efc65fc45cc6e24b5aa5e920da5f4d3739ea..3df1ba57ee78ddd1422f0cedc74c64ba696951c9
@@@ -66,7 -66,7 +66,7 @@@ FFmpegContent::FFmpegContent (shared_pt
  {
        list<cxml::NodePtr> c = node->node_children ("SubtitleStream");
        for (list<cxml::NodePtr>::const_iterator i = c.begin(); i != c.end(); ++i) {
-               _subtitle_streams.push_back (shared_ptr<FFmpegSubtitleStream> (new FFmpegSubtitleStream (*i, version)));
+               _subtitle_streams.push_back (shared_ptr<FFmpegSubtitleStream> (new FFmpegSubtitleStream (*i)));
                if ((*i)->optional_number_child<int> ("Selected")) {
                        _subtitle_stream = _subtitle_streams.back ();
                }
@@@ -163,7 -163,7 +163,7 @@@ FFmpegContent::examine (shared_ptr<Job
  
        shared_ptr<FFmpegExaminer> examiner (new FFmpegExaminer (shared_from_this ()));
  
 -      VideoContent::Frame video_length = 0;
 +      VideoFrame video_length = 0;
        video_length = examiner->video_length ();
        film->log()->log (String::compose ("Video length obtained from header as %1 frames", video_length));
  
@@@ -207,12 -207,12 +207,12 @@@ FFmpegContent::technical_summary () con
  {
        string as = "none";
        if (_audio_stream) {
-               as = String::compose ("id %1", _audio_stream->id);
+               as = _audio_stream->technical_summary ();
        }
  
        string ss = "none";
        if (_subtitle_stream) {
-               ss = String::compose ("id %1", _subtitle_stream->id);
+               ss = _subtitle_stream->technical_summary ();
        }
  
        pair<string, string> filt = Filter::ffmpeg_strings (_filters);
@@@ -262,12 -262,12 +262,12 @@@ FFmpegContent::set_audio_stream (shared
        signal_changed (FFmpegContentProperty::AUDIO_STREAM);
  }
  
 -AudioContent::Frame
 +AudioFrame
  FFmpegContent::audio_length () const
  {
        int const cafr = content_audio_frame_rate ();
        int const vfr  = video_frame_rate ();
 -      VideoContent::Frame const vl = video_length ();
 +      VideoFrame const vl = video_length ();
  
        boost::mutex::scoped_lock lm (_mutex);
        if (!_audio_stream) {
@@@ -310,69 -310,49 +310,48 @@@ FFmpegContent::output_audio_frame_rate 
        /* Resample to a DCI-approved sample rate */
        double t = dcp_audio_frame_rate (content_audio_frame_rate ());
  
 -      FrameRateConversion frc (video_frame_rate(), film->video_frame_rate());
 +      FrameRateChange frc (video_frame_rate(), film->video_frame_rate());
  
        /* Compensate if the DCP is being run at a different frame rate
           to the source; that is, if the video is run such that it will
           look different in the DCP compared to the source (slower or faster).
 -         skip/repeat doesn't come into effect here.
        */
  
        if (frc.change_speed) {
 -              t *= video_frame_rate() * frc.factor() / film->video_frame_rate();
 +              t /= frc.speed_up;
        }
  
        return rint (t);
  }
  
  bool
- operator== (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b)
+ operator== (FFmpegStream const & a, FFmpegStream const & b)
  {
-       return a.id == b.id;
+       return a._id == b._id;
  }
  
  bool
- operator!= (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b)
+ operator!= (FFmpegStream const & a, FFmpegStream const & b)
  {
-       return a.id != b.id;
+       return a._id != b._id;
  }
  
- bool
- operator== (FFmpegAudioStream const & a, FFmpegAudioStream const & b)
- {
-       return a.id == b.id;
- }
- bool
- operator!= (FFmpegAudioStream const & a, FFmpegAudioStream const & b)
+ FFmpegStream::FFmpegStream (shared_ptr<const cxml::Node> node)
+       : name (node->string_child ("Name"))
+       , _id (node->number_child<int> ("Id"))
  {
-       return a.id != b.id;
- }
  
- FFmpegStream::FFmpegStream (shared_ptr<const cxml::Node> node, int version)
-       : _legacy_id (false)
- {
-       name = node->string_child ("Name");
-       id = node->number_child<int> ("Id");
-       if (version == 4 || node->optional_bool_child ("LegacyId")) {
-               _legacy_id = true;
-       }
  }
  
  void
  FFmpegStream::as_xml (xmlpp::Node* root) const
  {
        root->add_child("Name")->add_child_text (name);
-       root->add_child("Id")->add_child_text (lexical_cast<string> (id));
-       if (_legacy_id) {
-               /* Write this so that version > 4 files are read in correctly
-                  if the Id came originally from a version <= 4 file.
-               */
-               root->add_child("LegacyId")->add_child_text ("1");
-       }
+       root->add_child("Id")->add_child_text (lexical_cast<string> (_id));
  }
  
  FFmpegAudioStream::FFmpegAudioStream (shared_ptr<const cxml::Node> node, int version)
-       : FFmpegStream (node, version)
+       : FFmpegStream (node)
        , mapping (node->node_child ("Mapping"), version)
  {
        frame_rate = node->number_child<int> ("FrameRate");
@@@ -392,34 -372,26 +371,26 @@@ FFmpegAudioStream::as_xml (xmlpp::Node
        mapping.as_xml (root->add_child("Mapping"));
  }
  
- int
- FFmpegStream::index (AVFormatContext const * fc) const
+ bool
+ FFmpegStream::uses_index (AVFormatContext const * fc, int index) const
  {
-       if (_legacy_id) {
-               return id;
-       }
-       
        size_t i = 0;
        while (i < fc->nb_streams) {
-               if (fc->streams[i]->id == id) {
-                       return i;
+               if (fc->streams[i]->id == _id) {
+                       return int (i) == index;
                }
                ++i;
        }
  
-       assert (false);
+       return false;
  }
  
  AVStream *
  FFmpegStream::stream (AVFormatContext const * fc) const
  {
-       if (_legacy_id) {
-               return fc->streams[id];
-       }
-       
        size_t i = 0;
        while (i < fc->nb_streams) {
-               if (fc->streams[i]->id == id) {
+               if (fc->streams[i]->id == _id) {
                        return fc->streams[i];
                }
                ++i;
   *  @param t String returned from to_string().
   *  @param v State file version.
   */
- FFmpegSubtitleStream::FFmpegSubtitleStream (shared_ptr<const cxml::Node> node, int version)
-       : FFmpegStream (node, version)
+ FFmpegSubtitleStream::FFmpegSubtitleStream (shared_ptr<const cxml::Node> node)
+       : FFmpegStream (node)
  {
        
  }
@@@ -445,13 -417,13 +416,13 @@@ FFmpegSubtitleStream::as_xml (xmlpp::No
        FFmpegStream::as_xml (root);
  }
  
 -Time
 +DCPTime
  FFmpegContent::full_length () const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
        
 -      FrameRateConversion frc (video_frame_rate (), film->video_frame_rate ());
 +      FrameRateChange frc (video_frame_rate (), film->video_frame_rate ());
        return video_length() * frc.factor() * TIME_HZ / film->video_frame_rate ();
  }
  
@@@ -495,7 -467,7 +466,7 @@@ FFmpegContent::identifier () cons
        boost::mutex::scoped_lock lm (_mutex);
  
        if (_subtitle_stream) {
-               s << "_" << _subtitle_stream->id;
+               s << "_" << _subtitle_stream->identifier ();
        }
  
        for (vector<Filter const *>::const_iterator i = _filters.begin(); i != _filters.end(); ++i) {
diff --combined src/lib/ffmpeg_content.h
index e637faf47643be48b0f3353bad5d440cb5231b62,6dbff41d1f43a306cb88976ef50636d3d3394447..d588fd2ee2d820d4495861ad43210406c767158c
@@@ -37,26 -37,35 +37,35 @@@ class FFmpegStrea
  public:
        FFmpegStream (std::string n, int i)
                : name (n)
-               , id (i)
-               , _legacy_id (false)
+               , _id (i)
        {}
                                
-       FFmpegStream (boost::shared_ptr<const cxml::Node>, int);
+       FFmpegStream (boost::shared_ptr<const cxml::Node>);
  
        void as_xml (xmlpp::Node *) const;
  
        /** @param c An AVFormatContext.
-        *  @return Stream index within the AVFormatContext.
+        *  @param index A stream index within the AVFormatContext.
+        *  @return true if this FFmpegStream uses the given stream index.
         */
-       int index (AVFormatContext const * c) const;
+       bool uses_index (AVFormatContext const * c, int index) const;
        AVStream* stream (AVFormatContext const * c) const;
  
+       std::string technical_summary () const {
+               return "id " + boost::lexical_cast<std::string> (_id);
+       }
+       std::string identifier () const {
+               return boost::lexical_cast<std::string> (_id);
+       }
        std::string name;
-       int id;
+       friend bool operator== (FFmpegStream const & a, FFmpegStream const & b);
+       friend bool operator!= (FFmpegStream const & a, FFmpegStream const & b);
        
  private:
-       /** If this is true, id is in fact the index */
-       bool _legacy_id;
+       int _id;
  };
  
  class FFmpegAudioStream : public FFmpegStream
@@@ -92,9 -101,6 +101,6 @@@ private
        {}
  };
  
- extern bool operator== (FFmpegAudioStream const & a, FFmpegAudioStream const & b);
- extern bool operator!= (FFmpegAudioStream const & a, FFmpegAudioStream const & b);
  class FFmpegSubtitleStream : public FFmpegStream
  {
  public:
                : FFmpegStream (n, i)
        {}
        
-       FFmpegSubtitleStream (boost::shared_ptr<const cxml::Node>, int);
+       FFmpegSubtitleStream (boost::shared_ptr<const cxml::Node>);
  
        void as_xml (xmlpp::Node *) const;
  };
  
- extern bool operator== (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b);
- extern bool operator!= (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b);
  class FFmpegContentProperty : public VideoContentProperty
  {
  public:
@@@ -136,13 -139,13 +139,13 @@@ public
        std::string technical_summary () const;
        std::string information () const;
        void as_xml (xmlpp::Node *) const;
 -      Time full_length () const;
 +      DCPTime full_length () const;
  
        std::string identifier () const;
        
        /* AudioContent */
        int audio_channels () const;
 -      AudioContent::Frame audio_length () const;
 +      AudioFrame audio_length () const;
        int content_audio_frame_rate () const;
        int output_audio_frame_rate () const;
        AudioMapping audio_mapping () const;
index fff9824895565bcc854129424ce8c40133344966,16da64c6014e5998e4fc581d838c9e3419a3c15f..26b713dd56b0f0be8234c5b125c4bd86128b51f0
@@@ -56,7 -56,7 +56,7 @@@ 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)
@@@ -69,6 -69,7 +69,6 @@@
        , _decode_video (video)
        , _decode_audio (audio)
        , _pts_offset (0)
 -      , _just_sought (false)
  {
        setup_subtitle ();
  
           Then we remove big initial gaps in PTS and we allow our
           insertion of black frames to work.
  
 -         We will do:
 -           audio_pts_to_use = audio_pts_from_ffmpeg + pts_offset;
 -           video_pts_to_use = video_pts_from_ffmpeg + pts_offset;
 +         We will do pts_to_use = pts_from_ffmpeg + pts_offset;
        */
  
        bool const have_video = video && c->first_video();
 -      bool const have_audio = audio && c->audio_stream() && c->audio_stream()->first_audio;
 +      bool const have_audio = _decode_audio && c->audio_stream () && c->audio_stream()->first_audio;
  
        /* First, make one of them start at 0 */
  
@@@ -138,10 -141,12 +138,10 @@@ FFmpegDecoder::flush (
                decode_audio_packet ();
        }
  
 -      /* Stop us being asked for any more data */
 -      _video_position = _ffmpeg_content->video_length ();
 -      _audio_position = _ffmpeg_content->audio_length ();
 +      AudioDecoder::flush ();
  }
  
 -void
 +bool
  FFmpegDecoder::pass ()
  {
        int r = av_read_frame (_format_context, &_packet);
                }
  
                flush ();
 -              return;
 +              return true;
        }
  
        shared_ptr<const Film> film = _film.lock ();
        
        if (si == _video_stream && _decode_video) {
                decode_video_packet ();
-       } else if (_ffmpeg_content->audio_stream() && si == _ffmpeg_content->audio_stream()->index (_format_context) && _decode_audio) {
+       } else if (_ffmpeg_content->audio_stream() && _ffmpeg_content->audio_stream()->uses_index (_format_context, si) && _decode_audio) {
                decode_audio_packet ();
-       } else if (_ffmpeg_content->subtitle_stream() && si == _ffmpeg_content->subtitle_stream()->index (_format_context) && film->with_subtitles ()) {
+       } else if (_ffmpeg_content->subtitle_stream() && _ffmpeg_content->subtitle_stream()->uses_index (_format_context, si) && film->with_subtitles ()) {
                decode_subtitle_packet ();
        }
  
        av_free_packet (&_packet);
 +      return false;
  }
  
  /** @param data pointer to array of pointers to buffers.
@@@ -289,135 -293,77 +289,135 @@@ 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;
 +              }
  
 -      if (accurate) {
 -              initial -= 5;
 +              ++frames_read;
 +
 +              double const time_base = av_q2d (_format_context->streams[_packet.stream_index]->time_base);
 +
 +              if (_packet.stream_index == _video_stream) {
 +
 +                      avcodec_get_frame_defaults (_frame);
 +                      
 +                      int finished = 0;
 +                      r = avcodec_decode_video2 (video_codec_context(), _frame, &finished, &_packet);
 +                      if (r >= 0 && finished) {
 +                              last_video = rint (
 +                                      (av_frame_get_best_effort_timestamp (_frame) * time_base + _pts_offset) * TIME_HZ
 +                                      );
 +                      }
 +
 +              } else if (_ffmpeg_content->audio_stream() && _packet.stream_index == _ffmpeg_content->audio_stream()->index (_format_context)) {
 +                      AVPacket copy_packet = _packet;
 +                      while (copy_packet.size > 0) {
 +
 +                              int finished;
 +                              r = avcodec_decode_audio4 (audio_codec_context(), _frame, &finished, &_packet);
 +                              if (r >= 0 && finished) {
 +                                      last_audio = rint (
 +                                              (av_frame_get_best_effort_timestamp (_frame) * time_base + _pts_offset) * TIME_HZ
 +                                              );
 +                              }
 +                                      
 +                              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)
 +{
 +      int64_t s = ((double (t) / TIME_HZ) - _pts_offset) /
 +              av_q2d (_format_context->streams[_video_stream]->time_base);
 +
 +      if (_ffmpeg_content->audio_stream ()) {
 +              s = min (
 +                      s, int64_t (
 +                              ((double (t) / TIME_HZ) - _pts_offset) /
 +                              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--;
  
 -      av_seek_frame (_format_context, _video_stream, initial_vt, AVSEEK_FLAG_BACKWARD);
 +      av_seek_frame (_format_context, _video_stream, s, AVSEEK_FLAG_BACKWARD);
  
        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)
 +{
 +      Decoder::seek (time, accurate);
 +      AudioDecoder::seek (time, accurate);
 +      
 +      /* If we are doing an accurate seek, our initial shot will be 200ms (200 being
 +         a number plucked from the air) earlier than we want to end up.  The loop below
 +         will hopefully then step through to where we want to be.
        */
 -      if (!accurate) {
 -              _just_sought = true;
 +
 +      ContentTime pre_roll = accurate ? (0.2 * TIME_HZ) : 0;
 +      ContentTime initial_seek = time - pre_roll;
 +      if (initial_seek < 0) {
 +              initial_seek = 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;
 -              }
 -
 -              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()
 -                              );
 +      int const N = minimal_run (boost::bind (&FFmpegDecoder::seek_overrun_finished, this, time, _1, _2));
  
 -                      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));
        }
  }
  
@@@ -434,7 -380,6 +434,7 @@@ FFmpegDecoder::decode_audio_packet (
  
                int frame_finished;
                int const decode_result = avcodec_decode_audio4 (audio_codec_context(), _frame, &frame_finished, &copy_packet);
 +
                if (decode_result < 0) {
                        shared_ptr<const Film> film = _film.lock ();
                        assert (film);
                }
  
                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 = (
 +                              av_frame_get_best_effort_timestamp (_frame) *
 +                              av_q2d (_ffmpeg_content->audio_stream()->stream (_format_context)->time_base)
 +                              + _pts_offset
 +                              ) * TIME_HZ;
                        
                        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;
@@@ -474,7 -433,7 +474,7 @@@ 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;
        }
  
                shared_ptr<const Film> film = _film.lock ();
                assert (film);
  
 -              graph.reset (new FilterGraph (_ffmpeg_content, libdcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format));
 +              graph.reset (new FilterGraph (_ffmpeg_content, dcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format));
                _filter_graphs.push_back (graph);
  
                film->log()->log (String::compose (N_("New graph for %1x%2, pixel format %3"), _frame->width, _frame->height, _frame->format));
                }
                
                if (i->second != AV_NOPTS_VALUE) {
 -
                        double const pts = i->second * av_q2d (_format_context->streams[_video_stream]->time_base) + _pts_offset;
 -
 -                      if (_just_sought) {
 -                              /* We just did a seek, so disable any attempts to correct for where we
 -                                 are / should be.
 -                              */
 -                              _video_position = rint (pts * _ffmpeg_content->video_frame_rate ());
 -                              _just_sought = false;
 -                      }
 -
 -                      double const next = _video_position / _ffmpeg_content->video_frame_rate();
 -                      double const one_frame = 1 / _ffmpeg_content->video_frame_rate ();
 -                      double delta = pts - next;
 -
 -                      while (delta > one_frame) {
 -                              /* This PTS is more than one frame forward in time of where we think we should be; emit
 -                                 a black frame.
 -                              */
 -
 -                              /* XXX: I think this should be a copy of the last frame... */
 -                              boost::shared_ptr<Image> black (
 -                                      new Image (
 -                                              static_cast<AVPixelFormat> (_frame->format),
 -                                              libdcp::Size (video_codec_context()->width, video_codec_context()->height),
 -                                              true
 -                                              )
 -                                      );
 -                              
 -                              black->make_black ();
 -                              video (image, false, _video_position);
 -                              delta -= one_frame;
 -                      }
 -
 -                      if (delta > -one_frame) {
 -                              /* This PTS is within a frame of being right; emit this (otherwise it will be dropped) */
 -                              video (image, false, _video_position);
 -                      }
 -                              
 +                      VideoFrame const f = rint (pts * _ffmpeg_content->video_frame_rate ());
 +                      video (image, false, f);
                } else {
                        shared_ptr<const Film> film = _film.lock ();
                        assert (film);
@@@ -521,15 -516,19 +521,19 @@@ FFmpegDecoder::setup_subtitle (
  {
        boost::mutex::scoped_lock lm (_mutex);
        
-       if (!_ffmpeg_content->subtitle_stream() || _ffmpeg_content->subtitle_stream()->index (_format_context) >= int (_format_context->nb_streams)) {
+       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 (_("could not find subtitle decoder"));
+               throw DecodeError (N_("could not find subtitle decoder"));
        }
        
        if (avcodec_open2 (_subtitle_codec_context, _subtitle_codec, 0) < 0) {
        }
  }
  
 -bool
 -FFmpegDecoder::done () const
 -{
 -      bool const vd = !_decode_video || (_video_position >= _ffmpeg_content->video_length());
 -      bool const ad = !_decode_audio || !_ffmpeg_content->audio_stream() || (_audio_position >= _ffmpeg_content->audio_length());
 -      return vd && ad;
 -}
 -      
  void
  FFmpegDecoder::decode_subtitle_packet ()
  {
           indicate that the previous subtitle should stop.
        */
        if (sub.num_rects <= 0) {
 -              subtitle (shared_ptr<Image> (), dcpomatic::Rect<double> (), 0, 0);
 +              image_subtitle (shared_ptr<Image> (), dcpomatic::Rect<double> (), 0, 0);
                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
           source that we may have chopped off for the DCP)
        */
 -      double const packet_time = (static_cast<double> (sub.pts ) / AV_TIME_BASE) + _pts_offset;
 -
 +      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;
 +      ContentTime const from = (packet_time + (double (sub.start_display_time) / 1e3)) * TIME_HZ;
 +      ContentTime const to = (packet_time + (double (sub.end_display_time) / 1e3)) * TIME_HZ;
  
        AVSubtitleRect const * rect = sub.rects[0];
  
        /* Note RGBA is expressed little-endian, so the first byte in the word is R, second
           G, third B, fourth A.
        */
 -      shared_ptr<Image> image (new Image (PIX_FMT_RGBA, libdcp::Size (rect->w, rect->h), true));
 +      shared_ptr<Image> image (new Image (PIX_FMT_RGBA, dcp::Size (rect->w, rect->h), true));
  
        /* Start of the first line in the subtitle */
        uint8_t* sub_p = rect->pict.data[0];
                out_p += image->stride()[0] / sizeof (uint32_t);
        }
  
 -      libdcp::Size const vs = _ffmpeg_content->video_size ();
 +      dcp::Size const vs = _ffmpeg_content->video_size ();
  
 -      subtitle (
 +      image_subtitle (
                image,
                dcpomatic::Rect<double> (
                        static_cast<double> (rect->x) / vs.width,
index 86dec9a8fe40be642de5ce881f1bdd0fa55e29b5,ec090ed6123745bc147ad06ba1f0b7cccc18a38e..e439566a10367ee105f9bbc5311098191931f38a
@@@ -75,13 -75,13 +75,13 @@@ FFmpegExaminer::FFmpegExaminer (shared_
  
                if (_packet.stream_index == _video_stream && !_first_video) {
                        if (avcodec_decode_video2 (context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) {
-                               _first_video = frame_time (_video_stream);
+                               _first_video = frame_time (_format_context->streams[_video_stream]);
                        }
                } else {
                        for (size_t i = 0; i < _audio_streams.size(); ++i) {
-                               if (_packet.stream_index == _audio_streams[i]->index (_format_context) && !_audio_streams[i]->first_audio) {
+                               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]->index (_format_context));
+                                               _audio_streams[i]->first_audio = frame_time (_audio_streams[i]->stream (_format_context));
                                        }
                                }
                        }
  }
  
  optional<double>
- FFmpegExaminer::frame_time (int stream) const
+ FFmpegExaminer::frame_time (AVStream* s) const
  {
        optional<double> t;
        
        int64_t const bet = av_frame_get_best_effort_timestamp (_frame);
        if (bet != AV_NOPTS_VALUE) {
-               t = bet * av_q2d (_format_context->streams[stream]->time_base);
+               t = bet * av_q2d (s->time_base);
        }
  
        return t;
@@@ -127,17 -127,17 +127,17 @@@ FFmpegExaminer::video_frame_rate () con
        return av_q2d (s->r_frame_rate);
  }
  
 -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
 +VideoFrame
  FFmpegExaminer::video_length () const
  {
 -      VideoContent::Frame const length = (double (_format_context->duration) / AV_TIME_BASE) * video_frame_rate();
 +      VideoFrame const length = (double (_format_context->duration) / AV_TIME_BASE) * video_frame_rate();
        return max (1, length);
  }
  
index 40d7dbf1d8358ff2a8eabfe71a7b58187464de3b,369dac29c992748b3b394b1a42ae1da3a4315235..81275a9e1733c4223f9a94610b1a2a6a1748ae7f
@@@ -30,8 -30,8 +30,8 @@@ public
        FFmpegExaminer (boost::shared_ptr<const FFmpegContent>);
        
        float video_frame_rate () const;
 -      libdcp::Size video_size () const;
 -      VideoContent::Frame video_length () const;
 +      dcp::Size video_size () const;
 +      VideoFrame video_length () const;
  
        std::vector<boost::shared_ptr<FFmpegSubtitleStream> > subtitle_streams () const {
                return _subtitle_streams;
@@@ -49,7 -49,7 +49,7 @@@ private
        std::string stream_name (AVStream* s) const;
        std::string audio_stream_name (AVStream* s) const;
        std::string subtitle_stream_name (AVStream* s) const;
-       boost::optional<double> frame_time (int) const;
+       boost::optional<double> frame_time (AVStream* s) const;
        
        std::vector<boost::shared_ptr<FFmpegSubtitleStream> > _subtitle_streams;
        std::vector<boost::shared_ptr<FFmpegAudioStream> > _audio_streams;
diff --combined src/lib/film.cc
index 774c1b392c715c7da834d6b0dd0dce1886477cd1,1d07ec77f4a0ff50ebc105970709cde687c29099..901c512844c9da3585c74fe5149c3a2e1df2b702
@@@ -78,22 -78,22 +78,22 @@@ using boost::to_upper_copy
  using boost::ends_with;
  using boost::starts_with;
  using boost::optional;
 -using libdcp::Size;
 -using libdcp::Signer;
 +using dcp::Size;
 +using dcp::Signer;
  
  /* 5 -> 6
   * AudioMapping XML changed.
   * 6 -> 7
   * Subtitle offset changed to subtitle y offset, and subtitle x offset added.
   */
- int const Film::state_version = 7;
+ int const Film::current_state_version = 7;
  
  /** Construct a Film object in a given directory.
   *
   *  @param dir Film directory.
   */
  
- Film::Film (boost::filesystem::path dir)
+ Film::Film (boost::filesystem::path dir, bool log)
        : _playlist (new Playlist)
        , _use_dci_name (true)
        , _dcp_content_type (Config::instance()->default_dcp_content_type ())
        , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ())
        , _dci_metadata (Config::instance()->default_dci_metadata ())
        , _video_frame_rate (24)
-       , _audio_channels (MAX_AUDIO_CHANNELS)
+       , _audio_channels (6)
        , _three_d (false)
        , _sequence_video (true)
        , _interop (false)
+       , _state_version (current_state_version)
        , _dirty (false)
  {
        set_dci_date_today ();
        }
  
        set_directory (result);
-       _log.reset (new FileLog (file ("log")));
+       if (log) {
+               _log.reset (new FileLog (file ("log")));
+       } else {
+               _log.reset (new NullLog);
+       }
  
        _playlist->set_sequence_video (_sequence_video);
  }
@@@ -327,22 -332,15 +332,15 @@@ Film::encoded_frames () cons
        return N;
  }
  
- /** Write state to our `metadata' file */
- void
- Film::write_metadata () const
+ shared_ptr<xmlpp::Document>
+ Film::metadata () const
  {
-       if (!boost::filesystem::exists (directory ())) {
-               boost::filesystem::create_directory (directory ());
-       }
-       
        LocaleGuard lg;
  
-       boost::filesystem::create_directories (directory ());
-       xmlpp::Document doc;
-       xmlpp::Element* root = doc.create_root_node ("Metadata");
+       shared_ptr<xmlpp::Document> doc (new xmlpp::Document);
+       xmlpp::Element* root = doc->create_root_node ("Metadata");
  
-       root->add_child("Version")->add_child_text (lexical_cast<string> (state_version));
+       root->add_child("Version")->add_child_text (lexical_cast<string> (current_state_version));
        root->add_child("Name")->add_child_text (_name);
        root->add_child("UseDCIName")->add_child_text (_use_dci_name ? "1" : "0");
  
        root->add_child("Key")->add_child_text (_key.hex ());
        _playlist->as_xml (root->add_child ("Playlist"));
  
-       doc.write_to_file_formatted (file("metadata.xml").string ());
-       
+       return doc;
+ }
+ /** Write state to our `metadata' file */
+ void
+ Film::write_metadata () const
+ {
+       boost::filesystem::create_directories (directory ());
+       shared_ptr<xmlpp::Document> doc = metadata ();
+       doc->write_to_file_formatted (file("metadata.xml").string ());
        _dirty = false;
  }
  
@@@ -388,7 -394,7 +394,7 @@@ Film::read_metadata (
        cxml::Document f ("Metadata");
        f.read_file (file ("metadata.xml"));
  
-       int const version = f.number_child<int> ("Version");
+       _state_version = f.number_child<int> ("Version");
        
        _name = f.string_child ("Name");
        _use_dci_name = f.bool_child ("UseDCIName");
        _sequence_video = f.bool_child ("SequenceVideo");
        _three_d = f.bool_child ("ThreeD");
        _interop = f.bool_child ("Interop");
 -      _key = libdcp::Key (f.string_child ("Key"));
 +      _key = dcp::Key (f.string_child ("Key"));
-       _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"), version);
+       _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"), _state_version);
  
        _dirty = false;
  }
@@@ -746,7 -752,7 +752,7 @@@ Film::j2c_path (int f, Eyes e, bool t) 
        return file (p);
  }
  
 -/** @return List of subdirectories (not full paths) containing DCPs that can be successfully libdcp::DCP::read() */
 +/** @return List of subdirectories (not full paths) containing DCPs that can be successfully dcp::DCP::read() */
  list<boost::filesystem::path>
  Film::dcps () const
  {
                        ) {
  
                        try {
 -                              libdcp::DCP dcp (*i);
 +                              dcp::DCP dcp (*i);
                                dcp.read ();
                                out.push_back (i->path().leaf ());
                        } catch (...) {
@@@ -855,7 -861,7 +861,7 @@@ Film::move_content_later (shared_ptr<Co
        _playlist->move_later (c);
  }
  
 -Time
 +DCPTime
  Film::length () const
  {
        return _playlist->length ();
@@@ -867,18 -873,12 +873,18 @@@ Film::has_subtitles () cons
        return _playlist->has_subtitles ();
  }
  
 -OutputVideoFrame
 +VideoFrame
  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)
  {
@@@ -897,31 -897,31 +903,31 @@@ Film::playlist_changed (
        signal_changed (CONTENT);
  }     
  
 -OutputAudioFrame
 -Film::time_to_audio_frames (Time t) const
 +AudioFrame
 +Film::time_to_audio_frames (DCPTime t) const
  {
        return t * audio_frame_rate () / TIME_HZ;
  }
  
 -OutputVideoFrame
 -Film::time_to_video_frames (Time t) const
 +VideoFrame
 +Film::time_to_video_frames (DCPTime t) const
  {
        return t * video_frame_rate () / TIME_HZ;
  }
  
 -Time
 -Film::audio_frames_to_time (OutputAudioFrame f) const
 +DCPTime
 +Film::audio_frames_to_time (AudioFrame f) const
  {
        return f * TIME_HZ / audio_frame_rate ();
  }
  
 -Time
 -Film::video_frames_to_time (OutputVideoFrame f) const
 +DCPTime
 +Film::video_frames_to_time (VideoFrame f) const
  {
        return f * TIME_HZ / video_frame_rate ();
  }
  
 -OutputAudioFrame
 +AudioFrame
  Film::audio_frame_rate () const
  {
        /* XXX */
@@@ -936,23 -936,23 +942,23 @@@ Film::set_sequence_video (bool s
        signal_changed (SEQUENCE_VIDEO);
  }
  
 -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 ();
  }
  
 -libdcp::KDM
 +dcp::KDM
  Film::make_kdm (
 -      shared_ptr<libdcp::Certificate> target,
 +      shared_ptr<dcp::Certificate> target,
        boost::filesystem::path dcp_dir,
        boost::posix_time::ptime from,
        boost::posix_time::ptime until
  {
        shared_ptr<const Signer> signer = make_signer ();
  
 -      libdcp::DCP dcp (dir (dcp_dir.string ()));
 +      dcp::DCP dcp (dir (dcp_dir.string ()));
        
        try {
                dcp.read ();
        
        time_t now = time (0);
        struct tm* tm = localtime (&now);
 -      string const issue_date = libdcp::tm_to_string (tm);
 +      string const issue_date = dcp::tm_to_string (tm);
        
        dcp.cpls().front()->set_mxf_keys (key ());
        
 -      return libdcp::KDM (dcp.cpls().front(), signer, target, from, until, "DCP-o-matic", issue_date);
 +      return dcp::KDM (dcp.cpls().front(), signer, target, from, until, "DCP-o-matic", issue_date);
  }
  
 -list<libdcp::KDM>
 +list<dcp::KDM>
  Film::make_kdms (
        list<shared_ptr<Screen> > screens,
        boost::filesystem::path dcp,
        boost::posix_time::ptime until
        ) const
  {
 -      list<libdcp::KDM> kdms;
 +      list<dcp::KDM> kdms;
  
        for (list<shared_ptr<Screen> >::iterator i = screens.begin(); i != screens.end(); ++i) {
                kdms.push_back (make_kdm ((*i)->certificate, dcp, from, until));
diff --combined src/lib/film.h
index 68916c7b097202509ab77e8f832d416b7ff42c72,7e65ecb16346107efe63da496176f5cf2f014adf..0a474bab7f91b52b530cf12cb261da437e22f545
@@@ -56,7 -56,7 +56,7 @@@ class Screen
  class Film : public boost::enable_shared_from_this<Film>, public boost::noncopyable
  {
  public:
-       Film (boost::filesystem::path);
+       Film (boost::filesystem::path, bool log = true);
  
        boost::filesystem::path info_dir () const;
        boost::filesystem::path j2c_path (int, Eyes, bool) const;
@@@ -85,6 -85,7 +85,7 @@@
  
        void read_metadata ();
        void write_metadata () const;
+       boost::shared_ptr<xmlpp::Document> metadata () const;
  
        std::string dci_name (bool if_created_now) const;
        std::string dcp_name (bool if_created_now = false) const;
                return _dirty;
        }
  
 -      libdcp::Size full_frame () const;
 +      dcp::Size full_frame () const;
  
        std::list<boost::filesystem::path> dcps () const;
  
        boost::shared_ptr<Player> make_player () const;
        boost::shared_ptr<Playlist> playlist () const;
  
 -      OutputAudioFrame audio_frame_rate () const;
 +      AudioFrame 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;
 +      AudioFrame time_to_audio_frames (DCPTime) const;
 +      VideoFrame time_to_video_frames (DCPTime) const;
 +      DCPTime video_frames_to_time (VideoFrame) const;
 +      DCPTime audio_frames_to_time (AudioFrame) 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;
 +      VideoFrame best_video_frame_rate () const;
 +      FrameRateChange active_frame_rate_change (DCPTime) const;
  
 -      libdcp::KDM
 +      dcp::KDM
        make_kdm (
 -              boost::shared_ptr<libdcp::Certificate> target,
 +              boost::shared_ptr<dcp::Certificate> target,
                boost::filesystem::path dcp,
                boost::posix_time::ptime from,
                boost::posix_time::ptime until
                ) const;
        
 -      std::list<libdcp::KDM> make_kdms (
 +      std::list<dcp::KDM> make_kdms (
                std::list<boost::shared_ptr<Screen> >,
                boost::filesystem::path dcp,
                boost::posix_time::ptime from,
                boost::posix_time::ptime until
                ) const;
  
 -      libdcp::Key key () const {
 +      dcp::Key key () const {
                return _key;
        }
  
+       int state_version () const {
+               return _state_version;
+       }
        /** Identifiers for the parts of our state;
            used for signalling changes.
        */
        mutable boost::signals2::signal<void (boost::weak_ptr<Content>, int)> ContentChanged;
  
        /** Current version number of the state file */
-       static int const state_version;
+       static int const current_state_version;
  
  private:
  
        bool _three_d;
        bool _sequence_video;
        bool _interop;
 -      libdcp::Key _key;
 +      dcp::Key _key;
  
+       int _state_version;
        /** true if our state has changed since we last saved it */
        mutable bool _dirty;
  
diff --combined src/lib/job.cc
index ce97ba2b2f13c6e2de3a8b69346fd752893995db,76976df322c1c3778bcac915709ea39a871f5d68..a312e738124d93b2e6aa518df0ce0db790f68a17
@@@ -66,7 -66,7 +66,7 @@@ 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());
  
@@@ -204,7 -204,7 +204,7 @@@ Job::set_state (State s
        }
  }
  
 -/** @return Time (in seconds) that this sub-job has been running */
 +/** @return DCPTime (in seconds) that this sub-job has been running */
  int
  Job::elapsed_time () const
  {
@@@ -239,7 -239,7 +239,7 @@@ Job::set_progress (float p, bool force
        }
  }
  
- /** @return fractional progress of this sub-job, or -1 if not known */
+ /** @return fractional progress of the current sub-job, or -1 if not known */
  float
  Job::progress () const
  {
@@@ -325,6 -325,29 +325,29 @@@ Job::status () cons
        return s.str ();
  }
  
+ string
+ Job::json_status () const
+ {
+       boost::mutex::scoped_lock lm (_state_mutex);
+       switch (_state) {
+       case NEW:
+               return N_("new");
+       case RUNNING:
+               return N_("running");
+       case PAUSED:
+               return N_("paused");
+       case FINISHED_OK:
+               return N_("finished_ok");
+       case FINISHED_ERROR:
+               return N_("finished_error");
+       case FINISHED_CANCELLED:
+               return N_("finished_cancelled");
+       }
+       return "";
+ }
  /** @return An estimate of the remaining time for this sub-job, in seconds */
  int
  Job::remaining_time () const
diff --combined src/lib/transcode_job.cc
index 46fc97fb31b74bdf14f6093dd0d973e34fa78709,7b304cb35b48d549e683bd403ec8b5d923b0e4e9..a0537cd428b343f2190bd07a57bdaee8ad65953c
@@@ -50,6 -50,12 +50,12 @@@ TranscodeJob::name () cons
        return String::compose (_("Transcode %1"), _film->name());
  }
  
+ string
+ TranscodeJob::json_name () const
+ {
+       return N_("transcode");
+ }
  void
  TranscodeJob::run ()
  {
@@@ -114,6 -120,6 +120,6 @@@ TranscodeJob::remaining_time () cons
        }
  
        /* 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();
 +      VideoFrame const left = _film->time_to_video_frames (_film->length ()) - t->video_frames_out();
        return left / fps;
  }
diff --combined src/lib/util.cc
index fd3a318b03419c043787db235f990efc638f3729,25fbc130b1be445a3e71df42459f4433970568e1..63b1a5395f23ac6036cc35a93b55ce0849451f2f
@@@ -48,7 -48,6 +48,7 @@@
  #include <openssl/md5.h>
  #include <magick/MagickCore.h>
  #include <magick/version.h>
 +#include <pangomm/init.h>
  #include <libdcp/version.h>
  #include <libdcp/util.h>
  #include <libdcp/signer_chain.h>
@@@ -90,6 -89,7 +90,7 @@@ using std::min
  using std::max;
  using std::list;
  using std::multimap;
+ using std::map;
  using std::istream;
  using std::numeric_limits;
  using std::pair;
@@@ -101,7 -101,7 +102,7 @@@ using boost::shared_ptr
  using boost::thread;
  using boost::lexical_cast;
  using boost::optional;
 -using libdcp::Size;
 +using dcp::Size;
  
  static boost::thread::id ui_thread;
  static boost::filesystem::path backtrace_file;
@@@ -251,7 -251,7 +252,7 @@@ dependency_version_summary (
          << N_("libswscale ") << ffmpeg_version_to_string (swscale_version()) << N_(", ")
          << MagickVersion << N_(", ")
          << N_("libssh ") << ssh_version (0) << N_(", ")
 -        << N_("libdcp ") << libdcp::version << N_(" git ") << libdcp::git_commit;
 +        << N_("libdcp ") << dcp::version << N_(" git ") << dcp::git_commit;
  
        return s.str ();
  }
@@@ -342,8 -342,7 +343,8 @@@ dcpomatic_setup (
  
        set_terminate (terminate);
  
 -      libdcp::init ();
 +      Pango::init ();
 +      dcp::init ();
        
        Ratio::setup_ratios ();
        DCPContentType::setup_dcp_content_types ();
@@@ -783,7 -782,7 +784,7 @@@ ensure_ui_thread (
   *  @return Equivalent number of audio frames for `v'.
   */
  int64_t
 -video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second)
 +video_frames_to_audio_frames (VideoFrame v, float audio_sample_rate, float frames_per_second)
  {
        return ((int64_t) v * audio_sample_rate / frames_per_second);
  }
  string
  audio_channel_name (int c)
  {
-       assert (MAX_AUDIO_CHANNELS == 6);
+       assert (MAX_AUDIO_CHANNELS == 8);
  
        /* TRANSLATORS: these are the names of audio channels; Lfe (sub) is the low-frequency
-          enhancement channel (sub-woofer).
+          enhancement channel (sub-woofer).  HI is the hearing-impaired audio track and
+          VI is the visually-impaired audio track (audio describe).
        */
        string const channels[] = {
                _("Left"),
                _("Lfe (sub)"),
                _("Left surround"),
                _("Right surround"),
+               _("HI"),
+               _("VI")
        };
  
        return channels[c];
  }
  
 -FrameRateConversion::FrameRateConversion (float source, int dcp)
 +FrameRateChange::FrameRateChange (float source, int dcp)
        : skip (false)
        , repeat (1)
        , change_speed (false)
                repeat = round (dcp / source);
        }
  
 -      change_speed = !about_equal (source * factor(), dcp);
 +      speed_up = dcp / (source * factor());
 +      change_speed = !about_equal (speed_up, 1.0);
  
        if (!skip && repeat == 1 && !change_speed) {
                description = _("Content and DCP have the same rate.\n");
@@@ -889,7 -890,7 +893,7 @@@ tidy_for_filename (string f
        return t;
  }
  
 -shared_ptr<const libdcp::Signer>
 +shared_ptr<const dcp::Signer>
  make_signer ()
  {
        boost::filesystem::path const sd = Config::instance()->signer_chain_directory ();
                if (!boost::filesystem::exists (p)) {
                        boost::filesystem::remove_all (sd);
                        boost::filesystem::create_directories (sd);
 -                      libdcp::make_signer_chain (sd, openssl_path ());
 +                      dcp::make_signer_chain (sd, openssl_path ());
                        break;
                }
  
                ++i;
        }
        
 -      libdcp::CertificateChain chain;
 +      dcp::CertificateChain chain;
  
        {
                boost::filesystem::path p (sd);
                p /= "ca.self-signed.pem";
 -              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p)));
 +              chain.add (shared_ptr<dcp::Certificate> (new dcp::Certificate (p)));
        }
  
        {
                boost::filesystem::path p (sd);
                p /= "intermediate.signed.pem";
 -              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p)));
 +              chain.add (shared_ptr<dcp::Certificate> (new dcp::Certificate (p)));
        }
  
        {
                boost::filesystem::path p (sd);
                p /= "leaf.signed.pem";
 -              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p)));
 +              chain.add (shared_ptr<dcp::Certificate> (new dcp::Certificate (p)));
        }
  
        boost::filesystem::path signer_key (sd);
        signer_key /= "leaf.key";
  
 -      return shared_ptr<const libdcp::Signer> (new libdcp::Signer (chain, signer_key));
 +      return shared_ptr<const dcp::Signer> (new dcp::Signer (chain, signer_key));
  }
  
- dcp::Size
- fit_ratio_within (float ratio, dcp::Size full_frame)
+ map<string, string>
+ split_get_request (string url)
+ {
+       enum {
+               AWAITING_QUESTION_MARK,
+               KEY,
+               VALUE
+       } state = AWAITING_QUESTION_MARK;
+       
+       map<string, string> r;
+       string k;
+       string v;
+       for (size_t i = 0; i < url.length(); ++i) {
+               switch (state) {
+               case AWAITING_QUESTION_MARK:
+                       if (url[i] == '?') {
+                               state = KEY;
+                       }
+                       break;
+               case KEY:
+                       if (url[i] == '=') {
+                               v.clear ();
+                               state = VALUE;
+                       } else {
+                               k += url[i];
+                       }
+                       break;
+               case VALUE:
+                       if (url[i] == '&') {
+                               r.insert (make_pair (k, v));
+                               k.clear ();
+                               state = KEY;
+                       } else {
+                               v += url[i];
+                       }
+                       break;
+               }
+       }
+       if (state == VALUE) {
+               r.insert (make_pair (k, v));
+       }
+       return r;
+ }
+ libdcp::Size
+ fit_ratio_within (float ratio, libdcp::Size full_frame)
  {
        if (ratio < full_frame.ratio ()) {
 -              return libdcp::Size (rint (full_frame.height * ratio), full_frame.height);
 +              return dcp::Size (rint (full_frame.height * ratio), full_frame.height);
        }
        
 -      return libdcp::Size (full_frame.width, rint (full_frame.width / ratio));
 +      return dcp::Size (full_frame.width, rint (full_frame.width / ratio));
 +}
 +
 +DCPTime
 +time_round_up (DCPTime t, DCPTime nearest)
 +{
 +      DCPTime const a = t + nearest - 1;
 +      return a - (a % nearest);
  }
  
  void *
@@@ -968,3 -1008,11 +1018,11 @@@ wrapped_av_malloc (size_t s
        }
        return p;
  }
+               
+ string
+ entities_to_text (string e)
+ {
+       boost::algorithm::replace_all (e, "%3A", ":");
+       boost::algorithm::replace_all (e, "%2F", "/");
+       return e;
+ }
diff --combined src/lib/util.h
index b89c71eee096de3092ef5b7217892f271088a863,ef29cc08f27f3ff411d74904b99632d70548cda1..76dbda190d7b2960c335014cc56626085c816f15
@@@ -32,7 -32,6 +32,7 @@@
  #include <boost/optional.hpp>
  #include <boost/filesystem.hpp>
  #include <libdcp/util.h>
 +#include <libdcp/signer.h>
  extern "C" {
  #include <libavcodec/avcodec.h>
  #include <libavfilter/avfilter.h>
@@@ -50,7 -49,7 +50,7 @@@
  #undef check
  
  /** The maximum number of audio channels that we can cope with */
- #define MAX_AUDIO_CHANNELS 6
+ #define MAX_AUDIO_CHANNELS 8
  
  #define DCPOMATIC_HELLO "Boys, you gotta learn not to talk to nuns that way"
  
@@@ -77,12 -76,14 +77,14 @@@ 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
 +struct FrameRateChange
  {
 -      FrameRateConversion (float, int);
 +      FrameRateChange (float, int);
  
        /** @return factor by which to multiply a source frame rate
            to get the effective rate after any skip or repeat has happened.
         */
        bool change_speed;
  
 +      /** Amount by which the video is being sped-up in the DCP; e.g. for a
 +       *  24fps source in a 25fps DCP this would be 25/24.
 +       */
 +      float speed_up;
 +
        std::string description;
  };
  
  extern int dcp_audio_frame_rate (int);
  extern int stride_round_up (int, int const *, int);
 +extern DCPTime time_round_up (DCPTime, DCPTime);
  extern std::multimap<std::string, std::string> read_key_value (std::istream& s);
  extern int get_required_int (std::multimap<std::string, std::string> const & kv, std::string k);
  extern float get_required_float (std::multimap<std::string, std::string> const & kv, std::string k);
@@@ -168,7 -163,7 +170,7 @@@ private
        int _timeout;
  };
  
 -extern int64_t video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second);
 +extern int64_t video_frames_to_audio_frames (VideoFrame v, float audio_sample_rate, float frames_per_second);
  
  class LocaleGuard
  {
diff --combined src/lib/wscript
index a5b069184024ba981c09f8a2e4e65f26749af194,8702adebbd2de8877b044fb132ff82b40414e0c5..688f4047d6b564ca6d03a4535cd6b58762daa678
@@@ -37,11 -37,11 +37,12 @@@ sources = ""
            job.cc
            job_manager.cc
            kdm.cc
+           json_server.cc
            log.cc
            player.cc
            playlist.cc
            ratio.cc
 +          render_subtitles.cc
            resampler.cc
            scp_dcp_job.cc
            scaler.cc
@@@ -51,9 -51,6 +52,9 @@@
            sndfile_content.cc
            sndfile_decoder.cc
            sound_processor.cc
 +          subrip.cc
 +          subrip_content.cc
 +          subrip_decoder.cc
            subtitle_content.cc
            subtitle_decoder.cc
            timer.cc
@@@ -80,7 -77,7 +81,7 @@@ def build(bld)
                   AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE SWRESAMPLE 
                   BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2
                   SNDFILE OPENJPEG POSTPROC TIFF MAGICK SSH DCP CXML GLIB LZMA XML++
 -                 CURL ZIP QUICKMAIL
 +                 CURL ZIP QUICKMAIL PANGOMM CAIROMM
                   """
  
      obj.source = sources + ' version.cc'
index 75c8ea5de0fcf2d78b5a9be421019badc69809bb,fe32192610afc0a6afb853526d4a8d863fbb7614..86f0399003787643955d7f66cf5df7ec9acb1f9c
@@@ -156,7 -156,7 +156,7 @@@ AudioMappingView::left_click (wxGridEve
                return;
        }
  
 -      libdcp::Channel d = static_cast<libdcp::Channel> (ev.GetCol() - 1);
 +      dcp::Channel d = static_cast<dcp::Channel> (ev.GetCol() - 1);
        
        if (_map.get (ev.GetRow(), d) > 0) {
                _map.set (ev.GetRow(), d, 0);
@@@ -182,28 -182,28 +182,28 @@@ AudioMappingView::right_click (wxGridEv
  void
  AudioMappingView::off ()
  {
 -      _map.set (_menu_row, static_cast<libdcp::Channel> (_menu_column - 1), 0);
 +      _map.set (_menu_row, static_cast<dcp::Channel> (_menu_column - 1), 0);
        map_changed ();
  }
  
  void
  AudioMappingView::full ()
  {
 -      _map.set (_menu_row, static_cast<libdcp::Channel> (_menu_column - 1), 1);
 +      _map.set (_menu_row, static_cast<dcp::Channel> (_menu_column - 1), 1);
        map_changed ();
  }
  
  void
  AudioMappingView::minus3dB ()
  {
 -      _map.set (_menu_row, static_cast<libdcp::Channel> (_menu_column - 1), 1 / sqrt (2));
 +      _map.set (_menu_row, static_cast<dcp::Channel> (_menu_column - 1), 1 / sqrt (2));
        map_changed ();
  }
  
  void
  AudioMappingView::edit ()
  {
 -      libdcp::Channel d = static_cast<libdcp::Channel> (_menu_column - 1);
 +      dcp::Channel d = static_cast<dcp::Channel> (_menu_column - 1);
        
        AudioGainDialog* dialog = new AudioGainDialog (this, _menu_row, _menu_column - 1, _map.get (_menu_row, d));
        if (dialog->ShowModal () == wxID_OK) {
@@@ -242,7 -242,7 +242,7 @@@ AudioMappingView::update_cells (
                _grid->SetCellValue (i, 0, wxString::Format (wxT("%d"), i + 1));
  
                for (int j = 1; j < _grid->GetNumberCols(); ++j) {
 -                      _grid->SetCellValue (i, j, std_to_wx (lexical_cast<string> (_map.get (i, static_cast<libdcp::Channel> (j - 1)))));
 +                      _grid->SetCellValue (i, j, std_to_wx (lexical_cast<string> (_map.get (i, static_cast<dcp::Channel> (j - 1)))));
                }
        }
  
@@@ -271,6 -271,10 +271,10 @@@ AudioMappingView::set_column_labels (
        int const c = _grid->GetNumberCols ();
        
        _grid->SetColLabelValue (0, _("Content channel"));
+ #if MAX_AUDIO_CHANNELS != 8
+ #warning AudioMappingView::set_column_labels() is expecting the wrong MAX_AUDIO_CHANNELS
+ #endif        
        
        if (c > 0) {
                _grid->SetColLabelValue (1, _("L"));
                _grid->SetColLabelValue (6, _("Rs"));
        }
  
+       if (c > 6) {
+               _grid->SetColLabelValue (7, _("HI"));
+       }
+       if (c > 7) {
+               _grid->SetColLabelValue (8, _("VI"));
+       }
+       
        _grid->AutoSize ();
  }
  
@@@ -318,7 -330,7 +330,7 @@@ AudioMappingView::mouse_moved (wxMouseE
        if (row != _last_tooltip_row || column != _last_tooltip_column) {
  
                wxString s;
 -              float const gain = _map.get (row, static_cast<libdcp::Channel> (column - 1));
 +              float const gain = _map.get (row, static_cast<dcp::Channel> (column - 1));
                if (gain == 0) {
                        s = wxString::Format (_("No audio will be passed from content channel %d to DCP channel %d."), row + 1, column);
                } else if (gain == 1) {
diff --combined test/stream_test.cc
index c8073c1c8032342a2c4b807c3ad774860b6ce48e,fed3ecabeb99283bcad12a4e0f542c5bef664b2c..1cd7e4a4270f60144ebd706cc15468c9193710e6
@@@ -67,17 -67,17 +67,17 @@@ BOOST_AUTO_TEST_CASE (stream_test
                
        FFmpegAudioStream a (shared_ptr<cxml::Node> (new cxml::Node (root)), 5);
  
-       BOOST_CHECK_EQUAL (a.id, 4);
+       BOOST_CHECK_EQUAL (a.identifier(), "4");
        BOOST_CHECK_EQUAL (a.frame_rate, 44100);
        BOOST_CHECK_EQUAL (a.channels, 2);
        BOOST_CHECK_EQUAL (a.name, "hello there world");
        BOOST_CHECK_EQUAL (a.mapping.content_channels(), 2);
  
 -      BOOST_CHECK_EQUAL (a.mapping.get (0, libdcp::LEFT), 1);
 -      BOOST_CHECK_EQUAL (a.mapping.get (0, libdcp::RIGHT), 0);
 -      BOOST_CHECK_EQUAL (a.mapping.get (0, libdcp::CENTRE), 1);
 -      BOOST_CHECK_EQUAL (a.mapping.get (1, libdcp::LEFT), 0);
 -      BOOST_CHECK_EQUAL (a.mapping.get (1, libdcp::RIGHT), 1);
 -      BOOST_CHECK_EQUAL (a.mapping.get (1, libdcp::CENTRE), 1);
 +      BOOST_CHECK_EQUAL (a.mapping.get (0, dcp::LEFT), 1);
 +      BOOST_CHECK_EQUAL (a.mapping.get (0, dcp::RIGHT), 0);
 +      BOOST_CHECK_EQUAL (a.mapping.get (0, dcp::CENTRE), 1);
 +      BOOST_CHECK_EQUAL (a.mapping.get (1, dcp::LEFT), 0);
 +      BOOST_CHECK_EQUAL (a.mapping.get (1, dcp::RIGHT), 1);
 +      BOOST_CHECK_EQUAL (a.mapping.get (1, dcp::CENTRE), 1);
  }
  
diff --combined wscript
index ebf444c8f6dc1566b7f51ab6c58bd91a4c66b483,64616eb73266c5306ddcd37f201dadc289e8c990..708e48910977ceb7c68017281e735b5293bb4eb9
+++ b/wscript
@@@ -3,7 -3,7 +3,7 @@@ import o
  import sys
  
  APPNAME = 'dcpomatic'
 -VERSION = '1.64.15devel'
 +VERSION = '2.0.0devel'
  
  def options(opt):
      opt.load('compiler_cxx')
@@@ -55,9 -55,9 +55,9 @@@ def dynamic_openjpeg(conf)
      conf.check_cfg(package='libopenjpeg', args='--cflags --libs', max_version='1.5.1', mandatory=True)
  
  def static_dcp(conf, static_boost, static_xmlpp, static_xmlsec, static_ssh):
--    conf.check_cfg(package='libdcp', atleast_version='0.92', args='--cflags', uselib_store='DCP', mandatory=True)
++    conf.check_cfg(package='libdcp-1.0', atleast_version='0.92', args='--cflags', uselib_store='DCP', mandatory=True)
      conf.env.DEFINES_DCP = [f.replace('\\', '') for f in conf.env.DEFINES_DCP]
--    conf.env.STLIB_DCP = ['dcp', 'asdcp-libdcp', 'kumu-libdcp']
++    conf.env.STLIB_DCP = ['dcp-1.0', 'asdcp-libdcp', 'kumu-libdcp']
      conf.env.LIB_DCP = ['glibmm-2.4', 'ssl', 'crypto', 'bz2', 'xslt']
  
      if static_boost:
@@@ -81,7 -81,7 +81,7 @@@
          conf.env.LIB_DCP.append('ssh')
  
  def dynamic_dcp(conf):
--    conf.check_cfg(package='libdcp', atleast_version='0.92', args='--cflags --libs', uselib_store='DCP', mandatory=True)
++    conf.check_cfg(package='libdcp-1.0', atleast_version='0.92', args='--cflags --libs', uselib_store='DCP', mandatory=True)
      conf.env.DEFINES_DCP = [f.replace('\\', '') for f in conf.env.DEFINES_DCP]
  
  def dynamic_ssh(conf):
@@@ -296,8 -296,6 +296,8 @@@ def configure(conf)
      conf.check_cfg(package='glib-2.0', args='--cflags --libs', uselib_store='GLIB', mandatory=True)
      conf.check_cfg(package= '', path=conf.options.magickpp_config, args='--cppflags --cxxflags --libs', uselib_store='MAGICK', mandatory=True)
      conf.check_cfg(package='libzip', args='--cflags --libs', uselib_store='ZIP', mandatory=True)
 +    conf.check_cfg(package='pangomm-1.4', args='--cflags --libs', uselib_store='PANGOMM', mandatory=True)
 +    conf.check_cfg(package='cairomm-1.0', args='--cflags --libs', uselib_store='CAIROMM', mandatory=True)
  
      conf.check_cc(fragment="""
                             #include <glib.h>